From 0c963da1fd8a92cb3a09e9446b8406d563e354df Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 14 Feb 2017 23:10:36 -0800 Subject: [PATCH 001/297] Fix IPv6 address parsing and validation. --- lib/src/config/config.rs | 9 ++++----- lib/src/config/mod.rs | 16 ++++++++++++---- lib/src/lib.rs | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 9131352a..65076166 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::net::ToSocketAddrs; +use std::net::{IpAddr, lookup_host}; use std::path::{Path, PathBuf}; use std::sync::RwLock; use std::convert::AsRef; @@ -268,19 +268,18 @@ impl Config { /// # fn config_test() -> Result<(), ConfigError> { /// let mut config = Config::new(Environment::Staging)?; /// assert!(config.set_address("localhost").is_ok()); + /// assert!(config.set_address("::").is_ok()); /// assert!(config.set_address("?").is_err()); /// # Ok(()) /// # } /// ``` pub fn set_address>(&mut self, address: A) -> config::Result<()> { let address = address.into(); - if address.contains(':') { - return Err(self.bad_type("address", "string", "a hostname or IP with no port")); - } else if format!("{}:{}", address, 80).to_socket_addrs().is_err() { + if address.parse::().is_err() && lookup_host(&address).is_err() { return Err(self.bad_type("address", "string", "a valid hostname or IP")); } - self.address = address.into(); + self.address = address; Ok(()) } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index cc609a3c..6bdbab95 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -638,11 +638,19 @@ mod test { default_config(Development).address("localhost") }); + + check_config!(RocketConfig::parse(r#" + [development] + address = "::" + "#.to_string(), TEST_CONFIG_FILENAME), { + default_config(Development).address("::") + }); + check_config!(RocketConfig::parse(r#" [dev] - address = "127.0.0.1" + address = "2001:db8::370:7334" "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("127.0.0.1") + default_config(Development).address("2001:db8::370:7334") }); check_config!(RocketConfig::parse(r#" @@ -930,9 +938,9 @@ mod test { check_config!(RocketConfig::parse(format!(r#" [{}] - address = "7.6.5.4" + address = "::1" "#, GLOBAL_ENV_NAME), TEST_CONFIG_FILENAME), { - default_config(*env).address("7.6.5.4") + default_config(*env).address("::1") }); check_config!(RocketConfig::parse(format!(r#" diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a39e9ef1..06df88b8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -5,6 +5,7 @@ #![feature(const_fn)] #![feature(type_ascription)] #![feature(pub_restricted)] +#![feature(lookup_host)] //! # Rocket - Core API Documentation //! From d8b90ebf5f6b9e432a677496effd6e3b746042a4 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 14 Feb 2017 23:34:27 -0800 Subject: [PATCH 002/297] Readd missing config address test. --- lib/src/config/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 6bdbab95..113d92c5 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -638,6 +638,12 @@ mod test { default_config(Development).address("localhost") }); + check_config!(RocketConfig::parse(r#" + [development] + address = "127.0.0.1" + "#.to_string(), TEST_CONFIG_FILENAME), { + default_config(Development).address("127.0.0.1") + }); check_config!(RocketConfig::parse(r#" [development] From d8afb4c7fae27c28afcebcd9bfadb7f299959532 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 15 Feb 2017 01:32:53 -0800 Subject: [PATCH 003/297] Implement Display and Error for ConfigError. Closes #189. --- lib/src/config/error.rs | 44 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/src/config/error.rs b/lib/src/config/error.rs index f125724f..8a814de2 100644 --- a/lib/src/config/error.rs +++ b/lib/src/config/error.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; +use std::error::Error; +use std::fmt; use super::Environment; +use self::ConfigError::*; use term_painter::Color::White; use term_painter::ToStyle; @@ -56,8 +59,6 @@ pub enum ConfigError { impl ConfigError { /// Prints this configuration error with Rocket formatting. pub fn pretty_print(&self) { - use self::ConfigError::*; - let valid_envs = Environment::valid(); match *self { BadCWD => error!("couldn't get current working directory"), @@ -106,10 +107,47 @@ impl ConfigError { /// Whether this error is of `NotFound` variant. #[inline(always)] pub fn is_not_found(&self) -> bool { - use self::ConfigError::*; match *self { NotFound => true, _ => false } } } + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + BadCWD => write!(f, "couldn't get current working directory"), + NotFound => write!(f, "config file was not found"), + IOError => write!(f, "I/O error while reading the config file"), + BadFilePath(ref p, _) => write!(f, "{:?} is not a valid config path", p), + BadEnv(ref e) => write!(f, "{:?} is not a valid `ROCKET_ENV` value", e), + ParseError(..) => write!(f, "the config file contains invalid TOML"), + BadEntry(ref e, _) => { + write!(f, "{:?} is not a valid `[environment]` entry", e) + } + BadType(ref n, e, a, _) => { + write!(f, "type mismatch for '{}'. expected {}, found {}", n, e, a) + } + BadEnvVal(ref k, ref v, _) => { + write!(f, "environment variable '{}={}' could not be parsed", k, v) + } + } + } +} + +impl Error for ConfigError { + fn description(&self) -> &str { + match *self { + BadCWD => "the current working directory could not be determined", + NotFound => "config file was not found", + IOError => "there was an I/O error while reading the config file", + BadFilePath(..) => "the config file path is invalid", + BadEntry(..) => "an environment specified as `[environment]` is invalid", + BadEnv(..) => "the environment specified in `ROCKET_ENV` is invalid", + ParseError(..) => "the config file contains invalid TOML", + BadType(..) => "a key was specified with a value of the wrong type", + BadEnvVal(..) => "an environment variable could not be parsed", + } + } +} From 6184d946194264d7b2a919313d75bbd775839753 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 16 Feb 2017 18:06:09 -0800 Subject: [PATCH 004/297] Expose docstring for JSON Responder impl. --- contrib/src/json/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/src/json/mod.rs b/contrib/src/json/mod.rs index 52744ba9..f8842219 100644 --- a/contrib/src/json/mod.rs +++ b/contrib/src/json/mod.rs @@ -89,9 +89,9 @@ impl FromData for JSON { } } -// Serializes the wrapped value into JSON. Returns a response with Content-Type -// JSON and a fixed-size body with the serialization. If serialization fails, an -// `Err` of `Status::InternalServerError` is returned. +/// Serializes the wrapped value into JSON. Returns a response with Content-Type +/// JSON and a fixed-size body with the serialized value. If serialization +/// fails, an `Err` of `Status::InternalServerError` is returned. impl Responder<'static> for JSON { fn respond(self) -> response::Result<'static> { serde_json::to_string(&self.0).map(|string| { From 3e063af965ece6fac3376eb78646eebad92ab439 Mon Sep 17 00:00:00 2001 From: Crazy-Owl Date: Mon, 13 Feb 2017 13:28:31 +0300 Subject: [PATCH 005/297] Use managed state in json example. --- examples/json/Cargo.toml | 1 - examples/json/src/main.rs | 26 ++++++++++++++------------ examples/json/src/tests.rs | 29 ++++++++++++++--------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 310b344f..d95ee879 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -9,7 +9,6 @@ rocket_codegen = { path = "../../codegen" } serde = "0.9" serde_json = "0.9" serde_derive = "0.9" -lazy_static = "*" [dependencies.rocket_contrib] path = "../../contrib" diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 50de2eb0..1b1fb424 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -3,13 +3,13 @@ extern crate rocket; extern crate serde_json; -#[macro_use] extern crate lazy_static; #[macro_use] extern crate rocket_contrib; #[macro_use] extern crate serde_derive; #[cfg(test)] mod tests; use rocket_contrib::{JSON, Value}; +use rocket::State; use std::collections::HashMap; use std::sync::Mutex; @@ -17,9 +17,7 @@ use std::sync::Mutex; type ID = usize; // We're going to store all of the messages here. No need for a DB. -lazy_static! { - static ref MAP: Mutex> = Mutex::new(HashMap::new()); -} +type MessageMap = Mutex>; #[derive(Serialize, Deserialize)] struct Message { @@ -29,8 +27,8 @@ struct Message { // TODO: This example can be improved by using `route` with muliple HTTP verbs. #[post("/", format = "application/json", data = "")] -fn new(id: ID, message: JSON) -> JSON { - let mut hashmap = MAP.lock().expect("map lock."); +fn new(id: ID, message: JSON, map: State) -> JSON { + let mut hashmap = map.lock().expect("map lock."); if hashmap.contains_key(&id) { JSON(json!({ "status": "error", @@ -43,8 +41,8 @@ fn new(id: ID, message: JSON) -> JSON { } #[put("/", format = "application/json", data = "")] -fn update(id: ID, message: JSON) -> Option> { - let mut hashmap = MAP.lock().unwrap(); +fn update(id: ID, message: JSON, map: State) -> Option> { + let mut hashmap = map.lock().unwrap(); if hashmap.contains_key(&id) { hashmap.insert(id, message.0.contents); Some(JSON(json!({ "status": "ok" }))) @@ -54,8 +52,8 @@ fn update(id: ID, message: JSON) -> Option> { } #[get("/", format = "application/json")] -fn get(id: ID) -> Option> { - let hashmap = MAP.lock().unwrap(); +fn get(id: ID, map: State) -> Option> { + let hashmap = map.lock().unwrap(); hashmap.get(&id).map(|contents| { JSON(Message { id: Some(id), @@ -72,9 +70,13 @@ fn not_found() -> JSON { })) } -fn main() { +fn rocket() -> rocket::Rocket { rocket::ignite() .mount("/message", routes![new, update, get]) .catch(errors![not_found]) - .launch(); + .manage(Mutex::new(HashMap::::new())) +} + +fn main() { + rocket().launch(); } diff --git a/examples/json/src/tests.rs b/examples/json/src/tests.rs index 4476ed0c..dc3a4cdb 100644 --- a/examples/json/src/tests.rs +++ b/examples/json/src/tests.rs @@ -5,21 +5,19 @@ use rocket::http::{Status, ContentType}; use rocket::Response; macro_rules! run_test { - ($req:expr, $test_fn:expr) => ({ - let rocket = rocket::ignite() - .mount("/message", routes![super::new, super::update, super::get]) - .catch(errors![super::not_found]); - + ($rocket: expr, $req:expr, $test_fn:expr) => ({ let mut req = $req; - $test_fn(req.dispatch_with(&rocket)); + $test_fn(req.dispatch_with($rocket)); }) } #[test] fn bad_get_put() { + let rocket = rocket(); + // Try to get a message with an ID that doesn't exist. let req = MockRequest::new(Get, "/message/99").header(ContentType::JSON); - run_test!(req, |mut response: Response| { + run_test!(&rocket, req, |mut response: Response| { assert_eq!(response.status(), Status::NotFound); let body = response.body().unwrap().into_string().unwrap(); @@ -29,7 +27,7 @@ fn bad_get_put() { // Try to get a message with an invalid ID. let req = MockRequest::new(Get, "/message/hi").header(ContentType::JSON); - run_test!(req, |mut response: Response| { + run_test!(&rocket, req, |mut response: Response| { assert_eq!(response.status(), Status::NotFound); let body = response.body().unwrap().into_string().unwrap(); assert!(body.contains("error")); @@ -37,7 +35,7 @@ fn bad_get_put() { // Try to put a message without a proper body. let req = MockRequest::new(Put, "/message/80").header(ContentType::JSON); - run_test!(req, |response: Response| { + run_test!(&rocket, req, |response: Response| { assert_eq!(response.status(), Status::BadRequest); }); @@ -46,16 +44,17 @@ fn bad_get_put() { .header(ContentType::JSON) .body(r#"{ "contents": "Bye bye, world!" }"#); - run_test!(req, |response: Response| { + run_test!(&rocket, req, |response: Response| { assert_eq!(response.status(), Status::NotFound); }); } #[test] fn post_get_put_get() { + let rocket = rocket(); // Check that a message with ID 1 doesn't exist. let req = MockRequest::new(Get, "/message/1").header(ContentType::JSON); - run_test!(req, |response: Response| { + run_test!(&rocket, req, |response: Response| { assert_eq!(response.status(), Status::NotFound); }); @@ -64,13 +63,13 @@ fn post_get_put_get() { .header(ContentType::JSON) .body(r#"{ "contents": "Hello, world!" }"#); - run_test!(req, |response: Response| { + run_test!(&rocket, req, |response: Response| { assert_eq!(response.status(), Status::Ok); }); // Check that the message exists with the correct contents. let req = MockRequest::new(Get, "/message/1") .header(ContentType::JSON); - run_test!(req, |mut response: Response| { + run_test!(&rocket, req, |mut response: Response| { assert_eq!(response.status(), Status::Ok); let body = response.body().unwrap().into_string().unwrap(); assert!(body.contains("Hello, world!")); @@ -81,13 +80,13 @@ fn post_get_put_get() { .header(ContentType::JSON) .body(r#"{ "contents": "Bye bye, world!" }"#); - run_test!(req, |response: Response| { + run_test!(&rocket, req, |response: Response| { assert_eq!(response.status(), Status::Ok); }); // Check that the message exists with the updated contents. let req = MockRequest::new(Get, "/message/1") .header(ContentType::JSON); - run_test!(req, |mut response: Response| { + run_test!(&rocket, req, |mut response: Response| { assert_eq!(response.status(), Status::Ok); let body = response.body().unwrap().into_string().unwrap(); assert!(!body.contains("Hello, world!")); From 0acfea9c712e03204afa0a08ea43f5b70eb53472 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 16 Feb 2017 18:30:44 -0800 Subject: [PATCH 006/297] Remove unnecessary pub qualifier in forms example. --- examples/forms/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index bacc7bfa..0f2608cd 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -42,7 +42,7 @@ fn user_page(username: &str) -> String { format!("This is {}'s page.", username) } -pub fn rocket() -> rocket::Rocket { +fn rocket() -> rocket::Rocket { rocket::ignite() .mount("/", routes![files::index, files::files, user_page, login]) } From 937fe50ad78a9b260a3484f4bdd56965d79fe3ee Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Thu, 16 Feb 2017 21:56:44 -0500 Subject: [PATCH 007/297] Fix typo in json example. --- examples/json/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 1b1fb424..4f4d1002 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -25,7 +25,7 @@ struct Message { contents: String } -// TODO: This example can be improved by using `route` with muliple HTTP verbs. +// TODO: This example can be improved by using `route` with multiple HTTP verbs. #[post("/", format = "application/json", data = "")] fn new(id: ID, message: JSON, map: State) -> JSON { let mut hashmap = map.lock().expect("map lock."); From 46403b8d0a6fb4de56b16bc1c41d62263304e2bc Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 17 Feb 2017 00:23:41 -0800 Subject: [PATCH 008/297] Iterate through Tera error chain for better errors. --- contrib/src/templates/tera_templates.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/src/templates/tera_templates.rs b/contrib/src/templates/tera_templates.rs index 20a9783f..2e7e440d 100644 --- a/contrib/src/templates/tera_templates.rs +++ b/contrib/src/templates/tera_templates.rs @@ -56,7 +56,11 @@ pub fn render(name: &str, _: &TemplateInfo, context: &T) -> Option match tera.value_render(name, context) { Ok(string) => Some(string), Err(e) => { - error_!("Error rendering Tera template '{}': {}", name, e); + error_!("Error rendering Tera template '{}'.", name); + for error in e.iter().skip(1) { + error_!("{}.", error); + } + None } } From 4161949a1cbd16fa9f3ceb228528c105087529f6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 16 Feb 2017 18:31:20 -0500 Subject: [PATCH 009/297] Add webp, ttf, otf, woff, and woff2 as known Content-Types. --- lib/src/http/content_type.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 13ec673c..be35667f 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -10,7 +10,7 @@ use http::ascii::{uncased_eq, UncasedAscii}; /// /// # Usage /// -/// ContentTypes should rarely be created directly. Instead, an associated +/// `ContentTypes` should rarely be created directly. Instead, an associated /// constant should be used; one is declared for most commonly used content /// types. /// @@ -114,8 +114,13 @@ impl ContentType { "GIF", GIF, is_gif => "image", "gif", "BMP", BMP, is_bmp => "image", "bmp", "JPEG", JPEG, is_jpeg => "image", "jpeg", + "WEBP", WEBP, is_webp => "image", "webp", "SVG", SVG, is_svg => "image", "svg+xml", - "PDF", PDF, is_pdf => "application", "pdf" + "PDF", PDF, is_pdf => "application", "pdf", + "TTF", TTF, is_ttf => "application", "font-sfnt", + "OTF", OTF, is_otf => "application", "font-sfnt", + "WOFF", WOFF, is_woff => "application", "font-woff", + "WOFF2", WOFF2, is_woff2 => "font", "woff2" } /// Returns the Content-Type associated with the extension `ext`. Not all @@ -158,8 +163,13 @@ impl ContentType { x if uncased_eq(x, "bmp") => ContentType::BMP, x if uncased_eq(x, "jpeg") => ContentType::JPEG, x if uncased_eq(x, "jpg") => ContentType::JPEG, + x if uncased_eq(x, "webp") => ContentType::WEBP, x if uncased_eq(x, "svg") => ContentType::SVG, x if uncased_eq(x, "pdf") => ContentType::PDF, + x if uncased_eq(x, "ttf") => ContentType::TTF, + x if uncased_eq(x, "otf") => ContentType::OTF, + x if uncased_eq(x, "woff") => ContentType::WOFF, + x if uncased_eq(x, "woff2") => ContentType::WOFF2, _ => ContentType::Any } } @@ -210,7 +220,7 @@ impl ContentType { ContentType { ttype: UncasedAscii::from(ttype), subtype: UncasedAscii::from(subtype), - params: params.map(|p| UncasedAscii::from(p)) + params: params.map(UncasedAscii::from) } } @@ -245,9 +255,9 @@ impl ContentType { None => "" }; - params.split(";") + params.split(';') .filter_map(|param| { - let mut kv = param.split("="); + let mut kv = param.split('='); match (kv.next(), kv.next()) { (Some(key), Some(val)) => Some((key.trim(), val.trim())), _ => None From 7e7c31b9e7f61400e17bb794ffeaf743524435f1 Mon Sep 17 00:00:00 2001 From: mikejiang Date: Fri, 17 Feb 2017 09:22:23 +0800 Subject: [PATCH 010/297] Add tests for extended_validation example. --- examples/extended_validation/Cargo.toml | 3 + examples/extended_validation/src/main.rs | 9 ++- examples/extended_validation/src/tests.rs | 86 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 examples/extended_validation/src/tests.rs diff --git a/examples/extended_validation/Cargo.toml b/examples/extended_validation/Cargo.toml index 721222f1..d0441ae5 100644 --- a/examples/extended_validation/Cargo.toml +++ b/examples/extended_validation/Cargo.toml @@ -6,3 +6,6 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index 22fa9603..20a8a2f1 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -4,6 +4,8 @@ extern crate rocket; mod files; +#[cfg(test)] +mod tests; use rocket::response::Redirect; use rocket::request::{Form, FromFormValue}; @@ -77,8 +79,11 @@ fn user_page(username: &str) -> String { format!("This is {}'s page.", username) } -fn main() { +fn rocket() -> rocket::Rocket { rocket::ignite() .mount("/", routes![files::index, files::files, user_page, login]) - .launch(); +} + +fn main() { + rocket().launch() } diff --git a/examples/extended_validation/src/tests.rs b/examples/extended_validation/src/tests.rs new file mode 100644 index 00000000..46e21717 --- /dev/null +++ b/examples/extended_validation/src/tests.rs @@ -0,0 +1,86 @@ +use rocket::testing::MockRequest; +use rocket::http::Method::*; +use rocket::http::{ContentType, Status}; + +use super::rocket; + +fn test_login(user: &str, pass: &str, age: &str, status: Status, body: T) + where T: Into> +{ + let rocket = rocket(); + let query = format!("username={}&password={}&age={}", user, pass, age); + + let mut req = MockRequest::new(Post, "/login") + .header(ContentType::Form) + .body(&query); + + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.status(), status); + + let body_str = response.body().and_then(|body| body.into_string()); + if let Some(expected_str) = body.into() { + assert!(body_str.map_or(false, |s| s.contains(expected_str))); + } +} + +#[test] +fn test_good_login() { + test_login("Sergio", "password", "30", Status::SeeOther, None); +} + +#[test] +fn test_invalid_user() { + test_login("-1", "password", "30", Status::Ok, "Unrecognized user"); + test_login("Mike", "password", "30", Status::Ok, "Unrecognized user"); +} + +#[test] +fn test_invalid_password() { + test_login("Sergio", "password1", "30", Status::Ok, "Wrong password!"); + test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: Too short!"); +} + +#[test] +fn test_invalid_age() { + test_login("Sergio", "password", "20", Status::Ok, "Must be at least 21."); + test_login("Sergio", "password", "-100", Status::Ok, "Must be at least 21."); + test_login("Sergio", "password", "hi", Status::Ok, "Age value is not a number"); +} + +fn check_bad_form(form_str: &str, status: Status) { + let rocket = rocket(); + let mut req = MockRequest::new(Post, "/login") + .header(ContentType::Form) + .body(form_str); + + let response = req.dispatch_with(&rocket); + assert_eq!(response.status(), status); +} + +#[test] +fn test_bad_form_abnromal_inputs() { + check_bad_form("&", Status::BadRequest); + check_bad_form("=", Status::BadRequest); + check_bad_form("&&&===&", Status::BadRequest); +} + +#[test] +fn test_bad_form_missing_fields() { + let bad_inputs: [&str; 6] = [ + "username=Sergio", + "password=pass", + "age=30", + "username=Sergio&password=pass", + "username=Sergio&age=30", + "password=pass&age=30" + ]; + + for bad_input in bad_inputs.into_iter() { + check_bad_form(bad_input, Status::UnprocessableEntity); + } +} + +#[test] +fn test_bad_form_additional_fields() { + check_bad_form("username=Sergio&password=pass&age=30&addition=1", Status::UnprocessableEntity); +} From d89c2a0cb5d1d709e1acc0b9b1ffb1d5c7e0ce11 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Feb 2017 00:23:20 -0800 Subject: [PATCH 011/297] Presort routes instead of sorting on each route. --- lib/src/router/mod.rs | 50 +++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index d7f851fd..b5c4e9de 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -23,30 +23,25 @@ impl Router { } pub fn add(&mut self, route: Route) { - // let selector = (route.method, route.path.segment_count()); let selector = route.method; - self.routes.entry(selector).or_insert_with(|| vec![]).push(route); + let entries = self.routes.entry(selector).or_insert_with(|| vec![]); + // TODO: We really just want an insertion at the correct spot here, + // instead of pushing to the end and _then_ sorting. + entries.push(route); + entries.sort_by(|a, b| a.rank.cmp(&b.rank)); } - // 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, req: &Request) -> Vec<&'b Route> { - trace_!("Trying to route: {}", req); - // let num_segments = req.uri.segment_count(); - // self.routes.get(&(req.method, num_segments)).map_or(vec![], |routes| { - self.routes.get(&req.method()).map_or(vec![], |routes| { - let mut matches: Vec<_> = routes.iter() + // Note that routes are presorted by rank on each `add`. + let matches = self.routes.get(&req.method()).map_or(vec![], |routes| { + routes.iter() .filter(|r| r.collides_with(req)) - .collect(); + .collect() + }); - // FIXME: Presort vector to avoid a sort on each route. - matches.sort_by(|a, b| a.rank.cmp(&b.rank)); - trace_!("All matches: {:?}", matches); - matches - }) + trace_!("Routing the request: {}", req); + trace_!("All matches: {:?}", matches); + matches } pub fn has_collisions(&self) -> bool { @@ -158,10 +153,7 @@ mod test { assert!(!default_rank_route_collisions(&["/a/b", "/a/b/"])); } - fn route<'a>(router: &'a Router, - method: Method, - uri: &str) - -> Option<&'a Route> { + fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> { let request = Request::new(method, URI::new(uri)); let matches = router.route(&request); if matches.len() > 0 { @@ -286,8 +278,8 @@ mod test { let expected = &[$($want),+]; assert!(routed_to.len() == expected.len()); for (got, expected) in routed_to.iter().zip(expected.iter()) { - assert_eq!(got.path.as_str() as &str, expected.1); assert_eq!(got.rank, expected.0); + assert_eq!(got.path.as_str() as &str, expected.1); } }) } @@ -306,6 +298,18 @@ mod test { expect: (2, "b/"), (3, "b/b") ); + assert_ranked_routing!( + to: "b/b", + with: [(2, "b/"), (1, "a/"), (3, "b/b")], + expect: (2, "b/"), (3, "b/b") + ); + + assert_ranked_routing!( + to: "b/b", + with: [(3, "b/b"), (2, "b/"), (1, "a/")], + expect: (2, "b/"), (3, "b/b") + ); + assert_ranked_routing!( to: "b/b", with: [(1, "a/"), (2, "b/"), (0, "b/b")], From 62a75cdde63bfb4275770c0380d82d1ac84c3c5a Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 22 Feb 2017 11:25:30 -0800 Subject: [PATCH 012/297] Use `append_raw` to ensure all headers are set. Fixes #206. --- lib/src/rocket.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 6c4bf3b3..c225f681 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -93,9 +93,10 @@ impl Rocket { *hyp_res.status_mut() = hyper::StatusCode::from_u16(response.status().code); for header in response.headers() { + // FIXME: Using hyper here requires two allocations. let name = header.name.into_string(); - let value = vec![header.value.into_owned().into()]; - hyp_res.headers_mut().set_raw(name, value); + let value = Vec::from(header.value.as_bytes()); + hyp_res.headers_mut().append_raw(name, value); } if response.body().is_none() { From efbfbd1045876234f4b9dd14e10c08076501d471 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 24 Feb 2017 13:19:50 -0800 Subject: [PATCH 013/297] Add 'into_bytes' and 'into_inner' methods to Body. --- lib/src/response/response.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 9d2a7f79..2302ebaf 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -36,6 +36,13 @@ impl Body { } } + /// Consumes `self` and returns the inner body. + pub fn into_inner(self) -> T { + match self { + Body::Sized(b, _) | Body::Chunked(b, _) => b + } + } + /// Returns `true` if `self` is a `Body::Sized`. pub fn is_sized(&self) -> bool { match *self { @@ -54,20 +61,33 @@ impl Body { } impl Body { - /// Attepts to read `self` into a `String` and returns it. If reading or - /// conversion fails, returns `None`. - pub fn into_string(self) -> Option { - let (mut body, mut string) = match self { - Body::Sized(b, size) => (b, String::with_capacity(size as usize)), - Body::Chunked(b, _) => (b, String::new()) + /// Attepts to read `self` into a `Vec` and returns it. If reading fails, + /// returns `None`. + pub fn into_bytes(self) -> Option> { + let (mut body, mut vec) = match self { + Body::Sized(b, size) => (b, Vec::with_capacity(size as usize)), + Body::Chunked(b, _) => (b, Vec::new()) }; - if let Err(e) = body.read_to_string(&mut string) { + if let Err(e) = body.read_to_end(&mut vec) { error_!("Error reading body: {:?}", e); return None; } - Some(string) + Some(vec) + } + + /// Attepts to read `self` into a `String` and returns it. If reading or + /// conversion fails, returns `None`. + pub fn into_string(self) -> Option { + self.into_bytes() + .and_then(|bytes| match String::from_utf8(bytes) { + Ok(string) => Some(string), + Err(e) => { + error_!("Body is invalid UTF-8: {}", e); + None + } + }) } } From d99de8e05b33a283276b2fe94508fe2dbf6d9ec0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 24 Feb 2017 13:57:33 -0800 Subject: [PATCH 014/297] New version: 0.2.1. --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca084a2..e2b1beb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# Version 0.2.1 (Feb 24, 2017) + +## Core Fixes + + * `Flash` cookie deletion functions as expected regardless of the path. + * `config` properly accepts IPv6 addresses. + * Multiple `Set-Cookie` headers are properly set. + +## Core Improvements + + * `Display` and `Error` were implemented for `ConfigError`. + * `webp`, `ttf`, `otf`, `woff`, and `woff2` were added as known content types. + * Routes are presorted for faster routing. + * `into_bytes` and `into_inner` methods were added to `Body`. + +## Codegen + + * Fixed `unmanaged_state` lint so that it works with prefilled type aliases. + +## Contrib + + * Better errors are emitted on Tera template parse errors. + +## Documentation + + * Fixed typos in `manage` and `JSON` docs. + +## Infrastructure + + * Updated doctests for latest Cargo nightly. + # Version 0.2.0 (Feb 06, 2017) Detailed release notes for v0.2 can also be found on diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 84f14ee8..8e41dc1b 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.0" +version = "0.2.1" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.0", path = "../lib/" } +rocket = { version = "0.2.1", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 037bb5ac..6c5fde6f 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.0" +version = "0.2.1" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -21,7 +21,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.0", path = "../lib/" } +rocket = { version = "0.2.1", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 4e222d18..9cebb27a 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.0" +version = "0.2.1" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -32,7 +32,7 @@ features = ["percent-encode"] [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.0", path = "../codegen" } +rocket_codegen = { version = "0.2.1", path = "../codegen" } [build-dependencies] ansi_term = "^0.9" From 8bf51d15e3176633662235615f29df4a066509d4 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 24 Feb 2017 15:04:01 -0800 Subject: [PATCH 015/297] Add a GitHub issue template. --- .github/ISSUE_TEMPLATE.md | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..ed48bc55 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,53 @@ +Hello, and thanks for opening a new issue about Rocket! + +Before opening your issue, we ask that you search through existing issues and +pull requests to see if your bug report, concern, request, or comment has +already been addressed. Ensure to search through both open and closed issues and +pull requests. If this is a question, feature request, or general comment, +please ensure that you read the relevant sections of the documentation before +posting your issue. Finally, consider asking questions on IRC or Matrix before +opening an issue. + +If you feel confident that your issue is unique, please including the following +information, selecting the category that best describes your issue: + +## Bug Reports + +Bug reports _must_ include: + + 1. The version of Rocket you're using. Ensure it's the latest, if possible. + + 2. A brief description of the bug that includes: + * The nature of the bug. + * When the bug occurs. + * What you expected vs. what actually happened. + + 3. How you discovered the bug. Short test cases are especially useful. + + 4. Ideas, if any, about what Rocket is doing incorrectly. + +## Questions + +Any questions _must_ include: + + 1. The version of Rocket this question is based on, if any. + + 2. What steps you've taken to answer the question yourself. + + 3. What documentation you believe should include an answer to this question. + +## Feature Requests + +Feature requests _must_ include: + + 1. Why you believe this feature is necessary. + + 2. A convincing use-case for this feature. + + 3. Why this feature can't or shouldn't exist outside of Rocket. + +## General Comments + +Feel free to comment at will. We simply ask that your comments are well +constructed and actionable. Consider whether IRC or Matrix would be a better +venue for discussion. From f0836e22fa87610eeafa934311cd879ccda2fd6d Mon Sep 17 00:00:00 2001 From: mikejiang Date: Sat, 18 Feb 2017 15:01:12 +0800 Subject: [PATCH 016/297] Add tests for static_files example. --- examples/static_files/Cargo.toml | 3 ++ examples/static_files/src/main.rs | 11 +++++-- examples/static_files/src/tests.rs | 53 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 examples/static_files/src/tests.rs diff --git a/examples/static_files/Cargo.toml b/examples/static_files/Cargo.toml index 84d5c276..e90b7c34 100644 --- a/examples/static_files/Cargo.toml +++ b/examples/static_files/Cargo.toml @@ -6,3 +6,6 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/static_files/src/main.rs b/examples/static_files/src/main.rs index 0655bc61..a47f80ff 100644 --- a/examples/static_files/src/main.rs +++ b/examples/static_files/src/main.rs @@ -3,6 +3,9 @@ extern crate rocket; +#[cfg(test)] +mod tests; + use std::io; use std::path::{Path, PathBuf}; @@ -18,6 +21,10 @@ fn files(file: PathBuf) -> Option { NamedFile::open(Path::new("static/").join(file)).ok() } -fn main() { - rocket::ignite().mount("/", routes![index, files]).launch(); +fn rocket() -> rocket::Rocket { + rocket::ignite().mount("/", routes![index, files]) +} + +fn main() { + rocket().launch(); } diff --git a/examples/static_files/src/tests.rs b/examples/static_files/src/tests.rs new file mode 100644 index 00000000..85626ee4 --- /dev/null +++ b/examples/static_files/src/tests.rs @@ -0,0 +1,53 @@ +use std::fs::File; +use std::io::Read; + +use rocket::testing::MockRequest; +use rocket::http::Method::*; +use rocket::http::Status; + +use super::rocket; + +fn test_query_file (path: &str, file: T, status: Status) + where T: Into> +{ + let rocket = rocket(); + let mut req = MockRequest::new(Get, &path); + + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.status(), status); + + let body_data = response.body().and_then(|body| body.into_bytes()); + if let Some(filename) = file.into() { + let expected_data = read_file_content(filename); + assert!(body_data.map_or(false, |s| s == expected_data)); + } +} + +fn read_file_content(path: &str) -> Vec { + let mut fp = File::open(&path).expect(&format!("Can not open {}", path)); + let mut file_content = vec![]; + + fp.read_to_end(&mut file_content).expect(&format!("Reading {} failed.", path)); + file_content +} + +#[test] +fn test_index_html() { + test_query_file("/", "static/index.html", Status::Ok); +} + +#[test] +fn test_hidden_file() { + test_query_file("/hidden/hi.txt", "static/hidden/hi.txt", Status::Ok); +} + +#[test] +fn test_icon_file() { + test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok); +} + +#[test] +fn test_invalid_path() { + test_query_file("/thou_shalt_not_exist", None, Status::NotFound); + test_query_file("/thou/shalt/not/exist", None, Status::NotFound); +} From 65c70479f8240451ae1404d68c879584819d34b2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 25 Feb 2017 14:22:49 -0800 Subject: [PATCH 017/297] Clarify which version of Hyper is benchmarked. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c92a25b8..d4998981 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,8 @@ Rocket is designed to be performant. At this time, its performance is [bottlenecked by the Hyper HTTP library](https://github.com/SergioBenitez/Rocket/issues/17). Even so, Rocket currently performs _significantly better_ than the latest version of -asynchronous Hyper on a simple "Hello, world!" benchmark. Rocket also performs -_significantly better_ than the Iron web framework: +multithreaded asynchronous Hyper on a simple "Hello, world!" benchmark. Rocket +also performs _significantly better_ than the Iron web framework: **Machine Specs:** @@ -169,7 +169,7 @@ _significantly better_ than the Iron web framework: Requests/sec: 75051.28 Transfer/sec: 10.45MB -**Hyper v0.10.0-a.0 (1/12/2016)** (46 LOC) results (best of 3, +/- 5000 req/s, +/- 30us latency): +**Hyper v0.10-rotor (1/12/2016)** (46 LOC) results (best of 3, +/- 5000 req/s, +/- 30us latency): Running 10s test @ http://localhost:80 1 threads and 18 connections From 56a631d4ba2bc8b778350945b30a460ddb3f5060 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 26 Feb 2017 18:26:02 -0800 Subject: [PATCH 018/297] Update codegen for latest nightly. --- codegen/build.rs | 4 ++-- codegen/src/lints/utils.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codegen/build.rs b/codegen/build.rs index f2b9f3ba..b3a21eaf 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -8,8 +8,8 @@ use ansi_term::Colour::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. -const MIN_DATE: &'static str = "2017-01-31"; -const MIN_VERSION: &'static str = "1.16.0-nightly"; +const MIN_DATE: &'static str = "2017-02-26"; +const MIN_VERSION: &'static str = "1.17.0-nightly"; // Convenience macro for writing to stderr. macro_rules! printerr { diff --git a/codegen/src/lints/utils.rs b/codegen/src/lints/utils.rs index 73a18384..6af05a8a 100644 --- a/codegen/src/lints/utils.rs +++ b/codegen/src/lints/utils.rs @@ -160,7 +160,7 @@ impl DefExt for Def { | Def::AssociatedTy(id) | Def::TyParam(id) | Def::Struct(id) | Def::StructCtor(id, ..) | Def::Union(id) | Def::Trait(id) | Def::Method(id) | Def::Const(id) | Def::AssociatedConst(id) - | Def::Local(id) | Def::Upvar(id, ..) | Def::Macro(id) => Some(id), + | Def::Local(id) | Def::Upvar(id, ..) | Def::Macro(id, ..) => Some(id), Def::Label(..) | Def::PrimTy(..) | Def::SelfTy(..) | Def::Err => None, } } From 6be902162d5f53caac0668b385baa8bcce51310e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 26 Feb 2017 18:31:15 -0800 Subject: [PATCH 019/297] New version: 0.2.2. --- CHANGELOG.md | 7 +++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b1beb1..c5373f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version 0.2.2 (Feb 26, 2017) + +## Codegen + + * Lints were updated for `2017-02-25` and `2017-02-26` nightlies. + * Minimum required `rustc` is `1.17.0-nightly (2017-02-26)`. + # Version 0.2.1 (Feb 24, 2017) ## Core Fixes diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 8e41dc1b..2135faad 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.1" +version = "0.2.2" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.1", path = "../lib/" } +rocket = { version = "0.2.2", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 6c5fde6f..48b2998b 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.1" +version = "0.2.2" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -21,7 +21,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.1", path = "../lib/" } +rocket = { version = "0.2.2", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 9cebb27a..6a3fd4ce 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.1" +version = "0.2.2" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -32,7 +32,7 @@ features = ["percent-encode"] [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.1", path = "../codegen" } +rocket_codegen = { version = "0.2.2", path = "../codegen" } [build-dependencies] ansi_term = "^0.9" From 722ee93f8ba8924c9700c23a24a74569c8b1ab2f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 7 Mar 2017 01:19:06 -0800 Subject: [PATCH 020/297] Update to cookie 0.7. Use 256-bit session_keys. This commit involves several breaking changes: * `session_key` config param must be a 256-bit base64 encoded string. * `FromRequest` is implemented for `Cookies`, not `Cookie`. * Only a single `Cookies` instance can be retrieved at a time. * `Config::take_session_key` returns a `Vec`. * `Into
` is implemented for `&Cookie`, not `Cookie`. --- codegen/tests/run-pass/complete-decorator.rs | 2 +- examples/config/Rocket.toml | 4 +- examples/cookies/src/main.rs | 6 +- lib/Cargo.toml | 9 ++- lib/src/config/builder.rs | 4 +- lib/src/config/config.rs | 38 ++++++------ lib/src/config/mod.rs | 20 +++--- lib/src/http/cookies.rs | 65 +++++++++++++++++--- lib/src/http/header.rs | 4 +- lib/src/lib.rs | 1 + lib/src/request/from_request.rs | 2 +- lib/src/request/request.rs | 31 +++++++--- lib/src/response/flash.rs | 18 ++---- 13 files changed, 129 insertions(+), 75 deletions(-) diff --git a/codegen/tests/run-pass/complete-decorator.rs b/codegen/tests/run-pass/complete-decorator.rs index f0f09f58..1819ec5e 100644 --- a/codegen/tests/run-pass/complete-decorator.rs +++ b/codegen/tests/run-pass/complete-decorator.rs @@ -16,7 +16,7 @@ struct User<'a> { fn get<'r>(name: &str, query: User<'r>, user: Form<'r, User<'r>>, - cookies: &Cookies) + cookies: Cookies) -> &'static str { "hi" } diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index 3640d3e4..2ae9bd0f 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -15,7 +15,7 @@ port = 80 log = "normal" workers = 8 # don't use this key! generate your own and keep it private! -session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" +session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" [production] address = "0.0.0.0" @@ -23,4 +23,4 @@ port = 80 workers = 12 log = "critical" # don't use this key! generate your own and keep it private! -session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" +session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index b45fe625..243c1954 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -20,14 +20,14 @@ struct Message { } #[post("/submit", data = "")] -fn submit(cookies: &Cookies, message: Form) -> Redirect { +fn submit(mut cookies: Cookies, message: Form) -> Redirect { cookies.add(Cookie::new("message", message.into_inner().message)); Redirect::to("/") } #[get("/")] -fn index(cookies: &Cookies) -> Template { - let cookie = cookies.find("message"); +fn index(cookies: Cookies) -> Template { + let cookie = cookies.get("message"); let mut context = HashMap::new(); if let Some(ref cookie) = cookie { context.insert("message", cookie.value()); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6a3fd4ce..e95db71d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -25,10 +25,13 @@ state = "^0.2" time = "^0.1" memchr = "1" +# FIXME: session support should be optional +base64 = "0.4" + +# FIXME: session support should be optional [dependencies.cookie] -version = "^0.6" -default-features = false -features = ["percent-encode"] +version = "^0.7" +features = ["percent-encode", "secure"] [dev-dependencies] lazy_static = "0.2" diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index 97f63a33..36746c4f 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -152,12 +152,12 @@ impl ConfigBuilder { /// use rocket::LoggingLevel; /// use rocket::config::{Config, Environment}; /// - /// let key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5"; + /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; /// let mut config = Config::build(Environment::Staging) /// .session_key(key) /// .unwrap(); /// - /// assert_eq!(config.take_session_key(), Some(key.to_string())); + /// assert!(config.take_session_key().is_some()); /// ``` pub fn session_key>(mut self, key: K) -> Self { self.session_key = Some(key.into()); diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 65076166..1f48a8ac 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -9,7 +9,7 @@ use std::env; use config::Environment::*; use config::{self, Value, ConfigBuilder, Environment, ConfigError}; -use num_cpus; +use {num_cpus, base64}; use logger::LoggingLevel; /// Structure for Rocket application configuration. @@ -44,7 +44,7 @@ pub struct Config { /// The path to the configuration file this config belongs to. pub config_path: PathBuf, /// The session key. - session_key: RwLock>, + session_key: RwLock>>, } macro_rules! parse { @@ -175,12 +175,6 @@ impl Config { ConfigError::BadType(id, expect, actual, self.config_path.clone()) } - // Aliases to `set` before the method is removed. - pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> config::Result<()> { - #[allow(deprecated)] - self.set(name, val) - } - /// Sets the configuration `val` for the `name` entry. If the `name` is one /// of "address", "port", "session_key", "log", or "workers" (the "default" /// values), the appropriate value in the `self` Config structure is set. @@ -195,8 +189,7 @@ impl Config { /// * **workers**: Integer (16-bit unsigned) /// * **log**: String /// * **session_key**: String (192-bit base64) - #[deprecated(since="0.2", note="use the set_{param} methods instead")] - pub fn set(&mut self, name: &str, val: &Value) -> config::Result<()> { + pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> config::Result<()> { if name == "address" { let address_str = parse!(self, name, val, as_str, "a string")?; self.set_address(address_str)?; @@ -335,20 +328,27 @@ impl Config { /// # use rocket::config::ConfigError; /// # fn config_test() -> Result<(), ConfigError> { /// let mut config = Config::new(Environment::Staging)?; - /// assert!(config.set_session_key("VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5").is_ok()); + /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; + /// assert!(config.set_session_key(key).is_ok()); /// assert!(config.set_session_key("hello? anyone there?").is_err()); /// # Ok(()) /// # } /// ``` pub fn set_session_key>(&mut self, key: K) -> config::Result<()> { let key = key.into(); - if key.len() != 32 { - return Err(self.bad_type("session_key", - "string", - "a 192-bit base64 string")); + let error = self.bad_type("session_key", "string", + "a 256-bit base64 encoded string"); + + if key.len() != 44 { + return Err(error); } - self.session_key = RwLock::new(Some(key)); + let bytes = match base64::decode(&key) { + Ok(bytes) => bytes, + Err(_) => return Err(error) + }; + + self.session_key = RwLock::new(Some(bytes)); Ok(()) } @@ -435,21 +435,21 @@ impl Config { /// use rocket::config::{Config, Environment}; /// /// // Create a new config with a session key. - /// let key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz"; + /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; /// let config = Config::build(Environment::Staging) /// .session_key(key) /// .unwrap(); /// /// // Get the key for the first time. /// let session_key = config.take_session_key(); - /// assert_eq!(session_key, Some(key.to_string())); + /// assert!(session_key.is_some()); /// /// // Try to get the key again. /// let session_key_again = config.take_session_key(); /// assert_eq!(session_key_again, None); /// ``` #[inline] - pub fn take_session_key(&self) -> Option { + pub fn take_session_key(&self) -> Option> { let mut key = self.session_key.write().expect("couldn't lock session key"); key.take() } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 113d92c5..1fc3c04a 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -40,9 +40,9 @@ //! * examples: `12`, `1`, `4` //! * **log**: _[string]_ how much information to log; one of `"normal"`, //! `"debug"`, or `"critical"` -//! * **session_key**: _[string]_ a 192-bit base64 encoded string (32 +//! * **session_key**: _[string]_ a 256-bit base64 encoded string (44 //! characters) to use as the session key -//! * example: `"VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5"` +//! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="` //! //! ### Rocket.toml //! @@ -70,7 +70,7 @@ //! workers = max(number_of_cpus, 2) //! log = "normal" //! # don't use this key! generate your own and keep it private! -//! session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" +//! session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" //! //! [production] //! address = "0.0.0.0" @@ -78,7 +78,7 @@ //! workers = max(number_of_cpus, 2) //! log = "critical" //! # don't use this key! generate your own and keep it private! -//! session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" +//! session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" //! ``` //! //! The `workers` parameter is computed by Rocket automatically; the value above @@ -587,7 +587,7 @@ mod test { port = 7810 workers = 21 log = "critical" - session_key = "01234567890123456789012345678901" + session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" template_dir = "mine" json = true pi = 3.14 @@ -598,7 +598,7 @@ mod test { .port(7810) .workers(21) .log_level(LoggingLevel::Critical) - .session_key("01234567890123456789012345678901") + .session_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=") .extra("template_dir", "mine") .extra("json", true) .extra("pi", 3.14); @@ -873,19 +873,19 @@ mod test { check_config!(RocketConfig::parse(r#" [stage] - session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" + session_key = "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" "#.to_string(), TEST_CONFIG_FILENAME), { default_config(Staging).session_key( - "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" + "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" ) }); check_config!(RocketConfig::parse(r#" [stage] - session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" + session_key = "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" "#.to_string(), TEST_CONFIG_FILENAME), { default_config(Staging).session_key( - "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" + "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" ) }); } diff --git a/lib/src/http/cookies.rs b/lib/src/http/cookies.rs index 02c08162..1371f156 100644 --- a/lib/src/http/cookies.rs +++ b/lib/src/http/cookies.rs @@ -1,17 +1,62 @@ use http::Header; -pub use cookie::Cookie; -pub use cookie::CookieJar; -pub use cookie::CookieBuilder; +use std::cell::RefMut; -/// Type alias to a `'static` CookieJar. -/// -/// A `CookieJar` should never be used without a `'static` lifetime. As a -/// result, you should always use this alias. -pub type Cookies = self::CookieJar<'static>; +pub use cookie::{Cookie, CookieJar, Iter, CookieBuilder, Delta}; -impl<'c> From> for Header<'static> { - fn from(cookie: Cookie) -> Header<'static> { +#[derive(Debug)] +pub enum Cookies<'a> { + Jarred(RefMut<'a, CookieJar>), + Empty(CookieJar) +} + +impl<'a> From> for Cookies<'a> { + fn from(jar: RefMut<'a, CookieJar>) -> Cookies<'a> { + Cookies::Jarred(jar) + } +} + +impl<'a> Cookies<'a> { + pub(crate) fn empty() -> Cookies<'static> { + Cookies::Empty(CookieJar::new()) + } + + pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { + match *self { + Cookies::Jarred(ref jar) => jar.get(name), + Cookies::Empty(_) => None + } + } + + pub fn add(&mut self, cookie: Cookie<'static>) { + if let Cookies::Jarred(ref mut jar) = *self { + jar.add(cookie) + } + } + + pub fn remove(&mut self, cookie: Cookie<'static>) { + if let Cookies::Jarred(ref mut jar) = *self { + jar.remove(cookie) + } + } + + pub fn iter(&self) -> Iter { + match *self { + Cookies::Jarred(ref jar) => jar.iter(), + Cookies::Empty(ref jar) => jar.iter() + } + } + + pub fn delta(&self) -> Delta { + match *self { + Cookies::Jarred(ref jar) => jar.delta(), + Cookies::Empty(ref jar) => jar.delta() + } + } +} + +impl<'a, 'c> From<&'a Cookie<'c>> for Header<'static> { + fn from(cookie: &Cookie) -> Header<'static> { Header::new("Set-Cookie", cookie.encoded().to_string()) } } diff --git a/lib/src/http/header.rs b/lib/src/http/header.rs index 92ae0590..2d203e13 100644 --- a/lib/src/http/header.rs +++ b/lib/src/http/header.rs @@ -340,10 +340,10 @@ impl<'h> HeaderMap<'h> { /// /// let mut map = HeaderMap::new(); /// - /// map.add(Cookie::new("a", "b")); + /// map.add(&Cookie::new("a", "b")); /// assert_eq!(map.get("Set-Cookie").count(), 1); /// - /// map.add(Cookie::new("c", "d")); + /// map.add(&Cookie::new("c", "d")); /// assert_eq!(map.get("Set-Cookie").count(), 2); /// ``` #[inline(always)] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 06df88b8..6066853b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -104,6 +104,7 @@ extern crate state; extern crate cookie; extern crate time; extern crate memchr; +extern crate base64; #[cfg(test)] #[macro_use] extern crate lazy_static; diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index f6ea9052..2d9bc309 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -204,7 +204,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Method { } } -impl<'a, 'r> FromRequest<'a, 'r> for &'a Cookies { +impl<'a, 'r> FromRequest<'a, 'r> for Cookies<'a> { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome { diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index ea5d38f0..a4e90608 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -14,6 +14,8 @@ use router::Route; use http::uri::{URI, Segments}; use http::{Method, ContentType, Header, HeaderMap, Cookie, Cookies}; +use http::CookieJar; + use http::hyper; /// The type of an incoming web request. @@ -29,7 +31,7 @@ pub struct Request<'r> { headers: HeaderMap<'r>, remote: Option, params: RefCell>, - cookies: Cookies, + cookies: RefCell, state: Option<&'r Container>, } @@ -54,7 +56,7 @@ impl<'r> Request<'r> { headers: HeaderMap::new(), remote: None, params: RefCell::new(Vec::new()), - cookies: Cookies::new(&[]), + cookies: RefCell::new(CookieJar::new()), state: None } } @@ -251,15 +253,24 @@ impl<'r> Request<'r> { /// request.cookies().add(Cookie::new("key", "val")); /// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4))); /// ``` - #[inline(always)] - pub fn cookies(&self) -> &Cookies { - &self.cookies + #[inline] + pub fn cookies(&self) -> Cookies { + match self.cookies.try_borrow_mut() { + Ok(jar) => Cookies::from(jar), + Err(_) => { + error_!("Multiple `Cookies` instances are active at once."); + info_!("An instance of `Cookies` must be dropped before another \ + can be retrieved."); + warn_!("The retrieved `Cookies` instance will be empty."); + Cookies::empty() + } + } } /// Replace all of the cookies in `self` with `cookies`. #[inline] - pub(crate) fn set_cookies(&mut self, cookies: Cookies) { - self.cookies = cookies; + pub(crate) fn set_cookies(&mut self, jar: CookieJar) { + self.cookies = RefCell::new(jar); } /// Returns `Some` of the Content-Type header of `self`. If the header is @@ -419,7 +430,7 @@ impl<'r> Request<'r> { // Set the request cookies, if they exist. TODO: Use session key. if let Some(cookie_headers) = h_headers.get_raw("Cookie") { - let mut cookies = Cookies::new(&[]); + let mut jar = CookieJar::new(); for header in cookie_headers { let raw_str = match ::std::str::from_utf8(header) { Ok(string) => string, @@ -432,11 +443,11 @@ impl<'r> Request<'r> { Err(_) => continue }; - cookies.add_original(cookie); + jar.add_original(cookie); } } - request.set_cookies(cookies); + request.set_cookies(jar); } // Set the rest of the headers. diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index a270f3e1..c0c5478f 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -1,6 +1,6 @@ use std::convert::AsRef; -use time::{self, Duration}; +use time::Duration; use outcome::IntoOutcome; use response::{Response, Responder}; @@ -184,7 +184,7 @@ impl<'r, R: Responder<'r>> Responder<'r> for Flash { trace_!("Flash: setting message: {}:{}", self.name, self.message); let cookie = self.cookie(); Response::build_from(self.responder.respond()?) - .header_adjoin(cookie) + .header_adjoin(&cookie) .ok() } } @@ -220,18 +220,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> { fn from_request(request: &'a Request<'r>) -> request::Outcome { trace_!("Flash: attemping to retrieve message."); - let r = request.cookies().find(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| { + let r = request.cookies().get(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| { trace_!("Flash: retrieving message: {:?}", cookie); - // Create the "deletion" cookie. We'll use it to clear the cookie. - let delete_cookie = Cookie::build(FLASH_COOKIE_NAME, "") - .max_age(Duration::seconds(0)) - .expires(time::now() - Duration::days(365)) - .path("/") - .finish(); - - // Add the deletion to the cookie jar, replacing the existing cookie. - request.cookies().add(delete_cookie); + // Delete the flash cookie from the jar. + let orig_cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish(); + request.cookies().remove(orig_cookie); // Parse the flash message. let content = cookie.value(); From 05a458942df41676fae065ccd7b16f594c32ab94 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 7 Mar 2017 02:12:49 -0800 Subject: [PATCH 021/297] Minor grammar fixes to ISSUE_TEMPLATE. --- .github/ISSUE_TEMPLATE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ed48bc55..4dbcb9f3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,11 +4,11 @@ Before opening your issue, we ask that you search through existing issues and pull requests to see if your bug report, concern, request, or comment has already been addressed. Ensure to search through both open and closed issues and pull requests. If this is a question, feature request, or general comment, -please ensure that you read the relevant sections of the documentation before -posting your issue. Finally, consider asking questions on IRC or Matrix before -opening an issue. +please ensure that you have read the relevant sections of the documentation +before posting your issue. Finally, consider asking questions on IRC or Matrix +before opening an issue. -If you feel confident that your issue is unique, please including the following +If you feel confident that your issue is unique, please include the following information, selecting the category that best describes your issue: ## Bug Reports From 16cb7297abeef3af03666247e2fbbf6d9aae683e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 03:28:12 -0800 Subject: [PATCH 022/297] Initial session support. This commit includes the following additions: * A `session` example was added. * `Config::take_session_key` was removed. * If a `session_key` is not supplied, one is automatically generated. * The `Session` type implements signed, encrypted sessions. * A `Session` can be retrieved via its request guard. --- Cargo.toml | 1 + examples/config/tests/common/mod.rs | 3 - examples/session/Cargo.toml | 16 +++++ examples/session/Rocket.toml | 7 ++ examples/session/src/main.rs | 87 +++++++++++++++++++++++ examples/session/templates/index.html.hbs | 15 ++++ examples/session/templates/login.html.hbs | 24 +++++++ lib/Cargo.toml | 27 ++++--- lib/src/config/builder.rs | 2 - lib/src/config/config.rs | 83 +++++++++++---------- lib/src/config/mod.rs | 16 +++-- lib/src/http/cookies.rs | 34 ++++++--- lib/src/http/mod.rs | 4 +- lib/src/http/session.rs | 62 ++++++++++++++++ lib/src/request/from_request.rs | 14 +++- lib/src/request/request.rs | 60 +++++++++++----- lib/src/response/flash.rs | 10 +-- lib/src/rocket.rs | 20 +++--- 18 files changed, 370 insertions(+), 115 deletions(-) create mode 100644 examples/session/Cargo.toml create mode 100644 examples/session/Rocket.toml create mode 100644 examples/session/src/main.rs create mode 100644 examples/session/templates/index.html.hbs create mode 100644 examples/session/templates/login.html.hbs create mode 100644 lib/src/http/session.rs diff --git a/Cargo.toml b/Cargo.toml index 50fef2e7..3934e3aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,5 +30,6 @@ members = [ "examples/pastebin", "examples/state", "examples/uuid", + "examples/session", # "examples/raw_sqlite", ] diff --git a/examples/config/tests/common/mod.rs b/examples/config/tests/common/mod.rs index 7517593d..43832aab 100644 --- a/examples/config/tests/common/mod.rs +++ b/examples/config/tests/common/mod.rs @@ -42,9 +42,6 @@ pub fn test_config(environment: Environment) { assert_eq!(config.extras().count(), 0); } } - - // Rocket `take`s the key, so this should always be `None`. - assert_eq!(config.take_session_key(), None); } pub fn test_hello() { diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml new file mode 100644 index 00000000..d1f37b9d --- /dev/null +++ b/examples/session/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "session" +version = "0.0.1" +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } + +[dependencies.rocket_contrib] +path = "../../contrib" +default-features = false +features = ["handlebars_templates"] + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/session/Rocket.toml b/examples/session/Rocket.toml new file mode 100644 index 00000000..d9c2fbaa --- /dev/null +++ b/examples/session/Rocket.toml @@ -0,0 +1,7 @@ +[staging] +session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" +address = "localhost" +port = 8000 + +[production] +session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs new file mode 100644 index 00000000..a94f98e2 --- /dev/null +++ b/examples/session/src/main.rs @@ -0,0 +1,87 @@ +#![feature(plugin, custom_derive, custom_attribute)] +#![plugin(rocket_codegen)] + +extern crate rocket_contrib; +extern crate rocket; + +use std::collections::HashMap; + +use rocket::Outcome; +use rocket::request::{self, Form, FlashMessage, FromRequest, Request}; +use rocket::response::{Redirect, Flash}; +use rocket::http::{Cookie, Session}; +use rocket_contrib::Template; + +#[derive(FromForm)] +struct Login { + username: String, + password: String +} + +#[derive(Debug)] +struct User(usize); + +impl<'a, 'r> FromRequest<'a, 'r> for User { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> request::Outcome { + let user = request.session() + .get("user_id") + .and_then(|cookie| cookie.value().parse().ok()) + .map(|id| User(id)); + + match user { + Some(user) => Outcome::Success(user), + None => Outcome::Forward(()) + } + } +} + +#[post("/login", data = "")] +fn login(mut session: Session, login: Form) -> Flash { + if login.get().username == "Sergio" && login.get().password == "password" { + session.add(Cookie::new("user_id", 1.to_string())); + Flash::success(Redirect::to("/"), "Successfully logged in.") + } else { + Flash::error(Redirect::to("/login"), "Invalid username/password.") + } +} + +#[post("/logout")] +fn logout(mut session: Session) -> Flash { + session.remove(Cookie::named("user_id")); + Flash::success(Redirect::to("/login"), "Successfully logged out.") +} + +#[get("/login")] +fn login_user(_user: User) -> Redirect { + Redirect::to("/") +} + +#[get("/login", rank = 2)] +fn login_page(flash: Option) -> Template { + let mut context = HashMap::new(); + if let Some(ref msg) = flash { + context.insert("flash", msg.msg()); + } + + Template::render("login", &context) +} + +#[get("/")] +fn user_index(user: User) -> Template { + let mut context = HashMap::new(); + context.insert("user_id", user.0); + Template::render("index", &context) +} + +#[get("/", rank = 2)] +fn index() -> Redirect { + Redirect::to("/login") +} + +fn main() { + rocket::ignite() + .mount("/", routes![index, user_index, login, logout, login_user, login_page]) + .launch() +} diff --git a/examples/session/templates/index.html.hbs b/examples/session/templates/index.html.hbs new file mode 100644 index 00000000..f1c84d0b --- /dev/null +++ b/examples/session/templates/index.html.hbs @@ -0,0 +1,15 @@ + + + + + + Rocket: Session Example + + +

Rocket Session Example

+

Logged in with user ID {{ user_id }}.

+
+ +
+ + diff --git a/examples/session/templates/login.html.hbs b/examples/session/templates/login.html.hbs new file mode 100644 index 00000000..504a2bcc --- /dev/null +++ b/examples/session/templates/login.html.hbs @@ -0,0 +1,24 @@ + + + + + + Rocket: Sessions + + +

Rocket Session: Please Login

+ {{#if flash}} +

{{ flash }}

+ {{else}} +

Please login to continue.

+ {{/if}} + +
+ + + + +

+
+ + diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e95db71d..e2a85b2f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,21 +14,21 @@ license = "MIT/Apache-2.0" build = "build.rs" categories = ["web-programming::http-server"] -[dependencies] -term-painter = "^0.2" -log = "^0.3" -url = "^1" -hyper = { version = "0.10.4", default-features = false } -toml = { version = "^0.2", default-features = false } -num_cpus = "1" -state = "^0.2" -time = "^0.1" -memchr = "1" +[features] +testing = [] -# FIXME: session support should be optional +[dependencies] +term-painter = "0.2" +log = "0.3" +url = "1" +hyper = { version = "0.10.4", default-features = false } +toml = { version = "0.2", default-features = false } +num_cpus = "1" +state = "0.2" +time = "0.1" +memchr = "1" base64 = "0.4" -# FIXME: session support should be optional [dependencies.cookie] version = "^0.7" features = ["percent-encode", "secure"] @@ -40,6 +40,3 @@ rocket_codegen = { version = "0.2.2", path = "../codegen" } [build-dependencies] ansi_term = "^0.9" version_check = "^0.1" - -[features] -testing = [] diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index 36746c4f..18647b37 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -156,8 +156,6 @@ impl ConfigBuilder { /// let mut config = Config::build(Environment::Staging) /// .session_key(key) /// .unwrap(); - /// - /// assert!(config.take_session_key().is_some()); /// ``` pub fn session_key>(mut self, key: K) -> Self { self.session_key = Some(key.into()); diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 1f48a8ac..e12554b6 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::net::{IpAddr, lookup_host}; use std::path::{Path, PathBuf}; -use std::sync::RwLock; use std::convert::AsRef; use std::fmt; use std::env; @@ -11,6 +10,29 @@ use config::{self, Value, ConfigBuilder, Environment, ConfigError}; use {num_cpus, base64}; use logger::LoggingLevel; +use http::Key; + +pub enum SessionKey { + Generated(Key), + Provided(Key) +} + +impl SessionKey { + #[inline] + pub fn kind(&self) -> &'static str { + match *self { + SessionKey::Generated(_) => "generated", + SessionKey::Provided(_) => "provided", + } + } + + #[inline] + fn inner(&self) -> &Key { + match *self { + SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key + } + } +} /// Structure for Rocket application configuration. /// @@ -44,7 +66,7 @@ pub struct Config { /// The path to the configuration file this config belongs to. pub config_path: PathBuf, /// The session key. - session_key: RwLock>>, + pub(crate) session_key: SessionKey, } macro_rules! parse { @@ -102,22 +124,17 @@ impl Config { Config::default(env, cwd.as_path().join("Rocket.custom.toml")) } - // Aliases to `default_for` before the method is removed. - pub(crate) fn default

(env: Environment, path: P) -> config::Result - where P: AsRef - { - #[allow(deprecated)] - Config::default_for(env, path) - } - /// Returns the default configuration for the environment `env` given that /// the configuration was stored at `config_path`. If `config_path` is not /// an absolute path, an `Err` of `ConfigError::BadFilePath` is returned. - #[deprecated(since="0.2", note="use the `new` or `build` methods instead")] - pub fn default_for

(env: Environment, config_path: P) -> config::Result + /// + /// # Panics + /// + /// Panics if randomness cannot be retrieved from the OS. + pub(crate) fn default

(env: Environment, path: P) -> config::Result where P: AsRef { - let config_path = config_path.as_ref().to_path_buf(); + let config_path = path.as_ref().to_path_buf(); if config_path.parent().is_none() { return Err(ConfigError::BadFilePath(config_path, "Configuration files must be rooted in a directory.")); @@ -126,6 +143,9 @@ impl Config { // Note: This may truncate if num_cpus::get() > u16::max. That's okay. let default_workers = ::std::cmp::max(num_cpus::get(), 2) as u16; + // Use a generated session key by default. + let key = SessionKey::Generated(Key::generate()); + Ok(match env { Development => { Config { @@ -134,7 +154,7 @@ impl Config { port: 8000, workers: default_workers, log_level: LoggingLevel::Normal, - session_key: RwLock::new(None), + session_key: key, extras: HashMap::new(), config_path: config_path, } @@ -146,7 +166,7 @@ impl Config { port: 80, workers: default_workers, log_level: LoggingLevel::Normal, - session_key: RwLock::new(None), + session_key: key, extras: HashMap::new(), config_path: config_path, } @@ -158,7 +178,7 @@ impl Config { port: 80, workers: default_workers, log_level: LoggingLevel::Critical, - session_key: RwLock::new(None), + session_key: key, extras: HashMap::new(), config_path: config_path, } @@ -348,7 +368,7 @@ impl Config { Err(_) => return Err(error) }; - self.session_key = RwLock::new(Some(bytes)); + self.session_key = SessionKey::Provided(Key::from_master(&bytes)); Ok(()) } @@ -425,33 +445,10 @@ impl Config { self.extras.iter().map(|(k, v)| (k.as_str(), v)) } - /// Moves the session key string out of the `self` Config, if there is one. - /// Because the value is moved out, subsequent calls will result in a return - /// value of `None`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// // Create a new config with a session key. - /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; - /// let config = Config::build(Environment::Staging) - /// .session_key(key) - /// .unwrap(); - /// - /// // Get the key for the first time. - /// let session_key = config.take_session_key(); - /// assert!(session_key.is_some()); - /// - /// // Try to get the key again. - /// let session_key_again = config.take_session_key(); - /// assert_eq!(session_key_again, None); - /// ``` + /// Retrieves the session key from `self`. #[inline] - pub fn take_session_key(&self) -> Option> { - let mut key = self.session_key.write().expect("couldn't lock session key"); - key.take() + pub(crate) fn session_key(&self) -> &Key { + self.session_key.inner() } /// Attempts to retrieve the extra named `name` as a string. diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 1fc3c04a..6849f33e 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -63,27 +63,29 @@ //! port = 8000 //! workers = max(number_of_cpus, 2) //! log = "normal" +//! session_key = [randomly generated at launch] //! //! [staging] //! address = "0.0.0.0" //! port = 80 //! workers = max(number_of_cpus, 2) //! log = "normal" -//! # don't use this key! generate your own and keep it private! -//! session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" +//! session_key = [randomly generated at launch] //! //! [production] //! address = "0.0.0.0" //! port = 80 //! workers = max(number_of_cpus, 2) //! log = "critical" -//! # don't use this key! generate your own and keep it private! -//! session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" +//! session_key = [randomly generated at launch] //! ``` //! -//! The `workers` parameter is computed by Rocket automatically; the value above -//! is not valid TOML syntax. When manually specifying the number of workers, -//! the value should be an integer: `workers = 10`. +//! The `workers` and `session_key` default parameters are computed by Rocket +//! automatically; the values above are not valid TOML syntax. When manually +//! specifying the number of workers, the value should be an integer: `workers = +//! 10`. When manually specifying the session key, the value should a 256-bit +//! base64 encoded string. Such a string can be generated with the `openssl` +//! command line tool: `openssl rand -base64 32`. //! //! The "global" pseudo-environment can be used to set and/or override //! configuration parameters globally. A parameter defined in a `[global]` table diff --git a/lib/src/http/cookies.rs b/lib/src/http/cookies.rs index 1371f156..cc655629 100644 --- a/lib/src/http/cookies.rs +++ b/lib/src/http/cookies.rs @@ -4,23 +4,34 @@ use std::cell::RefMut; pub use cookie::{Cookie, CookieJar, Iter, CookieBuilder, Delta}; +use cookie::{PrivateJar, Key}; + +impl<'a, 'c> From<&'a Cookie<'c>> for Header<'static> { + fn from(cookie: &Cookie) -> Header<'static> { + Header::new("Set-Cookie", cookie.encoded().to_string()) + } +} + #[derive(Debug)] pub enum Cookies<'a> { Jarred(RefMut<'a, CookieJar>), Empty(CookieJar) } -impl<'a> From> for Cookies<'a> { - fn from(jar: RefMut<'a, CookieJar>) -> Cookies<'a> { +impl<'a> Cookies<'a> { + pub(crate) fn new(jar: RefMut<'a, CookieJar>) -> Cookies<'a> { Cookies::Jarred(jar) } -} -impl<'a> Cookies<'a> { pub(crate) fn empty() -> Cookies<'static> { Cookies::Empty(CookieJar::new()) } + #[inline(always)] + pub(crate) fn parse_cookie(cookie_str: &str) -> Option> { + Cookie::parse_encoded(cookie_str).map(|c| c.into_owned()).ok() + } + pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { match *self { Cookies::Jarred(ref jar) => jar.get(name), @@ -40,6 +51,13 @@ impl<'a> Cookies<'a> { } } + pub(crate) fn private(&mut self, key: &Key) -> PrivateJar { + match *self { + Cookies::Jarred(ref mut jar) => jar.private(key), + Cookies::Empty(ref mut jar) => jar.private(key) + } + } + pub fn iter(&self) -> Iter { match *self { Cookies::Jarred(ref jar) => jar.iter(), @@ -47,16 +65,10 @@ impl<'a> Cookies<'a> { } } - pub fn delta(&self) -> Delta { + pub(crate) fn delta(&self) -> Delta { match *self { Cookies::Jarred(ref jar) => jar.delta(), Cookies::Empty(ref jar) => jar.delta() } } } - -impl<'a, 'c> From<&'a Cookie<'c>> for Header<'static> { - fn from(cookie: &Cookie) -> Header<'static> { - Header::new("Set-Cookie", cookie.encoded().to_string()) - } -} diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 7e5f3471..c015e328 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -9,6 +9,7 @@ pub mod hyper; pub mod uri; mod cookies; +mod session; mod method; mod content_type; mod status; @@ -23,4 +24,5 @@ pub use self::content_type::ContentType; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; -pub use self::cookies::{Cookie, Cookies, CookieJar, CookieBuilder}; +pub use self::cookies::*; +pub use self::session::*; diff --git a/lib/src/http/session.rs b/lib/src/http/session.rs new file mode 100644 index 00000000..b21dc2c4 --- /dev/null +++ b/lib/src/http/session.rs @@ -0,0 +1,62 @@ +use std::cell::{RefCell, RefMut}; + +use cookie::{Cookie, CookieJar, Delta}; +pub use cookie::Key; + +use http::{Header, Cookies}; + +const SESSION_PREFIX: &'static str = "__sess_"; + +pub struct Session<'a> { + cookies: RefCell>, + key: &'a Key +} + +impl<'a> Session<'a> { + #[inline(always)] + pub(crate) fn new(jar: RefMut<'a, CookieJar>, key: &'a Key) -> Session<'a> { + Session { cookies: RefCell::new(Cookies::new(jar)), key: key } + } + + #[inline(always)] + pub(crate) fn empty(key: &'a Key) -> Session<'a> { + Session { cookies: RefCell::new(Cookies::empty()), key: key } + } + + #[inline(always)] + pub(crate) fn header_for(cookie: &Cookie) -> Header<'static> { + Header::new("Set-Cookie", format!("{}{}", SESSION_PREFIX, cookie)) + } + + #[inline(always)] + pub(crate) fn parse_cookie(cookie_str: &str) -> Option> { + if !cookie_str.starts_with(SESSION_PREFIX) { + return None; + } + + let string = cookie_str[SESSION_PREFIX.len()..].to_string(); + Cookie::parse(string).ok() + } + + pub fn get(&self, name: &str) -> Option> { + self.cookies.borrow_mut().private(&self.key).get(name) + } + + pub fn add(&mut self, mut cookie: Cookie<'static>) { + cookie.set_http_only(true); + if cookie.path().is_none() { + cookie.set_path("/"); + } + + self.cookies.get_mut().private(&self.key).add(cookie) + } + + pub fn remove(&mut self, cookie: Cookie<'static>) { + self.cookies.get_mut().private(&self.key).remove(cookie) + } + + #[inline(always)] + pub(crate) fn delta(&mut self) -> Delta { + self.cookies.get_mut().delta() + } +} diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 2d9bc309..9ce5d925 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -5,7 +5,7 @@ use outcome::{self, IntoOutcome}; use request::Request; use outcome::Outcome::*; -use http::{Status, ContentType, Method, Cookies}; +use http::{Status, ContentType, Method, Cookies, Session}; use http::uri::URI; /// Type alias for the `Outcome` of a `FromRequest` conversion. @@ -85,11 +85,11 @@ impl IntoOutcome for Result { /// /// _This implementation always returns successfully._ /// -/// * **&Cookies** +/// * **Cookies** /// /// Returns a borrow to the [Cookies](/rocket/http/type.Cookies.html) in the /// incoming request. Note that `Cookies` implements internal mutability, so -/// a handle to `&Cookies` allows you to get _and_ set cookies in the +/// a handle to `Cookies` allows you to get _and_ set cookies in the /// request. /// /// _This implementation always returns successfully._ @@ -212,6 +212,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for Cookies<'a> { } } +impl<'a, 'r> FromRequest<'a, 'r> for Session<'a> { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + Success(request.session()) + } +} + impl<'a, 'r> FromRequest<'a, 'r> for ContentType { type Error = (); diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index a4e90608..cb7f6049 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -12,10 +12,7 @@ use super::{FromParam, FromSegments}; use router::Route; use http::uri::{URI, Segments}; -use http::{Method, ContentType, Header, HeaderMap, Cookie, Cookies}; - -use http::CookieJar; - +use http::{Method, ContentType, Header, HeaderMap, Cookies, Session, CookieJar, Key}; use http::hyper; /// The type of an incoming web request. @@ -28,10 +25,12 @@ use http::hyper; pub struct Request<'r> { method: Method, uri: URI<'r>, + key: Option<&'r Key>, headers: HeaderMap<'r>, remote: Option, params: RefCell>, cookies: RefCell, + session: RefCell, state: Option<&'r Container>, } @@ -54,9 +53,11 @@ impl<'r> Request<'r> { method: method, uri: uri.into(), headers: HeaderMap::new(), + key: None, remote: None, params: RefCell::new(Vec::new()), cookies: RefCell::new(CookieJar::new()), + session: RefCell::new(CookieJar::new()), state: None } } @@ -256,7 +257,7 @@ impl<'r> Request<'r> { #[inline] pub fn cookies(&self) -> Cookies { match self.cookies.try_borrow_mut() { - Ok(jar) => Cookies::from(jar), + Ok(jar) => Cookies::new(jar), Err(_) => { error_!("Multiple `Cookies` instances are active at once."); info_!("An instance of `Cookies` must be dropped before another \ @@ -267,12 +268,32 @@ impl<'r> Request<'r> { } } - /// Replace all of the cookies in `self` with `cookies`. + #[inline] + pub fn session(&self) -> Session { + match self.session.try_borrow_mut() { + Ok(jar) => Session::new(jar, self.key.unwrap()), + Err(_) => { + error_!("Multiple `Session` instances are active at once."); + info_!("An instance of `Session` must be dropped before another \ + can be retrieved."); + warn_!("The retrieved `Session` instance will be empty."); + Session::empty(self.key.unwrap()) + } + } + } + + /// Replace all of the cookies in `self` with those in `jar`. #[inline] pub(crate) fn set_cookies(&mut self, jar: CookieJar) { self.cookies = RefCell::new(jar); } + /// Replace all of the session cookie in `self` with those in `jar`. + #[inline] + pub(crate) fn set_session(&mut self, jar: CookieJar) { + self.session = RefCell::new(jar); + } + /// Returns `Some` of the Content-Type header of `self`. If the header is /// not present, returns `None`. /// @@ -407,6 +428,12 @@ impl<'r> Request<'r> { self.state = Some(state); } + /// Set the session key. For internal use only! + #[inline] + pub(crate) fn set_key(&mut self, key: &'r Key) { + self.key = Some(key); + } + /// Convert from Hyper types into a Rocket Request. pub(crate) fn from_hyp(h_method: hyper::Method, h_headers: hyper::header::Headers, @@ -428,26 +455,27 @@ impl<'r> Request<'r> { // Construct the request object. let mut request = Request::new(method, uri); - // Set the request cookies, if they exist. TODO: Use session key. + // Set the request cookies, if they exist. if let Some(cookie_headers) = h_headers.get_raw("Cookie") { - let mut jar = CookieJar::new(); + let mut cookie_jar = CookieJar::new(); + let mut session_jar = CookieJar::new(); for header in cookie_headers { let raw_str = match ::std::str::from_utf8(header) { Ok(string) => string, Err(_) => continue }; - for cookie_str in raw_str.split(";") { - let cookie = match Cookie::parse_encoded(cookie_str.to_string()) { - Ok(cookie) => cookie, - Err(_) => continue - }; - - jar.add_original(cookie); + for cookie_str in raw_str.split(";").map(|s| s.trim()) { + if let Some(cookie) = Session::parse_cookie(cookie_str) { + session_jar.add_original(cookie); + } else if let Some(cookie) = Cookies::parse_cookie(cookie_str) { + cookie_jar.add_original(cookie); + } } } - request.set_cookies(jar); + request.set_cookies(cookie_jar); + request.set_session(session_jar); } // Set the rest of the headers. diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index c0c5478f..14234cd4 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -223,10 +223,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> { let r = request.cookies().get(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| { trace_!("Flash: retrieving message: {:?}", cookie); - // Delete the flash cookie from the jar. - let orig_cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish(); - request.cookies().remove(orig_cookie); - // Parse the flash message. let content = cookie.value(); let (len_str, rest) = match content.find(|c: char| !c.is_digit(10)) { @@ -239,6 +235,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> { Ok(Flash::named(name, msg)) }); + // If we found a flash cookie, delete it from the jar. + if r.is_ok() { + let cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish(); + request.cookies().remove(cookie); + } + r.into_outcome() } } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index c225f681..fcda5707 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -20,7 +20,7 @@ use catcher::{self, Catcher}; use outcome::Outcome; use error::Error; -use http::{Method, Status, Header}; +use http::{Method, Status, Header, Session}; use http::hyper::{self, header}; use http::uri::URI; @@ -182,8 +182,9 @@ impl Rocket { -> Response<'r> { info!("{}:", request); - // Inform the request about the state. + // Inform the request about the state and session key. request.set_state(&self.state); + request.set_key(&self.config.session_key()); // Do a bit of preprocessing before routing. self.preprocess_request(request, &data); @@ -191,11 +192,16 @@ impl Rocket { // Route the request to get a response. match self.route(request, data) { Outcome::Success(mut response) => { - // A user's route responded! + // A user's route responded! Set the regular cookies. for cookie in request.cookies().delta() { response.adjoin_header(cookie); } + // And now the session cookies. + for cookie in request.session().delta() { + response.adjoin_header(Session::header_for(cookie)); + } + response } Outcome::Forward(data) => { @@ -342,13 +348,7 @@ impl Rocket { info_!("port: {}", White.paint(&config.port)); info_!("log: {}", White.paint(config.log_level)); info_!("workers: {}", White.paint(config.workers)); - - let session_key = config.take_session_key(); - if session_key.is_some() { - info_!("session key: {}", White.paint("present")); - warn_!("Signing and encryption of cookies is currently disabled."); - warn_!("See https://github.com/SergioBenitez/Rocket/issues/20 for info."); - } + info_!("session key: {}", White.paint(config.session_key.kind())); for (name, value) in config.extras() { info_!("{} {}: {}", Yellow.paint("[extra]"), name, White.paint(value)); From 4f8894f64578f8b62a65b33c81a60344ee851da5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 03:39:57 -0800 Subject: [PATCH 023/297] Don't allocate a String into after parsing a cookie. --- lib/src/http/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/http/session.rs b/lib/src/http/session.rs index b21dc2c4..efcb7920 100644 --- a/lib/src/http/session.rs +++ b/lib/src/http/session.rs @@ -34,8 +34,8 @@ impl<'a> Session<'a> { return None; } - let string = cookie_str[SESSION_PREFIX.len()..].to_string(); - Cookie::parse(string).ok() + Cookie::parse(&cookie_str[SESSION_PREFIX.len()..]).ok() + .map(|c| c.into_owned()) } pub fn get(&self, name: &str) -> Option> { From 63e89b04b42f4b5e98d13c2905ee3b5aa329406d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 14:25:58 -0800 Subject: [PATCH 024/297] Rename Session::add to Session::set. Also set a default expiration of 3 hours for session cookies. --- examples/session/src/main.rs | 2 +- lib/Cargo.toml | 2 +- lib/src/http/session.rs | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs index a94f98e2..7bb21eb2 100644 --- a/examples/session/src/main.rs +++ b/examples/session/src/main.rs @@ -40,7 +40,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { #[post("/login", data = "")] fn login(mut session: Session, login: Form) -> Flash { if login.get().username == "Sergio" && login.get().password == "password" { - session.add(Cookie::new("user_id", 1.to_string())); + session.set(Cookie::new("user_id", 1.to_string())); Flash::success(Redirect::to("/"), "Successfully logged in.") } else { Flash::error(Redirect::to("/login"), "Invalid username/password.") diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e2a85b2f..f58204ff 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -30,7 +30,7 @@ memchr = "1" base64 = "0.4" [dependencies.cookie] -version = "^0.7" +version = "0.7.2" features = ["percent-encode", "secure"] [dev-dependencies] diff --git a/lib/src/http/session.rs b/lib/src/http/session.rs index efcb7920..09f837ce 100644 --- a/lib/src/http/session.rs +++ b/lib/src/http/session.rs @@ -1,5 +1,6 @@ use std::cell::{RefCell, RefMut}; +use time::{self, Duration}; use cookie::{Cookie, CookieJar, Delta}; pub use cookie::Key; @@ -42,12 +43,20 @@ impl<'a> Session<'a> { self.cookies.borrow_mut().private(&self.key).get(name) } - pub fn add(&mut self, mut cookie: Cookie<'static>) { + pub fn set(&mut self, mut cookie: Cookie<'static>) { cookie.set_http_only(true); + if cookie.path().is_none() { cookie.set_path("/"); } + // TODO: Should this be configurable? + if cookie.max_age().is_none() && cookie.expires().is_none() { + let session_lifetime = Duration::hours(3); + cookie.set_max_age(session_lifetime); + cookie.set_expires(time::now() + session_lifetime); + } + self.cookies.get_mut().private(&self.key).add(cookie) } From a4f532da0969df234e0d543d5e911b81f3e08c61 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 14:29:24 -0800 Subject: [PATCH 025/297] Set examplem versions to 0.0.0. --- examples/config/Cargo.toml | 2 +- examples/content_types/Cargo.toml | 2 +- examples/cookies/Cargo.toml | 2 +- examples/errors/Cargo.toml | 2 +- examples/extended_validation/Cargo.toml | 2 +- examples/form_kitchen_sink/Cargo.toml | 2 +- examples/forms/Cargo.toml | 2 +- examples/from_request/Cargo.toml | 2 +- examples/handlebars_templates/Cargo.toml | 2 +- examples/hello_alt_methods/Cargo.toml | 2 +- examples/hello_person/Cargo.toml | 2 +- examples/hello_ranks/Cargo.toml | 2 +- examples/hello_world/Cargo.toml | 2 +- examples/json/Cargo.toml | 2 +- examples/manual_routes/Cargo.toml | 2 +- examples/optional_redirect/Cargo.toml | 2 +- examples/optional_result/Cargo.toml | 2 +- examples/pastebin/Cargo.toml | 2 +- examples/query_params/Cargo.toml | 2 +- examples/raw_sqlite/Cargo.toml | 2 +- examples/raw_upload/Cargo.toml | 2 +- examples/redirect/Cargo.toml | 2 +- examples/session/Cargo.toml | 2 +- examples/state/Cargo.toml | 2 +- examples/static_files/Cargo.toml | 2 +- examples/stream/Cargo.toml | 2 +- examples/testing/Cargo.toml | 2 +- examples/todo/Cargo.toml | 2 +- 28 files changed, 28 insertions(+), 28 deletions(-) diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml index 381710ad..884c1fa8 100644 --- a/examples/config/Cargo.toml +++ b/examples/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "config" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/content_types/Cargo.toml b/examples/content_types/Cargo.toml index c31e3cee..e1455c7e 100644 --- a/examples/content_types/Cargo.toml +++ b/examples/content_types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "content_types" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/cookies/Cargo.toml b/examples/cookies/Cargo.toml index cf32faba..ee0da5be 100644 --- a/examples/cookies/Cargo.toml +++ b/examples/cookies/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cookies" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/errors/Cargo.toml b/examples/errors/Cargo.toml index f7f251a6..9ef31a5c 100644 --- a/examples/errors/Cargo.toml +++ b/examples/errors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "errors" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/extended_validation/Cargo.toml b/examples/extended_validation/Cargo.toml index d0441ae5..c2c1ffa3 100644 --- a/examples/extended_validation/Cargo.toml +++ b/examples/extended_validation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "extended_validation" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/form_kitchen_sink/Cargo.toml b/examples/form_kitchen_sink/Cargo.toml index 202de636..67f9102e 100644 --- a/examples/form_kitchen_sink/Cargo.toml +++ b/examples/form_kitchen_sink/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "form_kitchen_sink" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/forms/Cargo.toml b/examples/forms/Cargo.toml index de6b87c6..6c73b2df 100644 --- a/examples/forms/Cargo.toml +++ b/examples/forms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "forms" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/from_request/Cargo.toml b/examples/from_request/Cargo.toml index a4d459d3..31b232a0 100644 --- a/examples/from_request/Cargo.toml +++ b/examples/from_request/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "from_request" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/handlebars_templates/Cargo.toml b/examples/handlebars_templates/Cargo.toml index c2e45575..3a5b4266 100644 --- a/examples/handlebars_templates/Cargo.toml +++ b/examples/handlebars_templates/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "handlebars_templates" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/hello_alt_methods/Cargo.toml b/examples/hello_alt_methods/Cargo.toml index abd35276..d3f67872 100644 --- a/examples/hello_alt_methods/Cargo.toml +++ b/examples/hello_alt_methods/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello_alt_methods" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/hello_person/Cargo.toml b/examples/hello_person/Cargo.toml index d703fbab..93ba8a66 100644 --- a/examples/hello_person/Cargo.toml +++ b/examples/hello_person/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello_person" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/hello_ranks/Cargo.toml b/examples/hello_ranks/Cargo.toml index e975e227..f3b6dc95 100644 --- a/examples/hello_ranks/Cargo.toml +++ b/examples/hello_ranks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello_ranks" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index b56f14d5..9e726476 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello_world" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index d95ee879..3bee1177 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/manual_routes/Cargo.toml b/examples/manual_routes/Cargo.toml index 150614fe..af75e891 100644 --- a/examples/manual_routes/Cargo.toml +++ b/examples/manual_routes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "manual_routes" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/optional_redirect/Cargo.toml b/examples/optional_redirect/Cargo.toml index ce7dae01..90ee8ab0 100644 --- a/examples/optional_redirect/Cargo.toml +++ b/examples/optional_redirect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "optional_redirect" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/optional_result/Cargo.toml b/examples/optional_result/Cargo.toml index cb5a0a59..b5ecb5b9 100644 --- a/examples/optional_result/Cargo.toml +++ b/examples/optional_result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "optional_result" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml index 3304556e..907ff59a 100644 --- a/examples/pastebin/Cargo.toml +++ b/examples/pastebin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pastebin" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/query_params/Cargo.toml b/examples/query_params/Cargo.toml index 41f2700b..45dd3c2c 100644 --- a/examples/query_params/Cargo.toml +++ b/examples/query_params/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "query_params" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/raw_sqlite/Cargo.toml b/examples/raw_sqlite/Cargo.toml index 00192fd1..293ddd6c 100644 --- a/examples/raw_sqlite/Cargo.toml +++ b/examples/raw_sqlite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "raw_sqlite" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/raw_upload/Cargo.toml b/examples/raw_upload/Cargo.toml index 0eeab4f1..d8282040 100644 --- a/examples/raw_upload/Cargo.toml +++ b/examples/raw_upload/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "raw_upload" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/redirect/Cargo.toml b/examples/redirect/Cargo.toml index cbfdacf1..2e4693be 100644 --- a/examples/redirect/Cargo.toml +++ b/examples/redirect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "redirect" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml index d1f37b9d..64c480e8 100644 --- a/examples/session/Cargo.toml +++ b/examples/session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "session" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/state/Cargo.toml b/examples/state/Cargo.toml index 48efe471..c41a40eb 100644 --- a/examples/state/Cargo.toml +++ b/examples/state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "state" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/static_files/Cargo.toml b/examples/static_files/Cargo.toml index e90b7c34..09159834 100644 --- a/examples/static_files/Cargo.toml +++ b/examples/static_files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "static_files" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/stream/Cargo.toml b/examples/stream/Cargo.toml index 9d563d72..fdd32bde 100644 --- a/examples/stream/Cargo.toml +++ b/examples/stream/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stream" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/testing/Cargo.toml b/examples/testing/Cargo.toml index f1ea0514..98770f56 100644 --- a/examples/testing/Cargo.toml +++ b/examples/testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "testing" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index 525e7896..abc6c276 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "todo" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] From 393225cedfe7e749af2b525b8451ecfde14c32b3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 15:08:13 -0800 Subject: [PATCH 026/297] Use ansi_term::Color, not Colour. --- codegen/Cargo.toml | 4 ++-- codegen/build.rs | 2 +- examples/todo/bootstrap.sh | 2 +- lib/Cargo.toml | 2 +- lib/build.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 2135faad..6f97d944 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -22,5 +22,5 @@ log = "^0.3" compiletest_rs = "^0.2" [build-dependencies] -ansi_term = "^0.9" -version_check = "^0.1" +ansi_term = "0.9" +version_check = "0.1" diff --git a/codegen/build.rs b/codegen/build.rs index b3a21eaf..9d0d887c 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -4,7 +4,7 @@ extern crate ansi_term; extern crate version_check; -use ansi_term::Colour::{Red, Yellow, Blue, White}; +use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. diff --git a/examples/todo/bootstrap.sh b/examples/todo/bootstrap.sh index f6be9914..661d0851 100755 --- a/examples/todo/bootstrap.sh +++ b/examples/todo/bootstrap.sh @@ -6,7 +6,7 @@ DATABASE_URL=${SCRIPT_PATH}/db/db.sql pushd $SCRIPT_PATH > /dev/null # install the diesel CLI tools if they're not installed if ! command -v diesel >/dev/null 2>&1; then - cargo install diesel_cli > /dev/null + cargo install diesel_cli --no-default-features --features=sqlite > /dev/null fi # create db/db.sql diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f58204ff..95360614 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -38,5 +38,5 @@ lazy_static = "0.2" rocket_codegen = { version = "0.2.2", path = "../codegen" } [build-dependencies] -ansi_term = "^0.9" +ansi_term = "0.9" version_check = "^0.1" diff --git a/lib/build.rs b/lib/build.rs index 7fa9f524..e159803a 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -4,7 +4,7 @@ extern crate ansi_term; extern crate version_check; -use ansi_term::Colour::{Red, Yellow, Blue, White}; +use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version}; // Specifies the minimum nightly version needed to compile Rocket. From 4f704e95f2987cf365ef798c03c6a47d67c086a2 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 1 Mar 2017 21:39:18 -0800 Subject: [PATCH 027/297] Capitalize Rocket in Server response header. --- lib/src/rocket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index fcda5707..cc58ac8f 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -79,7 +79,7 @@ impl Rocket { fn issue_response(&self, mut response: Response, hyp_res: hyper::FreshResponse) { // Add the 'rocket' server header, and write out the response. // TODO: If removing Hyper, write out `Date` header too. - response.set_header(Header::new("Server", "rocket")); + response.set_header(Header::new("Server", "Rocket")); match self.write_response(response, hyp_res) { Ok(_) => info_!("{}", Green.paint("Response succeeded.")), From d43678c35ea3cbaab9343faf1565638f44f94570 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Tue, 7 Feb 2017 22:40:14 -0500 Subject: [PATCH 028/297] Add MsgPack implementation to contrib. --- Cargo.toml | 1 + contrib/Cargo.toml | 4 +- contrib/src/lib.rs | 8 ++ contrib/src/msgpack/mod.rs | 134 ++++++++++++++++++++++++++++++++++ examples/msgpack/Cargo.toml | 18 +++++ examples/msgpack/src/main.rs | 38 ++++++++++ examples/msgpack/src/tests.rs | 44 +++++++++++ lib/src/http/content_type.rs | 1 + lib/src/response/content.rs | 1 + lib/src/testing.rs | 4 +- 10 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 contrib/src/msgpack/mod.rs create mode 100644 examples/msgpack/Cargo.toml create mode 100644 examples/msgpack/src/main.rs create mode 100644 examples/msgpack/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 3934e3aa..130c84ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "examples/from_request", "examples/stream", "examples/json", + "examples/msgpack", "examples/handlebars_templates", "examples/form_kitchen_sink", "examples/config", diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 48b2998b..849e8ecf 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT/Apache-2.0" [features] default = ["json"] json = ["serde", "serde_json"] +msgpack = ["serde", "rmp-serde"] tera_templates = ["tera", "templates"] handlebars_templates = ["handlebars", "templates"] @@ -27,9 +28,10 @@ log = "^0.3" # UUID dependencies. uuid = { version = "^0.4", optional = true } -# JSON and templating dependencies. +# Serialization and templating dependencies. serde = { version = "^0.9", optional = true } serde_json = { version = "^0.9.3", optional = true } +rmp-serde = { version = "^0.12", optional = true } # Templating dependencies only. handlebars = { version = "^0.25", optional = true, features = ["serde_type"] } diff --git a/contrib/src/lib.rs b/contrib/src/lib.rs index 18945897..a90c4dbc 100644 --- a/contrib/src/lib.rs +++ b/contrib/src/lib.rs @@ -14,6 +14,7 @@ //! an asterisk next to the features that are enabled by default: //! //! * [json*](struct.JSON.html) +//! * [msgpack](struct.MsgPack.html) //! * [handlebars_templates](struct.Template.html) //! * [tera_templates](struct.Template.html) //! * [uuid](struct.UUID.html) @@ -55,6 +56,13 @@ pub mod json; #[cfg(feature = "json")] pub use json::{JSON, SerdeError, Value}; +#[cfg(feature = "msgpack")] +#[doc(hidden)] +pub mod msgpack; + +#[cfg(feature = "msgpack")] +pub use msgpack::{MsgPack, MsgPackError}; + #[cfg(feature = "templates")] mod templates; diff --git a/contrib/src/msgpack/mod.rs b/contrib/src/msgpack/mod.rs new file mode 100644 index 00000000..acf47520 --- /dev/null +++ b/contrib/src/msgpack/mod.rs @@ -0,0 +1,134 @@ +extern crate rmp_serde; + +use std::ops::{Deref, DerefMut}; +use std::io::{Cursor, Read}; + +use rocket::outcome::Outcome; +use rocket::request::Request; +use rocket::data::{self, Data, FromData}; +use rocket::response::{self, Responder, Response}; +use rocket::http::{ContentType, Status}; + +use serde::{Serialize, Deserialize}; + +pub use self::rmp_serde::decode::Error as MsgPackError; + +/// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to easily +/// consume and respond with MessagePack data. +/// +/// If you're receiving MessagePack data, simply add a `data` parameter to your route +/// arguments and ensure the type of the parameter is a `MsgPack`, where `T` is +/// some type you'd like to parse from MessagePack. `T` must implement `Deserialize` +/// from [Serde](https://github.com/serde-rs/serde). The data is parsed from the +/// HTTP request body. +/// +/// ```rust,ignore +/// #[post("/users/", format = "application/msgpack", data = "")] +/// fn new_user(user: MsgPack) { +/// ... +/// } +/// ``` +/// +/// You don't _need_ to use `format = "application/msgpack"`, but it _may_ be what +/// you want. Using `format = application/msgpack` means that any request that +/// doesn't specify "application/msgpack" as its first `Content-Type:` header +/// parameter will not be routed to this handler. By default, Rocket will accept a +/// Content Type of any of the following for MessagePack data: +/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or `bin/x-msgpack`. +/// +/// If you're responding with MessagePack data, return a `MsgPack` type, where `T` +/// implements `Serialize` from [Serde](https://github.com/serde-rs/serde). The +/// content type of the response is set to `application/msgpack` automatically. +/// +/// ```rust,ignore +/// #[get("/users/")] +/// fn user(id: usize) -> MsgPack { +/// let user_from_id = User::from(id); +/// ... +/// MsgPack(user_from_id) +/// } +/// ``` +#[derive(Debug)] +pub struct MsgPack(pub T); + +impl MsgPack { + /// Consumes the `MsgPack` wrapper and returns the wrapped item. + /// + /// # Example + /// ```rust + /// # use rocket_contrib::MsgPack; + /// let string = "Hello".to_string(); + /// let my_msgpack = MsgPack(string); + /// assert_eq!(my_msgpack.into_inner(), "Hello".to_string()); + /// ``` + pub fn into_inner(self) -> T { + self.0 + } +} + +/// Maximum size of MessagePack data is 1MB. +/// TODO: Determine this size from some configuration parameter. +const MAX_SIZE: u64 = 1048576; + +/// Accepted content types are: +/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, and `bin/x-msgpack` +fn is_msgpack_content_type(ct: &ContentType) -> bool { + (ct.ttype == "application" || ct.ttype == "bin") + && (ct.subtype == "msgpack" || ct.subtype == "x-msgpack") +} + +impl FromData for MsgPack { + type Error = MsgPackError; + + fn from_data(request: &Request, data: Data) -> data::Outcome { + if !request.content_type().map_or(false, |ct| is_msgpack_content_type(&ct)) { + error_!("Content-Type is not MessagePack."); + return Outcome::Forward(data); + } + + let mut buf = Vec::new(); + if let Err(e) = data.open().take(MAX_SIZE).read_to_end(&mut buf) { + let e = MsgPackError::InvalidDataRead(e); + error_!("Couldn't read request data: {:?}", e); + return Outcome::Failure((Status::BadRequest, e)); + }; + + match rmp_serde::from_slice(&buf).map(|val| MsgPack(val)) { + Ok(value) => Outcome::Success(value), + Err(e) => { + error_!("Couldn't parse MessagePack body: {:?}", e); + Outcome::Failure((Status::BadRequest, e)) + } + } + } +} + +/// Serializes the wrapped value into MessagePack. Returns a response with Content-Type +/// MessagePack and a fixed-size body with the serialization. If serialization fails, an +/// `Err` of `Status::InternalServerError` is returned. +impl Responder<'static> for MsgPack { + fn respond(self) -> response::Result<'static> { + rmp_serde::to_vec(&self.0).map_err(|e| { + error_!("MsgPack failed to serialize: {:?}", e); + Status::InternalServerError + }).and_then(|buf| { + Response::build() + .sized_body(Cursor::new(buf)) + .ok() + }) + } +} + +impl Deref for MsgPack { + type Target = T; + + fn deref<'a>(&'a self) -> &'a T { + &self.0 + } +} + +impl DerefMut for MsgPack { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.0 + } +} diff --git a/examples/msgpack/Cargo.toml b/examples/msgpack/Cargo.toml new file mode 100644 index 00000000..590e3349 --- /dev/null +++ b/examples/msgpack/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "msgpack" +version = "0.0.1" +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } +serde = "0.9" +serde_derive = "0.9" + +[dependencies.rocket_contrib] +path = "../../contrib" +default-features = false +features = ["msgpack"] + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/msgpack/src/main.rs b/examples/msgpack/src/main.rs new file mode 100644 index 00000000..ffff2400 --- /dev/null +++ b/examples/msgpack/src/main.rs @@ -0,0 +1,38 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; +extern crate rocket_contrib; +#[macro_use] extern crate serde_derive; + +#[cfg(test)] mod tests; + +use rocket_contrib::MsgPack; + +#[derive(Serialize, Deserialize)] +struct Message { + id: usize, + contents: String +} + +#[get("/", format = "application/msgpack")] +fn get(id: usize) -> MsgPack { + MsgPack(Message { + id: id, + contents: "Hello, world!".to_string(), + }) +} + +#[post("/", data = "", format = "application/msgpack")] +fn create(data: MsgPack) -> Result { + Ok(data.into_inner().contents) +} + +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount("/message", routes![get, create]) +} + +fn main() { + rocket().launch(); +} diff --git a/examples/msgpack/src/tests.rs b/examples/msgpack/src/tests.rs new file mode 100644 index 00000000..0419a075 --- /dev/null +++ b/examples/msgpack/src/tests.rs @@ -0,0 +1,44 @@ +use rocket; +use rocket::testing::MockRequest; +use rocket::http::Method::*; +use rocket::http::{Status, ContentType}; +use rocket::Response; + +#[derive(Serialize, Deserialize)] +struct Message { + id: usize, + contents: String +} + +macro_rules! run_test { + ($rocket: expr, $req:expr, $test_fn:expr) => ({ + let mut req = $req; + $test_fn(req.dispatch_with($rocket)); + }) +} + +#[test] +fn msgpack_get() { + let rocket = rocket(); + let req = MockRequest::new(Get, "/message/1").header(ContentType::MsgPack); + run_test!(&rocket, req, |mut response: Response| { + assert_eq!(response.status(), Status::Ok); + let body = response.body().unwrap().into_bytes().unwrap(); + // Represents a message of `[1, "Hello, world!"]` + assert_eq!(&body, &[146, 1, 173, 72, 101, 108, 108, 111, 44, 32, 119, 111, + 114, 108, 100, 33]); + }); +} + +#[test] +fn msgpack_post() { + let rocket = rocket(); + let req = MockRequest::new(Post, "/message") + .header(ContentType::MsgPack) + // Represents a message of `[2, "Goodbye, world!"]` + .body(&[146, 2, 175, 71, 111, 111, 100, 98, 121, 101, 44, 32, 119, 111, 114, 108, 100, 33]); + run_test!(&rocket, req, |mut response: Response| { + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.body().unwrap().into_string().unwrap(), "Goodbye, world!"); + }); +} diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index be35667f..7ec8e46c 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -104,6 +104,7 @@ impl ContentType { "HTML", HTML, is_html => "text", "html" ; "charset=utf-8", "Plain", Plain, is_plain => "text", "plain" ; "charset=utf-8", "JSON", JSON, is_json => "application", "json", + "MsgPack", MsgPack, is_msgpack => "application", "msgpack", "form", Form, is_form => "application", "x-www-form-urlencoded", "JavaScript", JavaScript, is_javascript => "application", "javascript", "CSS", CSS, is_css => "text", "css" ; "charset=utf-8", diff --git a/lib/src/response/content.rs b/lib/src/response/content.rs index dc15776d..38403e4b 100644 --- a/lib/src/response/content.rs +++ b/lib/src/response/content.rs @@ -82,6 +82,7 @@ macro_rules! ctrs { ctrs! { JSON: "JSON", "application/json", XML: "XML", "text/xml", + MsgPack: "MessagePack", "application/msgpack", HTML: "HTML", "text/html", Plain: "plain text", "text/plain", CSS: "CSS", "text/css", diff --git a/lib/src/testing.rs b/lib/src/testing.rs index d23ae494..a7e71eb8 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -224,8 +224,8 @@ impl<'r> MockRequest<'r> { /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#); /// ``` #[inline] - pub fn body>(mut self, body: S) -> Self { - self.data = Data::new(body.as_ref().as_bytes().into()); + pub fn body>(mut self, body: S) -> Self { + self.data = Data::new(body.as_ref().into()); self } From fc89d0e96c1137cda78973cde094f1f73572262f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 15:17:37 -0800 Subject: [PATCH 029/297] Move {json,msgpack}/mod.rs to top-level contrib/src. --- contrib/src/{json/mod.rs => json.rs} | 0 contrib/src/{msgpack/mod.rs => msgpack.rs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename contrib/src/{json/mod.rs => json.rs} (100%) rename contrib/src/{msgpack/mod.rs => msgpack.rs} (100%) diff --git a/contrib/src/json/mod.rs b/contrib/src/json.rs similarity index 100% rename from contrib/src/json/mod.rs rename to contrib/src/json.rs diff --git a/contrib/src/msgpack/mod.rs b/contrib/src/msgpack.rs similarity index 100% rename from contrib/src/msgpack/mod.rs rename to contrib/src/msgpack.rs From 1683102e74e3f3b00314396a7f5e8f1b01c5f07e Mon Sep 17 00:00:00 2001 From: aStoate Date: Mon, 9 Jan 2017 12:32:12 +1030 Subject: [PATCH 030/297] Add tests for manual_routes example. --- examples/manual_routes/Cargo.toml | 3 ++ examples/manual_routes/src/main.rs | 18 +++++++-- examples/manual_routes/src/tests.rs | 63 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 examples/manual_routes/src/tests.rs diff --git a/examples/manual_routes/Cargo.toml b/examples/manual_routes/Cargo.toml index af75e891..3b20079f 100644 --- a/examples/manual_routes/Cargo.toml +++ b/examples/manual_routes/Cargo.toml @@ -5,3 +5,6 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index c95c7e90..1e23f3f7 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -1,5 +1,8 @@ extern crate rocket; +#[cfg(test)] +mod tests; + use std::io; use std::fs::File; @@ -7,6 +10,7 @@ use rocket::{Request, Route, Data, Catcher, Error}; use rocket::http::Status; use rocket::request::FromParam; use rocket::response::{self, Responder}; +use rocket::response::status::Custom; use rocket::handler::Outcome; use rocket::http::Method::*; @@ -23,7 +27,10 @@ fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> { } fn echo_url(req: &Request, _: Data) -> Outcome<'static> { - let param = req.uri().as_str().split_at(6).1; + let param = req.uri() + .as_str() + .split_at(6) + .1; Outcome::of(String::from_param(param).unwrap()) } @@ -52,10 +59,10 @@ fn get_upload(_: &Request, _: Data) -> Outcome<'static> { } fn not_found_handler<'r>(_: Error, req: &'r Request) -> response::Result<'r> { - format!("Couldn't find: {}", req.uri()).respond() + Custom(Status::NotFound, format!("Couldn't find: {}", req.uri())).respond() } -fn main() { +fn rocket() -> rocket::Rocket { let always_forward = Route::ranked(1, Get, "/", forward); let hello = Route::ranked(2, Get, "/", hi); @@ -72,5 +79,8 @@ fn main() { .mount("/hello", vec![name.clone()]) .mount("/hi", vec![name]) .catch(vec![not_found_catcher]) - .launch(); +} + +fn main() { + rocket().launch(); } diff --git a/examples/manual_routes/src/tests.rs b/examples/manual_routes/src/tests.rs new file mode 100644 index 00000000..859e050d --- /dev/null +++ b/examples/manual_routes/src/tests.rs @@ -0,0 +1,63 @@ +use super::*; +use rocket::testing::MockRequest; +use rocket::http::{ContentType, Status}; +use rocket::http::Method::*; + +fn test(uri: &str, content_type: ContentType, status: Status, body: String) { + let rocket = rocket(); + let mut request = MockRequest::new(Get, uri).header(content_type); + let mut response = request.dispatch_with(&rocket); + + assert_eq!(response.status(), status); + assert_eq!(response.body().and_then(|b| b.into_string()), Some(body)); +} + +#[test] +fn test_forward() { + test("/", ContentType::Plain, Status::Ok, "Hello!".to_string()); +} + +#[test] +fn test_name() { + for &name in &[("John"), ("Mike"), ("Angela")] { + let uri = format!("/hello/{}", name); + test(&uri, ContentType::Plain, Status::Ok, name.to_string()); + } +} + +#[test] +fn test_echo() { + let echo = "echo text"; + let uri = format!("/echo:echo text"); + test(&uri, ContentType::Plain, Status::Ok, echo.to_string()); +} + +#[test] +fn test_upload() { + let expected_body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \ + sed do eiusmod tempor incididunt ut labore et dolore \ + magna aliqua"; + let rocket = rocket(); + let mut request = MockRequest::new(Post, "/upload") + .header(ContentType::Plain) + .body(expected_body); + let response = request.dispatch_with(&rocket); + + assert_eq!(response.status(), Status::Ok); + + let mut request = MockRequest::new(Get, "/upload"); + let mut response = request.dispatch_with(&rocket); + + let expected = expected_body.to_string(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected)); +} + +#[test] +fn test_not_found() { + let uri = "/wrong_address"; + test(uri, + ContentType::Plain, + Status::NotFound, + format!("Couldn't find: {}", uri)); +} From ca2bde63867fa439aa913c634b83d5d80e91acb1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 15:22:36 -0800 Subject: [PATCH 031/297] Slight cleanup of manual_routes example. --- examples/manual_routes/src/tests.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/manual_routes/src/tests.rs b/examples/manual_routes/src/tests.rs index 859e050d..452bdcb9 100644 --- a/examples/manual_routes/src/tests.rs +++ b/examples/manual_routes/src/tests.rs @@ -34,30 +34,28 @@ fn test_echo() { #[test] fn test_upload() { + let rocket = rocket(); let expected_body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \ sed do eiusmod tempor incididunt ut labore et dolore \ - magna aliqua"; - let rocket = rocket(); + magna aliqua".to_string(); + + // Upload the body. let mut request = MockRequest::new(Post, "/upload") .header(ContentType::Plain) - .body(expected_body); + .body(&expected_body); let response = request.dispatch_with(&rocket); - assert_eq!(response.status(), Status::Ok); + // Ensure we get back the same body. let mut request = MockRequest::new(Get, "/upload"); let mut response = request.dispatch_with(&rocket); - - let expected = expected_body.to_string(); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected)); + assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected_body)); } #[test] fn test_not_found() { let uri = "/wrong_address"; - test(uri, - ContentType::Plain, - Status::NotFound, - format!("Couldn't find: {}", uri)); + let expected_body = format!("Couldn't find: {}", uri); + test(uri, ContentType::Plain, Status::NotFound, expected_body); } From c465109fb43c15f56e6c2585382a4dc58b3ca480 Mon Sep 17 00:00:00 2001 From: Artem Biryukov Date: Tue, 21 Feb 2017 05:43:17 +0300 Subject: [PATCH 032/297] Add `get_slice` and `get_table` methods to `Config`. --- lib/src/config/config.rs | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index e12554b6..f6087e1c 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -547,6 +547,58 @@ impl Config { parse!(self, name, value, as_float, "a float") } + /// Attempts to retrieve the extra named `name` as a slice of an array. + /// + /// # Errors + /// + /// If an extra with `name` doesn't exist, returns an `Err` of `NotFound`. + /// If an extra with `name` _does_ exist but is not an array, returns a + /// `BadType` error. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::{Config, Environment}; + /// + /// let config = Config::build(Environment::Staging) + /// .extra("numbers", vec![1, 2, 3]) + /// .unwrap(); + /// + /// assert!(config.get_slice("numbers").is_ok()); + /// ``` + pub fn get_slice(&self, name: &str) -> config::Result<&[Value]> { + let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + parse!(self, name, value, as_slice, "a slice") + } + + /// Attempts to retrieve the extra named `name` as a table. + /// + /// # Errors + /// + /// If an extra with `name` doesn't exist, returns an `Err` of `NotFound`. + /// If an extra with `name` _does_ exist but is not a table, returns a + /// `BadType` error. + /// + /// # Example + /// + /// ```rust + /// use std::collections::BTreeMap; + /// use rocket::config::{Config, Environment}; + /// + /// let mut table = BTreeMap::new(); + /// table.insert("my_value".to_string(), 1); + /// + /// let config = Config::build(Environment::Staging) + /// .extra("my_table", table) + /// .unwrap(); + /// + /// assert!(config.get_table("my_table").is_ok()); + /// ``` + pub fn get_table(&self, name: &str) -> config::Result<&config::Table> { + let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + parse!(self, name, value, as_table, "a table") + } + /// Returns the path at which the configuration file for `self` is stored. /// For instance, if the configuration file is at `/tmp/Rocket.toml`, the /// path `/tmp` is returned. From 5086f0eb01bcd2a789993ab0444dbd14ede272d0 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Tue, 31 Jan 2017 10:36:37 -0800 Subject: [PATCH 033/297] Default generic T in `JSON` to `Value`. --- contrib/src/json.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contrib/src/json.rs b/contrib/src/json.rs index f8842219..f8ab601b 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -29,6 +29,7 @@ pub use serde_json::error::Error as SerdeError; /// ... /// } /// ``` +/// /// You don't _need_ to use `format = "application/json"`, but it _may_ be what /// you want. Using `format = application/json` means that any request that /// doesn't specify "application/json" as its `Content-Type` header value will @@ -46,9 +47,8 @@ pub use serde_json::error::Error as SerdeError; /// JSON(user_from_id) /// } /// ``` -/// #[derive(Debug)] -pub struct JSON(pub T); +pub struct JSON(pub T); impl JSON { /// Consumes the JSON wrapper and returns the wrapped item. @@ -129,14 +129,16 @@ impl DerefMut for JSON { /// ``` /// /// The return type of a macro invocation is -/// [Value](/rocket_contrib/enum.Value.html). A value created with this macro -/// can be returned from a handler as follows: +/// [Value](/rocket_contrib/enum.Value.html). This is the default value for the +/// type parameter of [JSON](/rocket_contrib/struct.JSON.html) and as such, you +/// can return `JSON` without specifying the type. A value created with this +/// macro can be returned from a handler as follows: /// /// ```rust,ignore -/// use rocket_contrib::{JSON, Value}; +/// use rocket_contrib::JSON; /// /// #[get("/json")] -/// fn get_json() -> JSON { +/// fn get_json() -> JSON { /// JSON(json!({ /// "key": "value", /// "array": [1, 2, 3, 4] From 5434f467a9ca77c85f821ac6eb54997fb7ccd9fa Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 8 Mar 2017 17:56:08 -0800 Subject: [PATCH 034/297] Check msgpack example version to 0.0.0. --- examples/msgpack/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/msgpack/Cargo.toml b/examples/msgpack/Cargo.toml index 590e3349..4d0bd76b 100644 --- a/examples/msgpack/Cargo.toml +++ b/examples/msgpack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "msgpack" -version = "0.0.1" +version = "0.0.0" workspace = "../../" [dependencies] From 47fe659ebe64e4079105798aa8850dc952c1bdd9 Mon Sep 17 00:00:00 2001 From: Scott Schroeder Date: Fri, 10 Mar 2017 17:42:09 -0800 Subject: [PATCH 035/297] Preserve multiple incoming header values. --- lib/src/request/mod.rs | 98 ++++++++++++++++++++++++++++++++++++++ lib/src/request/request.rs | 11 ++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib/src/request/mod.rs b/lib/src/request/mod.rs index e94be1ab..205972df 100644 --- a/lib/src/request/mod.rs +++ b/lib/src/request/mod.rs @@ -15,3 +15,101 @@ pub use self::state::State; /// Type alias to retrieve [Flash](/rocket/response/struct.Flash.html) messages /// from a request. pub type FlashMessage = ::response::Flash<()>; + +#[cfg(test)] +mod tests { + /// These tests are related to Issue#223 + /// The way we were getting the headers from hyper + /// was causing a list to come back as a comma separated + /// list of entries. + + use super::Request; + use super::super::http::hyper::header::Headers; + use hyper::method::Method; + use hyper::uri::RequestUri; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::collections::HashMap; + + fn check_headers(test_headers: HashMap>) { + let h_method: Method = Method::Get; + let h_uri: RequestUri = RequestUri::AbsolutePath("/test".to_string()); + let h_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000); + let mut h_headers: Headers = Headers::new(); + + for (key, values) in &test_headers { + let raw_bytes: Vec> = values + .iter() + .map(|v| v.clone().into_bytes()) + .collect(); + h_headers.set_raw(key.clone(), raw_bytes); + } + + let req = match Request::from_hyp(h_method, h_headers, h_uri, h_addr) { + Ok(req) => req, + Err(e) => panic!("Building Request failed: {:?}", e), + }; + + let r_headers = req.headers(); + + for (key, values) in &test_headers { + for (v1, v2) in values.iter().zip(r_headers.get(&key)) { + assert_eq!(v1, v2) + } + } + } + + #[test] + fn test_single_header_single_entry() { + let mut test_headers = HashMap::new(); + test_headers.insert("friends".to_string(), vec![ + "alice".to_string(), + ]); + check_headers(test_headers); + } + + #[test] + fn test_single_header_multiple_entries() { + let mut test_headers = HashMap::new(); + test_headers.insert("friends".to_string(), vec![ + "alice".to_string(), + "bob".to_string() + ]); + check_headers(test_headers); + } + + #[test] + fn test_single_header_comma_entry() { + let mut test_headers = HashMap::new(); + test_headers.insert("friends".to_string(), vec![ + "alice".to_string(), + "bob, carol".to_string() + ]); + check_headers(test_headers); + } + + #[test] + fn test_multiple_headers_single_entry() { + let mut test_headers = HashMap::new(); + test_headers.insert("friends".to_string(), vec![ + "alice".to_string(), + ]); + test_headers.insert("enemies".to_string(), vec![ + "victor".to_string(), + ]); + check_headers(test_headers); + } + + #[test] + fn test_multiple_headers_multiple_entries() { + let mut test_headers = HashMap::new(); + test_headers.insert("friends".to_string(), vec![ + "alice".to_string(), + "bob".to_string(), + ]); + test_headers.insert("enemies".to_string(), vec![ + "david".to_string(), + "emily".to_string(), + ]); + check_headers(test_headers); + } +} diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index cb7f6049..c664bc65 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use std::net::SocketAddr; use std::fmt; +use std::str; use term_painter::Color::*; use term_painter::ToStyle; @@ -480,8 +481,14 @@ impl<'r> Request<'r> { // Set the rest of the headers. for hyp in h_headers.iter() { - let header = Header::new(hyp.name().to_string(), hyp.value_string()); - request.add_header(header); + if let Some(header_values) = h_headers.get_raw(hyp.name()) { + for value in header_values { + let value_str = str::from_utf8(value) + .map_err(|_| format!("Bad Header: {:?}", hyp))?; + let header = Header::new(hyp.name().to_string(), value_str.to_string()); + request.add_header(header); + } + } } // Set the remote address. From ca30e5e90132f85c6c6f068b6f7fe545170e9618 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 15 Mar 2017 19:20:20 -0700 Subject: [PATCH 036/297] Cleanup request tests; move into separate file. --- lib/src/request/mod.rs | 101 ++----------------------------------- lib/src/request/request.rs | 6 +-- lib/src/request/tests.rs | 46 +++++++++++++++++ 3 files changed, 52 insertions(+), 101 deletions(-) create mode 100644 lib/src/request/tests.rs diff --git a/lib/src/request/mod.rs b/lib/src/request/mod.rs index 205972df..358fed60 100644 --- a/lib/src/request/mod.rs +++ b/lib/src/request/mod.rs @@ -6,6 +6,9 @@ mod form; mod from_request; mod state; +#[cfg(test)] +mod tests; + pub use self::request::Request; pub use self::from_request::{FromRequest, Outcome}; pub use self::param::{FromParam, FromSegments}; @@ -15,101 +18,3 @@ pub use self::state::State; /// Type alias to retrieve [Flash](/rocket/response/struct.Flash.html) messages /// from a request. pub type FlashMessage = ::response::Flash<()>; - -#[cfg(test)] -mod tests { - /// These tests are related to Issue#223 - /// The way we were getting the headers from hyper - /// was causing a list to come back as a comma separated - /// list of entries. - - use super::Request; - use super::super::http::hyper::header::Headers; - use hyper::method::Method; - use hyper::uri::RequestUri; - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use std::collections::HashMap; - - fn check_headers(test_headers: HashMap>) { - let h_method: Method = Method::Get; - let h_uri: RequestUri = RequestUri::AbsolutePath("/test".to_string()); - let h_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000); - let mut h_headers: Headers = Headers::new(); - - for (key, values) in &test_headers { - let raw_bytes: Vec> = values - .iter() - .map(|v| v.clone().into_bytes()) - .collect(); - h_headers.set_raw(key.clone(), raw_bytes); - } - - let req = match Request::from_hyp(h_method, h_headers, h_uri, h_addr) { - Ok(req) => req, - Err(e) => panic!("Building Request failed: {:?}", e), - }; - - let r_headers = req.headers(); - - for (key, values) in &test_headers { - for (v1, v2) in values.iter().zip(r_headers.get(&key)) { - assert_eq!(v1, v2) - } - } - } - - #[test] - fn test_single_header_single_entry() { - let mut test_headers = HashMap::new(); - test_headers.insert("friends".to_string(), vec![ - "alice".to_string(), - ]); - check_headers(test_headers); - } - - #[test] - fn test_single_header_multiple_entries() { - let mut test_headers = HashMap::new(); - test_headers.insert("friends".to_string(), vec![ - "alice".to_string(), - "bob".to_string() - ]); - check_headers(test_headers); - } - - #[test] - fn test_single_header_comma_entry() { - let mut test_headers = HashMap::new(); - test_headers.insert("friends".to_string(), vec![ - "alice".to_string(), - "bob, carol".to_string() - ]); - check_headers(test_headers); - } - - #[test] - fn test_multiple_headers_single_entry() { - let mut test_headers = HashMap::new(); - test_headers.insert("friends".to_string(), vec![ - "alice".to_string(), - ]); - test_headers.insert("enemies".to_string(), vec![ - "victor".to_string(), - ]); - check_headers(test_headers); - } - - #[test] - fn test_multiple_headers_multiple_entries() { - let mut test_headers = HashMap::new(); - test_headers.insert("friends".to_string(), vec![ - "alice".to_string(), - "bob".to_string(), - ]); - test_headers.insert("enemies".to_string(), vec![ - "david".to_string(), - "emily".to_string(), - ]); - check_headers(test_headers); - } -} diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index c664bc65..bab853f3 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -483,9 +483,9 @@ impl<'r> Request<'r> { for hyp in h_headers.iter() { if let Some(header_values) = h_headers.get_raw(hyp.name()) { for value in header_values { - let value_str = str::from_utf8(value) - .map_err(|_| format!("Bad Header: {:?}", hyp))?; - let header = Header::new(hyp.name().to_string(), value_str.to_string()); + // This is not totally correct since values needn't be UTF8. + let value_str = String::from_utf8_lossy(value).into_owned(); + let header = Header::new(hyp.name().to_string(), value_str); request.add_header(header); } } diff --git a/lib/src/request/tests.rs b/lib/src/request/tests.rs new file mode 100644 index 00000000..1f4ed8bb --- /dev/null +++ b/lib/src/request/tests.rs @@ -0,0 +1,46 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::collections::HashMap; + +use {hyper, Request}; + +macro_rules! assert_headers { + ($($key:expr => [$($value:expr),+]),+) => ({ + // Set up the parameters to the hyper request object. + let h_method = hyper::method::Method::Get; + let h_uri = hyper::uri::RequestUri::AbsolutePath("/test".to_string()); + let h_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000); + let mut h_headers = hyper::header::Headers::new(); + + // Add all of the passed in headers to the request. + $($(h_headers.append_raw($key.to_string(), $value.as_bytes().into());)+)+ + + // Build up what we expect the headers to actually be. + let mut expected = HashMap::new(); + $(expected.entry($key).or_insert(vec![]).append(&mut vec![$($value),+]);)+ + + // Dispatch the request and check that the headers are what we expect. + let req = Request::from_hyp(h_method, h_headers, h_uri, h_addr).unwrap(); + let actual_headers = req.headers(); + for (key, values) in expected.iter() { + let actual: Vec<_> = actual_headers.get(key).collect(); + assert_eq!(*values, actual); + } + }) +} + +#[test] +fn test_multiple_headers_from_hyp() { + assert_headers!("friends" => ["alice"]); + assert_headers!("friends" => ["alice", "bob"]); + assert_headers!("friends" => ["alice", "bob, carol"]); + assert_headers!("friends" => ["alice, david", "bob, carol", "eric, frank"]); + assert_headers!("friends" => ["alice"], "enemies" => ["victor"]); + assert_headers!("friends" => ["alice", "bob"], "enemies" => ["david", "emily"]); +} + +#[test] +fn test_multiple_headers_merge_into_one_from_hyp() { + assert_headers!("friend" => ["alice"], "friend" => ["bob"]); + assert_headers!("friend" => ["alice"], "friend" => ["bob"], "friend" => ["carol"]); + assert_headers!("friend" => ["alice"], "friend" => ["bob"], "enemy" => ["carol"]); +} From 7139941e04ef711d4de9c1469c65cd3040bff4d9 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 15 Mar 2017 19:26:15 -0700 Subject: [PATCH 037/297] Ensure no files have trailing whitespace. --- examples/uuid/src/main.rs | 8 ++++---- scripts/test.sh | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/uuid/src/main.rs b/examples/uuid/src/main.rs index 47a4832f..712db9bd 100644 --- a/examples/uuid/src/main.rs +++ b/examples/uuid/src/main.rs @@ -15,8 +15,8 @@ use rocket_contrib::UUID; mod tests; lazy_static! { - // A small people lookup table for the sake of this example. In a real - // application this could be a database lookup. Notice that we use the + // A small people lookup table for the sake of this example. In a real + // application this could be a database lookup. Notice that we use the // uuid::Uuid type here and not the rocket_contrib::UUID type. static ref PEOPLE: HashMap = { let mut m = HashMap::new(); @@ -32,8 +32,8 @@ lazy_static! { #[get("/people/")] fn people(id: UUID) -> Result { - // Because UUID implements the Deref trait, we use Deref coercion to - // convert rocket_contrib::UUID to uuid::Uuid. + // Because UUID implements the Deref trait, we use Deref coercion to convert + // rocket_contrib::UUID to uuid::Uuid. Ok(PEOPLE.get(&id) .map(|person| format!("We found: {}", person)) .ok_or(format!("Person not found for UUID: {}", id))?) diff --git a/scripts/test.sh b/scripts/test.sh index b9de6755..6a9b891b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -28,7 +28,7 @@ function check_versions_match() { done } -# Ensures there are not tabs in any file in the directories $@. +# Ensures there are no tabs in any file. function ensure_tab_free() { local tab=$(printf '\t') local matches=$(grep -I -R "${tab}" $ROOT_DIR | egrep -v '/target|/.git|LICENSE') @@ -39,6 +39,16 @@ function ensure_tab_free() { fi } +# Ensures there are no files with trailing whitespace. +function ensure_trailing_whitespace_free() { + local matches=$(egrep -I -R " +$" $ROOT_DIR | egrep -v "/target|/.git") + if ! [ -z "${matches}" ]; then + echo "Trailing whitespace was found in the following:" + echo "${matches}" + exit 1 + fi +} + function bootstrap_examples() { for file in ${EXAMPLES_DIR}/*; do if [ -d "${file}" ]; then @@ -65,6 +75,9 @@ check_versions_match "${LIB_DIR}" "${CODEGEN_DIR}" "${CONTRIB_DIR}" echo ":: Checking for tabs..." ensure_tab_free +echo ":: Checking for trailing whitespace..." +ensure_trailing_whitespace_free + echo ":: Updating dependencies..." cargo update From 9d10aa23295377bf8c4d4638864f9b2acfdcd402 Mon Sep 17 00:00:00 2001 From: Alan Stoate Date: Fri, 10 Mar 2017 09:52:32 +1030 Subject: [PATCH 038/297] Update `Catcher` example: returns Status::NotFound on 404. --- lib/src/catcher.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/catcher.rs b/lib/src/catcher.rs index f9a86065..cc418ec2 100644 --- a/lib/src/catcher.rs +++ b/lib/src/catcher.rs @@ -79,9 +79,11 @@ impl Catcher { /// # #![allow(unused_variables)] /// use rocket::{Catcher, Request, Error}; /// use rocket::response::{Result, Responder}; + /// use rocket::response::status::Custom; + /// use rocket::http::Status; /// /// fn handle_404<'r>(_: Error, req: &'r Request) -> Result<'r> { - /// format!("Couldn't find: {}", req.uri()).respond() + /// Custom(Status::NotFound, format!("Couldn't find: {}", req.uri())).respond() /// } /// /// fn handle_500<'r>(_: Error, _: &'r Request) -> Result<'r> { From da157a061d4dd1a58f4094a703940c28dc828caa Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 15 Mar 2017 20:30:07 -0700 Subject: [PATCH 039/297] Don't use hyper directly in request tests. --- lib/src/request/tests.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/request/tests.rs b/lib/src/request/tests.rs index 1f4ed8bb..9b7ab7b9 100644 --- a/lib/src/request/tests.rs +++ b/lib/src/request/tests.rs @@ -1,13 +1,14 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::collections::HashMap; -use {hyper, Request}; +use Request; +use http::hyper; macro_rules! assert_headers { ($($key:expr => [$($value:expr),+]),+) => ({ // Set up the parameters to the hyper request object. - let h_method = hyper::method::Method::Get; - let h_uri = hyper::uri::RequestUri::AbsolutePath("/test".to_string()); + let h_method = hyper::Method::Get; + let h_uri = hyper::RequestUri::AbsolutePath("/test".to_string()); let h_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8000); let mut h_headers = hyper::header::Headers::new(); From 65da98896219674e2ea9356626d9936969cbb3b3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 15 Mar 2017 22:10:09 -0700 Subject: [PATCH 040/297] Return a `LaunchError` from `launch` when launching fails. This is a (minor) breaking change. If `rocket.launch()` is the last expression in a function, the return type will change from `()` to `LaunchError`. A simple workaround that preserves the previous functionality is to simply add a semicolon after `launch()`: `rocket.launch();`. resolves #34 --- examples/config/src/main.rs | 2 +- examples/cookies/src/main.rs | 2 +- examples/errors/src/main.rs | 5 +- examples/extended_validation/src/main.rs | 2 +- examples/forms/src/main.rs | 2 +- examples/from_request/src/main.rs | 2 +- examples/optional_redirect/src/main.rs | 2 +- examples/pastebin/src/main.rs | 2 +- examples/query_params/src/main.rs | 2 +- examples/session/src/main.rs | 2 +- examples/testing/src/main.rs | 2 +- lib/src/catcher.rs | 2 +- lib/src/error.rs | 158 +++++++++++++++++++++++ lib/src/http/hyper.rs | 1 + lib/src/lib.rs | 6 +- lib/src/request/state.rs | 2 +- lib/src/response/flash.rs | 2 +- lib/src/rocket.rs | 35 ++--- 18 files changed, 199 insertions(+), 32 deletions(-) diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index 84583182..f53c6559 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -6,5 +6,5 @@ extern crate config; // This example's illustration is the Rocket.toml file. fn main() { - rocket::ignite().mount("/hello", routes![config::hello]).launch() + rocket::ignite().mount("/hello", routes![config::hello]).launch(); } diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index 243c1954..f4f114dc 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -37,5 +37,5 @@ fn index(cookies: Cookies) -> Template { } fn main() { - rocket::ignite().mount("/", routes![submit, index]).launch() + rocket::ignite().mount("/", routes![submit, index]).launch(); } diff --git a/examples/errors/src/main.rs b/examples/errors/src/main.rs index 47ebd736..cebdc34b 100644 --- a/examples/errors/src/main.rs +++ b/examples/errors/src/main.rs @@ -20,8 +20,11 @@ fn not_found(req: &rocket::Request) -> content::HTML { } fn main() { - rocket::ignite() + let e = rocket::ignite() .mount("/", routes![hello]) .catch(errors![not_found]) .launch(); + + println!("Whoops! Rocket didn't launch!"); + println!("This went wrong: {}", e); } diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index 20a8a2f1..347ac9da 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -85,5 +85,5 @@ fn rocket() -> rocket::Rocket { } fn main() { - rocket().launch() + rocket().launch(); } diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 0f2608cd..6ab3c601 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -48,5 +48,5 @@ fn rocket() -> rocket::Rocket { } fn main() { - rocket().launch() + rocket().launch(); } diff --git a/examples/from_request/src/main.rs b/examples/from_request/src/main.rs index 40134bea..f9d4d813 100644 --- a/examples/from_request/src/main.rs +++ b/examples/from_request/src/main.rs @@ -29,7 +29,7 @@ fn header_count(header_count: HeaderCount) -> String { } fn main() { - rocket::ignite().mount("/", routes![header_count]).launch() + rocket::ignite().mount("/", routes![header_count]).launch(); } #[cfg(test)] diff --git a/examples/optional_redirect/src/main.rs b/examples/optional_redirect/src/main.rs index 95ee9911..a25fcf08 100644 --- a/examples/optional_redirect/src/main.rs +++ b/examples/optional_redirect/src/main.rs @@ -26,5 +26,5 @@ fn login() -> &'static str { } fn main() { - rocket::ignite().mount("/", routes![root, user, login]).launch() + rocket::ignite().mount("/", routes![root, user, login]).launch(); } diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index e7c08a4e..b15cdf1b 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -53,5 +53,5 @@ fn index() -> &'static str { } fn main() { - rocket::ignite().mount("/", routes![index, upload, retrieve]).launch() + rocket::ignite().mount("/", routes![index, upload, retrieve]).launch(); } diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs index 636904ab..37059bf6 100644 --- a/examples/query_params/src/main.rs +++ b/examples/query_params/src/main.rs @@ -21,5 +21,5 @@ fn hello(person: Person) -> String { } fn main() { - rocket::ignite().mount("/", routes![hello]).launch() + rocket::ignite().mount("/", routes![hello]).launch(); } diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs index 7bb21eb2..a2733b5d 100644 --- a/examples/session/src/main.rs +++ b/examples/session/src/main.rs @@ -83,5 +83,5 @@ fn index() -> Redirect { fn main() { rocket::ignite() .mount("/", routes![index, user_index, login, logout, login_user, login_page]) - .launch() + .launch(); } diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 885316ce..0ca16f63 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -9,7 +9,7 @@ fn hello() -> &'static str { } fn main() { - rocket::ignite().mount("/", routes![hello]).launch() + rocket::ignite().mount("/", routes![hello]).launch(); } #[cfg(test)] diff --git a/lib/src/catcher.rs b/lib/src/catcher.rs index cc418ec2..ba4498d2 100644 --- a/lib/src/catcher.rs +++ b/lib/src/catcher.rs @@ -55,7 +55,7 @@ use term_painter::Color::*; /// /// fn main() { /// # if false { // We don't actually want to launch the server in an example. -/// rocket::ignite().catch(errors![internal_error, not_found]).launch() +/// rocket::ignite().catch(errors![internal_error, not_found]).launch(); /// # } /// } /// ``` diff --git a/lib/src/error.rs b/lib/src/error.rs index b141dcef..a9ce4c02 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,3 +1,10 @@ +//! Types representing various errors that can occur in a Rocket application. + +use std::{io, fmt}; +use std::sync::atomic::{Ordering, AtomicBool}; + +use http::hyper; + /// [unstable] Error type for Rocket. Likely to change. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Error { @@ -12,3 +19,154 @@ pub enum Error { /// The requested key/index does not exist. NoKey, } + +/// The kind of launch error that occured. +/// +/// In almost every instance, a launch error occurs because of an I/O error; +/// this represented by the `Io` variant. The `Unknown` variant captures all +/// other kinds of launch errors. +#[derive(Debug)] +pub enum LaunchErrorKind { + Io(io::Error), + Unknown(Box<::std::error::Error + Send + Sync>) +} + +/// An error that occurred during launch. +/// +/// A `LaunchError` is returned by +/// [rocket::launch](/rocket/struct.Rocket.html#method.launch) when launching an +/// application fails for some reason. +/// +/// # Panics +/// +/// A value of this type panics if it is dropped without first being inspected. +/// An _inspection_ occurs when any method is called. For instance, if +/// `println!("Error: {}", e)` is called, where `e: LaunchError`, the +/// `Display::fmt` method being called by `println!` results in `e` being marked +/// as inspected; a subsequent `drop` of the value will _not_ result in a panic. +/// The following snippet illustrates this: +/// +/// ```rust +/// # if false { +/// let error = rocket::ignite().launch(); +/// +/// // This line is only reached if launching failed. This "inspects" the error. +/// println!("Launch failed! Error: {}", error); +/// +/// // This call to drop (explicit here for demonstration) will do nothing. +/// drop(error); +/// # } +/// ``` +/// +/// When a value of this type panics, the corresponding error message is pretty +/// printed to the console. The following snippet illustrates this: +/// +/// ```rust +/// # if false { +/// let error = rocket::ignite().launch(); +/// +/// // This call to drop (explicit here for demonstration) will result in +/// // `error` being pretty-printed to the console along with a `panic!`. +/// drop(error); +/// # } +/// ``` +pub struct LaunchError { + handled: AtomicBool, + kind: LaunchErrorKind +} + +impl LaunchError { + #[inline(always)] + fn new(kind: LaunchErrorKind) -> LaunchError { + LaunchError { handled: AtomicBool::new(false), kind: kind } + } + + #[inline(always)] + fn was_handled(&self) -> bool { + self.handled.load(Ordering::Acquire) + } + + #[inline(always)] + fn mark_handled(&self) { + self.handled.store(true, Ordering::Release) + } + + /// Retrieve the `kind` of the launch error. + /// + /// # Example + /// + /// ```rust + /// # if false { + /// let error = rocket::ignite().launch(); + /// + /// // This line is only reached if launch failed. + /// let error_kind = error.kind(); + /// # } + /// ``` + #[inline] + pub fn kind(&self) -> &LaunchErrorKind { + self.mark_handled(); + &self.kind + } +} + +impl From for LaunchError { + fn from(error: hyper::Error) -> LaunchError { + match error { + hyper::Error::Io(e) => LaunchError::new(LaunchErrorKind::Io(e)), + e => LaunchError::new(LaunchErrorKind::Unknown(Box::new(e))) + } + } +} + +impl fmt::Display for LaunchErrorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), + LaunchErrorKind::Unknown(ref e) => write!(f, "unknown error: {}", e) + } + } +} + +impl fmt::Debug for LaunchError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.mark_handled(); + write!(f, "{:?}", self.kind()) + } +} + +impl fmt::Display for LaunchError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.mark_handled(); + write!(f, "{}", self.kind()) + } +} + +impl ::std::error::Error for LaunchError { + fn description(&self) -> &str { + self.mark_handled(); + match *self.kind() { + LaunchErrorKind::Io(_) => "an I/O error occured during launch", + LaunchErrorKind::Unknown(_) => "an unknown error occured during launch" + } + } +} + +impl Drop for LaunchError { + fn drop(&mut self) { + if self.was_handled() { + return + } + + match *self.kind() { + LaunchErrorKind::Io(ref e) => { + error!("Rocket failed to launch due to an I/O error."); + panic!("{}", e); + } + LaunchErrorKind::Unknown(ref e) => { + error!("Rocket failed to launch due to an unknown error."); + panic!("{}", e); + } + } + } +} diff --git a/lib/src/http/hyper.rs b/lib/src/http/hyper.rs index 504e9d67..54814df4 100644 --- a/lib/src/http/hyper.rs +++ b/lib/src/http/hyper.rs @@ -13,6 +13,7 @@ pub(crate) use hyper::net; pub(crate) use hyper::method::Method; pub(crate) use hyper::status::StatusCode; +pub(crate) use hyper::error::Error; pub(crate) use hyper::uri::RequestUri; pub(crate) use hyper::http::h1; pub(crate) use hyper::buffer; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6066853b..77240bca 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -74,7 +74,7 @@ //! //! fn main() { //! # if false { // We don't actually want to launch the server in an example. -//! rocket::ignite().mount("/", routes![hello]).launch() +//! rocket::ignite().mount("/", routes![hello]).launch(); //! # } //! } //! ``` @@ -117,8 +117,8 @@ pub mod outcome; pub mod config; pub mod data; pub mod handler; +pub mod error; -mod error; mod router; mod rocket; mod codegen; @@ -133,7 +133,7 @@ mod ext; #[doc(inline)] pub use data::Data; pub use router::Route; pub use request::{Request, State}; -pub use error::Error; +pub use error::{Error, LaunchError}; pub use catcher::Catcher; pub use rocket::Rocket; diff --git a/lib/src/request/state.rs b/lib/src/request/state.rs index 5fc37e24..c76d9fc3 100644 --- a/lib/src/request/state.rs +++ b/lib/src/request/state.rs @@ -46,7 +46,7 @@ use http::Status; /// rocket::ignite() /// .mount("/", routes![index, raw_config_value]) /// .manage(config) -/// .launch() +/// .launch(); /// # } /// } /// ``` diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index 14234cd4..ceb629e2 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -71,7 +71,7 @@ const FLASH_COOKIE_NAME: &'static str = "_flash"; /// /// fn main() { /// # if false { // We don't actually want to launch the server in an example. -/// rocket::ignite().mount("/", routes![login, index]).launch() +/// rocket::ignite().mount("/", routes![login, index]).launch(); /// # } /// } /// ``` diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index cc58ac8f..092785f7 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -18,7 +18,7 @@ use response::{Body, Response}; use router::{Router, Route}; use catcher::{self, Catcher}; use outcome::Outcome; -use error::Error; +use error::{Error, LaunchError}; use http::{Method, Status, Header, Session}; use http::hyper::{self, header}; @@ -391,7 +391,7 @@ impl Rocket { /// fn main() { /// # if false { // We don't actually want to launch the server in an example. /// rocket::ignite().mount("/hello", routes![hi]) - /// # .launch() + /// # .launch(); /// # } /// } /// ``` @@ -411,7 +411,7 @@ impl Rocket { /// /// # if false { // We don't actually want to launch the server in an example. /// rocket::ignite().mount("/hello", vec![Route::new(Get, "/world", hi)]) - /// # .launch() + /// # .launch(); /// # } /// ``` pub fn mount(mut self, base: &str, routes: Vec) -> Self { @@ -459,7 +459,7 @@ impl Rocket { /// fn main() { /// # if false { // We don't actually want to launch the server in an example. /// rocket::ignite().catch(errors![internal_error, not_found]) - /// # .launch() + /// # .launch(); /// # } /// } /// ``` @@ -514,7 +514,7 @@ impl Rocket { /// rocket::ignite() /// .mount("/", routes![index]) /// .manage(MyValue(10)) - /// .launch() + /// .launch(); /// # } /// } /// ``` @@ -530,19 +530,23 @@ impl Rocket { /// Starts the application server and begins listening for and dispatching /// requests to mounted routes and catchers. /// - /// # Panics + /// # Error /// - /// If the server could not be started, this method prints the reason and - /// then exits the process. + /// If there is a problem starting the application, a + /// [LaunchError](/rocket/struct.LaunchError.html) is returned. Note + /// that a value of type `LaunchError` panics if dropped without first being + /// inspected. See the [LaunchError + /// documentation](/rocket/struct.LaunchError.html) for more + /// information. /// /// # Examples /// /// ```rust /// # if false { - /// rocket::ignite().launch() + /// rocket::ignite().launch(); /// # } /// ``` - pub fn launch(self) { + pub fn launch(self) -> LaunchError { if self.router.has_collisions() { warn!("Route collisions detected!"); } @@ -550,10 +554,7 @@ impl Rocket { let full_addr = format!("{}:{}", self.config.address, self.config.port); let server = match hyper::Server::http(full_addr.as_str()) { Ok(hyper_server) => hyper_server, - Err(e) => { - error!("Failed to start server."); - panic!("{}", e); - } + Err(e) => return LaunchError::from(e) }; info!("🚀 {} {}{}...", @@ -562,6 +563,10 @@ impl Rocket { White.bold().paint(&full_addr)); let threads = self.config.workers as usize; - server.handle_threads(self, threads).unwrap(); + if let Err(e) = server.handle_threads(self, threads) { + return LaunchError::from(e); + } + + unreachable!("the call to `handle_threads` should block on success") } } From ec92046d3ac3d8fc772b3cf4eba990726593b937 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 16 Mar 2017 00:51:28 -0700 Subject: [PATCH 041/297] Clarify that rocket::launch doesn't return on success. --- lib/src/rocket.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 092785f7..0ac1e753 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -528,7 +528,8 @@ impl Rocket { } /// Starts the application server and begins listening for and dispatching - /// requests to mounted routes and catchers. + /// requests to mounted routes and catchers. Unless there is an error, this + /// function does not return and blocks until program termination. /// /// # Error /// From 7808ad164973ebc7259bbdcb0539b782f2beba41 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 20 Mar 2017 13:55:40 -0700 Subject: [PATCH 042/297] Update codegen for latest nightly. --- codegen/build.rs | 2 +- codegen/src/lints/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen/build.rs b/codegen/build.rs index 9d0d887c..703af08b 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -8,7 +8,7 @@ use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. -const MIN_DATE: &'static str = "2017-02-26"; +const MIN_DATE: &'static str = "2017-03-19"; const MIN_VERSION: &'static str = "1.17.0-nightly"; // Convenience macro for writing to stderr. diff --git a/codegen/src/lints/mod.rs b/codegen/src/lints/mod.rs index c978db17..1c3ef1dd 100644 --- a/codegen/src/lints/mod.rs +++ b/codegen/src/lints/mod.rs @@ -229,7 +229,7 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for RocketLint { let attr_value = kind.attrs().iter().filter_map(|attr| { match attr.check_name(ROUTE_ATTR) { false => None, - true => attr.value.meta_item_list().and_then(|list| list[0].name()) + true => attr.meta_item_list().and_then(|list| list[0].name()) } }).next(); From d09b4138d961798f27b95c1efc31c1eb37178e59 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 21 Mar 2017 02:04:07 -0700 Subject: [PATCH 043/297] Implement the (currently unused) MediaType struct. --- lib/Cargo.toml | 3 + lib/src/config/mod.rs | 1 + lib/src/http/ascii.rs | 7 + lib/src/http/known_media_types.rs | 50 ++++ lib/src/http/media_type.rs | 375 ++++++++++++++++++++++++++++++ lib/src/http/mod.rs | 5 + lib/src/http/parse/checkers.rs | 13 ++ lib/src/http/parse/indexed_str.rs | 54 +++++ lib/src/http/parse/media_type.rs | 67 ++++++ lib/src/http/parse/mod.rs | 6 + lib/src/lib.rs | 5 + 11 files changed, 586 insertions(+) create mode 100644 lib/src/http/known_media_types.rs create mode 100644 lib/src/http/media_type.rs create mode 100644 lib/src/http/parse/checkers.rs create mode 100644 lib/src/http/parse/indexed_str.rs create mode 100644 lib/src/http/parse/media_type.rs create mode 100644 lib/src/http/parse/mod.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 95360614..39489ef7 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -28,6 +28,9 @@ state = "0.2" time = "0.1" memchr = "1" base64 = "0.4" +smallvec = "0.3" +pear = "0.0.2" +pear_codegen = "0.0.2" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 6849f33e..d7caf835 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -498,6 +498,7 @@ mod test { const TEST_CONFIG_FILENAME: &'static str = "/tmp/testing/Rocket.toml"; + // TODO: It's a shame we have to depend on lazy_static just for this. lazy_static! { static ref ENV_LOCK: Mutex = Mutex::new(0); } diff --git a/lib/src/http/ascii.rs b/lib/src/http/ascii.rs index 64509dc4..8f71f8c2 100644 --- a/lib/src/http/ascii.rs +++ b/lib/src/http/ascii.rs @@ -90,6 +90,13 @@ impl Ord for UncasedAsciiRef { } } +impl fmt::Display for UncasedAsciiRef { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + /// An uncased (case-preserving) ASCII string. #[derive(Clone, Debug)] pub struct UncasedAscii<'s> { diff --git a/lib/src/http/known_media_types.rs b/lib/src/http/known_media_types.rs new file mode 100644 index 00000000..8731e1c0 --- /dev/null +++ b/lib/src/http/known_media_types.rs @@ -0,0 +1,50 @@ +macro_rules! known_media_types { + ($cont:ident) => ($cont! { + Any (is_any): "any Content-Type", "*", "*", + HTML (is_html): "HTML", "text", "html" ; "charset" => "utf-8", + Plain (is_plain): "plaintext", "text", "plain" ; "charset" => "utf-8", + JSON (is_json): "JSON", "application", "json", + Form (is_form): "forms", "application", "x-www-form-urlencoded", + JavaScript (is_javascript): "JavaScript", "application", "javascript", + CSS (is_css): "CSS", "text", "css" ; "charset" => "utf-8", + FormData (is_form_data): "multipart form data", "multipart", "form-data", + XML (is_xml): "XML", "text", "xml" ; "charset" => "utf-8", + CSV (is_csv): "CSV", "text", "csv" ; "charset" => "utf-8", + PNG (is_png): "PNG", "image", "png", + GIF (is_gif): "GIF", "image", "gif", + BMP (is_bmp): "BMP", "image", "bmp", + JPEG (is_jpeg): "JPEG", "image", "jpeg", + WEBP (is_webp): "WEBP", "image", "webp", + SVG (is_svg): "SVG", "image", "svg+xml", + PDF (is_pdf): "PDF", "application", "pdf", + TTF (is_ttf): "TTF", "application", "font-sfnt", + OTF (is_otf): "OTF", "application", "font-sfnt", + WOFF (is_woff): "WOFF", "application", "font-woff", + WOFF2 (is_woff2): "WOFF2", "font", "woff2" + }) +} + +macro_rules! known_extensions { + ($cont:ident) => ($cont! { + "txt" => Plain, + "html" => HTML, + "htm" => HTML, + "xml" => XML, + "csv" => CSV, + "js" => JavaScript, + "css" => CSS, + "json" => JSON, + "png" => PNG, + "gif" => GIF, + "bmp" => BMP, + "jpeg" => JPEG, + "jpg" => JPEG, + "webp" => WEBP, + "svg" => SVG, + "pdf" => PDF, + "ttf" => TTF, + "otf" => OTF, + "woff" => WOFF, + "woff2" => WOFF2 + }) +} diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs new file mode 100644 index 00000000..38fc9985 --- /dev/null +++ b/lib/src/http/media_type.rs @@ -0,0 +1,375 @@ +use std::borrow::Cow; +use std::str::FromStr; +use std::fmt; +use std::hash::{Hash, Hasher}; + +use http::ascii::{uncased_eq, UncasedAsciiRef}; +use http::parse::{IndexedStr, parse_media_type}; + +use smallvec::SmallVec; + +#[derive(Debug, Clone)] +struct MediaParam { + key: IndexedStr, + value: IndexedStr, +} + +// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`. +#[derive(Debug, Clone)] +pub enum MediaParams { + Empty, + Static(&'static [(IndexedStr, IndexedStr)]), + Dynamic(SmallVec<[(IndexedStr, IndexedStr); 2]>) +} + +// TODO: impl PartialEq, Hash for `MediaType`. +#[derive(Debug, Clone)] +pub struct MediaType { + /// Storage for the entire media type string. This will be `Some` when the + /// media type was parsed from a string and `None` when it was created + /// manually. + #[doc(hidden)] + pub source: Option>, + /// The top-level type. + #[doc(hidden)] + pub top: IndexedStr, + /// The subtype. + #[doc(hidden)] + pub sub: IndexedStr, + /// The parameters, if any. + #[doc(hidden)] + pub params: MediaParams +} + +macro_rules! media_str { + ($string:expr) => (IndexedStr::Concrete(Cow::Borrowed($string))) +} + +macro_rules! media_types { + ($($name:ident ($check:ident): $str:expr, $t:expr, + $s:expr $(; $k:expr => $v:expr)*),+) => { + $( + #[doc="[MediaType](struct.MediaType.html) for "] + #[doc=$str] + #[doc=": "] #[doc=$t] #[doc="/"] #[doc=$s] #[doc=""] + #[allow(non_upper_case_globals)] + pub const $name: MediaType = MediaType { + source: None, + top: media_str!($t), + sub: media_str!($s), + params: MediaParams::Static(&[$((media_str!($k), media_str!($v))),*]) + }; + + #[inline(always)] + pub fn $check(&self) -> bool { + *self == MediaType::$name + } + )+ + + /// Returns `true` if this MediaType is known to Rocket, that is, + /// there is an associated constant for `self`. + pub fn is_known(&self) -> bool { + $(if self.$check() { return true })+ + false + } + }; +} + +macro_rules! from_extension { + ($($ext:expr => $name:ident),*) => ( + pub fn from_extension(ext: &str) -> Option { + match ext { + $(x if uncased_eq(x, $ext) => Some(MediaType::$name)),*, + _ => None + } + } + ) +} + +impl MediaType { + #[inline] + pub fn new(top: T, sub: S) -> MediaType + where T: Into>, S: Into> + { + MediaType { + source: None, + top: IndexedStr::Concrete(top.into()), + sub: IndexedStr::Concrete(sub.into()), + params: MediaParams::Empty, + } + } + + #[inline] + pub fn with_params(top: T, sub: S, ps: P) -> MediaType + where T: Into>, S: Into>, + K: Into>, V: Into>, + P: IntoIterator + { + let mut params = SmallVec::new(); + for (key, val) in ps { + params.push(( + IndexedStr::Concrete(key.into()), + IndexedStr::Concrete(val.into()) + )) + } + + MediaType { + source: None, + top: IndexedStr::Concrete(top.into()), + sub: IndexedStr::Concrete(sub.into()), + params: MediaParams::Dynamic(params) + } + } + + known_extensions!(from_extension); + + #[inline] + pub fn top(&self) -> &UncasedAsciiRef { + self.top.to_str(self.source.as_ref()).into() + } + + #[inline] + pub fn sub(&self) -> &UncasedAsciiRef { + self.sub.to_str(self.source.as_ref()).into() + } + + #[inline] + pub fn params<'a>(&'a self) -> impl Iterator + 'a { + let param_slice = match self.params { + MediaParams::Static(slice) => slice, + MediaParams::Dynamic(ref vec) => &vec[..], + MediaParams::Empty => &[] + }; + + param_slice.iter() + .map(move |&(ref key, ref val)| { + let source_str = self.source.as_ref(); + (key.to_str(source_str), val.to_str(source_str)) + }) + } + + #[inline(always)] + pub fn into_owned(self) -> MediaType { + MediaType { + source: self.source.map(|c| c.into_owned().into()), + top: self.top, + sub: self.sub, + params: self.params + } + } + + known_media_types!(media_types); +} + +impl FromStr for MediaType { + // Ideally we'd return a `ParseError`, but that required a lifetime. + type Err = String; + + #[inline] + fn from_str(raw: &str) -> Result { + parse_media_type(raw) + .map(|mt| mt.into_owned()) + .map_err(|e| e.to_string()) + } +} + +impl PartialEq for MediaType { + fn eq(&self, other: &MediaType) -> bool { + self.top() == other.top() && self.sub() == other.sub() + } +} + +impl Hash for MediaType { + fn hash(&self, state: &mut H) { + self.top().hash(state); + self.sub().hash(state); + + for (key, val) in self.params() { + key.hash(state); + val.hash(state); + } + } +} + +impl fmt::Display for MediaType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.top(), self.sub())?; + for (key, val) in self.params() { + write!(f, "; {}={}", key, val)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + use super::MediaType; + + macro_rules! assert_no_parse { + ($string:expr) => ({ + let result = MediaType::from_str($string); + if result.is_ok() { + panic!("{:?} parsed unexpectedly.", $string) + } + }); + } + + macro_rules! assert_parse { + ($string:expr) => ({ + let result = MediaType::from_str($string); + match result { + Ok(media_type) => media_type, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + (@full $string:expr, $result:expr, $(($k:expr, $v:expr)),*) => ({ + let result = assert_parse!($string); + assert_eq!(result, $result); + + let result = assert_parse!($string); + assert_eq!(result, $result); + + let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*]; + if expected_params.len() > 0 { + assert_eq!(result.params().count(), expected_params.len()); + let all_params = result.params().zip(expected_params.iter()); + for ((key, val), &(ekey, eval)) in all_params { + assert_eq!(key, ekey); + assert_eq!(val, eval); + } + } + }); + + (from: $string:expr, into: $result:expr) + => (assert_parse_eq!(@full $string, $result, )); + (from: $string:expr, into: $result:expr, params: $(($key:expr, $val:expr)),*) + => (assert_parse_eq!(@full $string, $result, $(($key, $val)),*)); + } + + #[test] + fn check_does_parse() { + assert_parse!("text/html"); + assert_parse!("a/b"); + assert_parse!("*/*"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!(from: "text/html", into: MediaType::HTML); + assert_parse_eq!(from: "text/html; charset=utf-8", into: MediaType::HTML); + assert_parse_eq!(from: "text/html", into: MediaType::new("text", "html")); + + assert_parse_eq!(from: "a/b", into: MediaType::new("a", "b")); + assert_parse_eq!(from: "*/*", into: MediaType::Any); + assert_parse_eq!(from: "application/pdf", into: MediaType::PDF); + assert_parse_eq!(from: "application/json", into: MediaType::JSON); + assert_parse_eq!(from: "image/svg+xml", into: MediaType::SVG); + + assert_parse_eq!(from: "*/json", into: MediaType::new("*", "json")); + assert_parse_eq! { + from: "application/*; param=1", + into: MediaType::new("application", "*") + }; + } + + #[test] + fn check_param_eq() { + assert_parse_eq! { + from: "text/html; a=b; b=c; c=d", + into: MediaType::new("text", "html"), + params: ("a", "b"), ("b", "c"), ("c", "d") + }; + + assert_parse_eq! { + from: "text/html;a=b;b=c; c=d; d=e", + into: MediaType::new("text", "html"), + params: ("a", "b"), ("b", "c"), ("c", "d"), ("d", "e") + }; + + assert_parse_eq! { + from: "text/html; charset=utf-8", + into: MediaType::new("text", "html"), + params: ("charset", "utf-8") + }; + + assert_parse_eq! { + from: "application/*; param=1", + into: MediaType::new("application", "*"), + params: ("param", "1") + }; + + assert_parse_eq! { + from: "*/*;q=0.5;b=c;c=d", + into: MediaType::Any, + params: ("q", "0.5"), ("b", "c"), ("c", "d") + }; + + assert_parse_eq! { + from: "multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq", + into: MediaType::FormData, + params: ("boundary", "----WebKitFormBoundarypRshfItmvaC3aEuq") + }; + + assert_parse_eq! { + from: r#"*/*; a="hello, world!@#$%^&*();;hi""#, + into: MediaType::Any, + params: ("a", "hello, world!@#$%^&*();;hi") + }; + + assert_parse_eq! { + from: r#"application/json; a=";,;""#, + into: MediaType::JSON, + params: ("a", ";,;") + }; + + assert_parse_eq! { + from: r#"application/json; a=";,;"; b=c"#, + into: MediaType::JSON, + params: ("a", ";,;"), ("b", "c") + }; + + assert_parse_eq! { + from: r#"application/json; b=c; a=";.,.;""#, + into: MediaType::JSON, + params: ("b", "c"), ("a", ";.,.;") + }; + + assert_parse_eq! { + from: r#"*/*; a="a"; b="b"; a=a; b=b; c=c"#, + into: MediaType::Any, + params: ("a", "a"), ("b", "b"), ("a", "a"), ("b", "b"), ("c", "c") + }; + } + + #[test] + fn check_params_do_parse() { + assert_parse!("*/*; q=1; q=2"); + assert_parse!("*/*; q=1;q=2;q=3;a=v;c=1;da=1;sdlkldsadasd=uhisdcb89"); + assert_parse!("*/*; q=1; q=2"); + assert_parse!("*/*; q=1; q=2; a=b;c=d; e=f; a=s;a=e"); + assert_parse!("*/*; q=1; q=2 ; a=b"); + assert_parse!("*/*; q=1; q=2; hello=\"world !\""); + } + + #[test] + fn test_bad_parses() { + assert_no_parse!("application//json"); + assert_no_parse!("application///json"); + assert_no_parse!("a/b;"); + assert_no_parse!("*/*; a=b;;"); + assert_no_parse!("*/*; a=b;a"); + assert_no_parse!("*/*; a=b; "); + assert_no_parse!("*/*; a=b;"); + assert_no_parse!("*/*; a = b"); + assert_no_parse!("*/*; a= b"); + assert_no_parse!("*/*; a =b"); + assert_no_parse!(r#"*/*; a="b"#); + assert_no_parse!(r#"*/*; a="b; c=d"#); + assert_no_parse!(r#"*/*; a="b; c=d"#); + } +} diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index c015e328..ee11c776 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -8,12 +8,16 @@ pub mod hyper; pub mod uri; +#[macro_use] +mod known_media_types; mod cookies; mod session; mod method; +mod media_type; mod content_type; mod status; mod header; +mod parse; // We need to export this for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) @@ -24,5 +28,6 @@ pub use self::content_type::ContentType; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; +pub use self::media_type::*; pub use self::cookies::*; pub use self::session::*; diff --git a/lib/src/http/parse/checkers.rs b/lib/src/http/parse/checkers.rs new file mode 100644 index 00000000..26585529 --- /dev/null +++ b/lib/src/http/parse/checkers.rs @@ -0,0 +1,13 @@ +#[inline(always)] +pub fn is_whitespace(byte: char) -> bool { + byte == ' ' || byte == '\t' +} + +#[inline] +pub fn is_valid_token(c: char) -> bool { + match c { + '0'...'9' | 'A'...'Z' | '^'...'~' | '#'...'\'' + | '!' | '*' | '+' | '-' | '.' => true, + _ => false + } +} diff --git a/lib/src/http/parse/indexed_str.rs b/lib/src/http/parse/indexed_str.rs new file mode 100644 index 00000000..892675b7 --- /dev/null +++ b/lib/src/http/parse/indexed_str.rs @@ -0,0 +1,54 @@ +use std::borrow::Cow; + +type Index = u32; + +#[derive(Debug, Clone)] +pub enum IndexedStr { + Indexed(Index, Index), + Concrete(Cow<'static, str>) +} + +impl IndexedStr { + /// Whether this string is derived from indexes or not. + pub fn is_indexed(&self) -> bool { + match *self { + IndexedStr::Indexed(..) => true, + IndexedStr::Concrete(..) => false, + } + } + + /// Retrieves the string `self` corresponds to. If `self` is derived from + /// indexes, the corresponding subslice of `string` is returned. Otherwise, + /// the concrete string is returned. + /// + /// # Panics + /// + /// Panics if `self` is an indexed string and `string` is None. + pub fn to_str<'a>(&'a self, string: Option<&'a Cow>) -> &'a str { + if self.is_indexed() && string.is_none() { + panic!("Cannot convert indexed str to str without base string!") + } + + match *self { + IndexedStr::Indexed(i, j) => &string.unwrap()[(i as usize)..(j as usize)], + IndexedStr::Concrete(ref mstr) => &*mstr, + } + } + + pub fn from(needle: &str, haystack: &str) -> Option { + let haystack_start = haystack.as_ptr() as usize; + let needle_start = needle.as_ptr() as usize; + + if needle_start < haystack_start { + return None; + } + + if (needle_start + needle.len()) > (haystack_start + haystack.len()) { + return None; + } + + let start = needle_start - haystack_start; + let end = start + needle.len(); + Some(IndexedStr::Indexed(start as Index, end as Index)) + } +} diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs new file mode 100644 index 00000000..6f300d36 --- /dev/null +++ b/lib/src/http/parse/media_type.rs @@ -0,0 +1,67 @@ +use std::borrow::Cow; + +use pear::ParseResult; +use pear::parsers::*; +use pear::combinators::*; +use smallvec::SmallVec; + +use http::{MediaType, MediaParams}; +use http::parse::checkers::{is_whitespace, is_valid_token}; +use http::parse::IndexedStr; + +#[parser] +fn quoted_string<'a>(input: &mut &'a str) -> ParseResult<&'a str, &'a str> { + eat('"'); + + let mut is_escaped = false; + let inner = take_while(|c| { + if is_escaped { is_escaped = false; return true; } + if c == '\\' { is_escaped = true; return true; } + c != '"' + }); + + eat('"'); + inner +} + +macro_rules! switch_repeat { + ($input:expr, $($cases:tt)*) => (repeat!($input, switch!($($cases)*))) +} + +#[parser] +fn media_type<'a>(input: &mut &'a str, + source: &'a str) -> ParseResult<&'a str, MediaType> { + let top = take_some_while(|c| is_valid_token(c) && c != '/'); + eat('/'); + let sub = take_some_while(is_valid_token); + + let mut params = SmallVec::new(); + switch_repeat! { + surrounded(|i| eat(i, ';'), is_whitespace) => { + skip_while(is_whitespace); + let key = take_some_while(|c| is_valid_token(c) && c != '='); + eat('='); + + let value = switch! { + peek('"') => quoted_string(), + _ => take_some_while(|c| is_valid_token(c) && c != ';') + }; + + let indexed_key = IndexedStr::from(key, source).expect("key"); + let indexed_val = IndexedStr::from(value, source).expect("val"); + params.push((indexed_key, indexed_val)) + }, + _ => break + } + + MediaType { + source: Some(Cow::Owned(source.to_string())), + top: IndexedStr::from(top, source).expect("top in source"), + sub: IndexedStr::from(sub, source).expect("sub in source"), + params: MediaParams::Dynamic(params) + } +} + +pub fn parse_media_type(mut input: &str) -> ParseResult<&str, MediaType> { + parse!(&mut input, (media_type(input), eof()).0) +} diff --git a/lib/src/http/parse/mod.rs b/lib/src/http/parse/mod.rs new file mode 100644 index 00000000..f1913347 --- /dev/null +++ b/lib/src/http/parse/mod.rs @@ -0,0 +1,6 @@ +mod media_type; +mod indexed_str; +mod checkers; + +pub use self::indexed_str::*; +pub use self::media_type::*; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 77240bca..6ed3917a 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -6,6 +6,9 @@ #![feature(type_ascription)] #![feature(pub_restricted)] #![feature(lookup_host)] +#![feature(plugin)] + +#![plugin(pear_codegen)] //! # Rocket - Core API Documentation //! @@ -95,6 +98,7 @@ //! #[macro_use] extern crate log; +#[macro_use] extern crate pear; extern crate term_painter; extern crate hyper; extern crate url; @@ -105,6 +109,7 @@ extern crate cookie; extern crate time; extern crate memchr; extern crate base64; +extern crate smallvec; #[cfg(test)] #[macro_use] extern crate lazy_static; From 7076ae3c1dde5d4e511a27eebb437c78862a608d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 21 Mar 2017 15:42:11 -0700 Subject: [PATCH 044/297] Move parsing tests to parse module. --- lib/Cargo.toml | 4 +- lib/src/http/ascii.rs | 6 + lib/src/http/media_type.rs | 186 +------------------------------ lib/src/http/parse/media_type.rs | 178 ++++++++++++++++++++++++++++- 4 files changed, 184 insertions(+), 190 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 39489ef7..887faa61 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,8 +29,8 @@ time = "0.1" memchr = "1" base64 = "0.4" smallvec = "0.3" -pear = "0.0.2" -pear_codegen = "0.0.2" +pear = "0.0.3" +pear_codegen = "0.0.3" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/http/ascii.rs b/lib/src/http/ascii.rs index 8f71f8c2..4f25318f 100644 --- a/lib/src/http/ascii.rs +++ b/lib/src/http/ascii.rs @@ -22,6 +22,12 @@ use std::fmt; #[derive(Debug)] pub struct UncasedAsciiRef(str); +impl UncasedAsciiRef { + pub fn as_str(&self) -> &str { + &self.0 + } +} + impl PartialEq for UncasedAsciiRef { #[inline(always)] fn eq(&self, other: &UncasedAsciiRef) -> bool { diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 38fc9985..f31fb9f1 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -148,16 +148,6 @@ impl MediaType { }) } - #[inline(always)] - pub fn into_owned(self) -> MediaType { - MediaType { - source: self.source.map(|c| c.into_owned().into()), - top: self.top, - sub: self.sub, - params: self.params - } - } - known_media_types!(media_types); } @@ -167,9 +157,7 @@ impl FromStr for MediaType { #[inline] fn from_str(raw: &str) -> Result { - parse_media_type(raw) - .map(|mt| mt.into_owned()) - .map_err(|e| e.to_string()) + parse_media_type(raw).map_err(|e| e.to_string()) } } @@ -201,175 +189,3 @@ impl fmt::Display for MediaType { Ok(()) } } - -#[cfg(test)] -mod test { - use std::str::FromStr; - use super::MediaType; - - macro_rules! assert_no_parse { - ($string:expr) => ({ - let result = MediaType::from_str($string); - if result.is_ok() { - panic!("{:?} parsed unexpectedly.", $string) - } - }); - } - - macro_rules! assert_parse { - ($string:expr) => ({ - let result = MediaType::from_str($string); - match result { - Ok(media_type) => media_type, - Err(e) => panic!("{:?} failed to parse: {}", $string, e) - } - }); - } - - macro_rules! assert_parse_eq { - (@full $string:expr, $result:expr, $(($k:expr, $v:expr)),*) => ({ - let result = assert_parse!($string); - assert_eq!(result, $result); - - let result = assert_parse!($string); - assert_eq!(result, $result); - - let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*]; - if expected_params.len() > 0 { - assert_eq!(result.params().count(), expected_params.len()); - let all_params = result.params().zip(expected_params.iter()); - for ((key, val), &(ekey, eval)) in all_params { - assert_eq!(key, ekey); - assert_eq!(val, eval); - } - } - }); - - (from: $string:expr, into: $result:expr) - => (assert_parse_eq!(@full $string, $result, )); - (from: $string:expr, into: $result:expr, params: $(($key:expr, $val:expr)),*) - => (assert_parse_eq!(@full $string, $result, $(($key, $val)),*)); - } - - #[test] - fn check_does_parse() { - assert_parse!("text/html"); - assert_parse!("a/b"); - assert_parse!("*/*"); - } - - #[test] - fn check_parse_eq() { - assert_parse_eq!(from: "text/html", into: MediaType::HTML); - assert_parse_eq!(from: "text/html; charset=utf-8", into: MediaType::HTML); - assert_parse_eq!(from: "text/html", into: MediaType::new("text", "html")); - - assert_parse_eq!(from: "a/b", into: MediaType::new("a", "b")); - assert_parse_eq!(from: "*/*", into: MediaType::Any); - assert_parse_eq!(from: "application/pdf", into: MediaType::PDF); - assert_parse_eq!(from: "application/json", into: MediaType::JSON); - assert_parse_eq!(from: "image/svg+xml", into: MediaType::SVG); - - assert_parse_eq!(from: "*/json", into: MediaType::new("*", "json")); - assert_parse_eq! { - from: "application/*; param=1", - into: MediaType::new("application", "*") - }; - } - - #[test] - fn check_param_eq() { - assert_parse_eq! { - from: "text/html; a=b; b=c; c=d", - into: MediaType::new("text", "html"), - params: ("a", "b"), ("b", "c"), ("c", "d") - }; - - assert_parse_eq! { - from: "text/html;a=b;b=c; c=d; d=e", - into: MediaType::new("text", "html"), - params: ("a", "b"), ("b", "c"), ("c", "d"), ("d", "e") - }; - - assert_parse_eq! { - from: "text/html; charset=utf-8", - into: MediaType::new("text", "html"), - params: ("charset", "utf-8") - }; - - assert_parse_eq! { - from: "application/*; param=1", - into: MediaType::new("application", "*"), - params: ("param", "1") - }; - - assert_parse_eq! { - from: "*/*;q=0.5;b=c;c=d", - into: MediaType::Any, - params: ("q", "0.5"), ("b", "c"), ("c", "d") - }; - - assert_parse_eq! { - from: "multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq", - into: MediaType::FormData, - params: ("boundary", "----WebKitFormBoundarypRshfItmvaC3aEuq") - }; - - assert_parse_eq! { - from: r#"*/*; a="hello, world!@#$%^&*();;hi""#, - into: MediaType::Any, - params: ("a", "hello, world!@#$%^&*();;hi") - }; - - assert_parse_eq! { - from: r#"application/json; a=";,;""#, - into: MediaType::JSON, - params: ("a", ";,;") - }; - - assert_parse_eq! { - from: r#"application/json; a=";,;"; b=c"#, - into: MediaType::JSON, - params: ("a", ";,;"), ("b", "c") - }; - - assert_parse_eq! { - from: r#"application/json; b=c; a=";.,.;""#, - into: MediaType::JSON, - params: ("b", "c"), ("a", ";.,.;") - }; - - assert_parse_eq! { - from: r#"*/*; a="a"; b="b"; a=a; b=b; c=c"#, - into: MediaType::Any, - params: ("a", "a"), ("b", "b"), ("a", "a"), ("b", "b"), ("c", "c") - }; - } - - #[test] - fn check_params_do_parse() { - assert_parse!("*/*; q=1; q=2"); - assert_parse!("*/*; q=1;q=2;q=3;a=v;c=1;da=1;sdlkldsadasd=uhisdcb89"); - assert_parse!("*/*; q=1; q=2"); - assert_parse!("*/*; q=1; q=2; a=b;c=d; e=f; a=s;a=e"); - assert_parse!("*/*; q=1; q=2 ; a=b"); - assert_parse!("*/*; q=1; q=2; hello=\"world !\""); - } - - #[test] - fn test_bad_parses() { - assert_no_parse!("application//json"); - assert_no_parse!("application///json"); - assert_no_parse!("a/b;"); - assert_no_parse!("*/*; a=b;;"); - assert_no_parse!("*/*; a=b;a"); - assert_no_parse!("*/*; a=b; "); - assert_no_parse!("*/*; a=b;"); - assert_no_parse!("*/*; a = b"); - assert_no_parse!("*/*; a= b"); - assert_no_parse!("*/*; a =b"); - assert_no_parse!(r#"*/*; a="b"#); - assert_no_parse!(r#"*/*; a="b; c=d"#); - assert_no_parse!(r#"*/*; a="b; c=d"#); - } -} diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index 6f300d36..7d96aa33 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use pear::ParseResult; +use pear::{ParseError, ParseResult}; use pear::parsers::*; use pear::combinators::*; use smallvec::SmallVec; @@ -62,6 +62,178 @@ fn media_type<'a>(input: &mut &'a str, } } -pub fn parse_media_type(mut input: &str) -> ParseResult<&str, MediaType> { - parse!(&mut input, (media_type(input), eof()).0) +pub fn parse_media_type(mut input: &str) -> Result> { + parse!(&mut input, (media_type(input), eof()).0).into() +} + +#[cfg(test)] +mod test { + use http::MediaType; + use super::parse_media_type; + + macro_rules! assert_no_parse { + ($string:expr) => ({ + let result: Result<_, _> = parse_media_type($string).into(); + if result.is_ok() { + panic!("{:?} parsed unexpectedly.", $string) + } + }); + } + + macro_rules! assert_parse { + ($string:expr) => ({ + let result: Result<_, _> = parse_media_type($string).into(); + match result { + Ok(media_type) => media_type, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + (@full $string:expr, $result:expr, $(($k:expr, $v:expr)),*) => ({ + let result = assert_parse!($string); + assert_eq!(result, $result); + + let result = assert_parse!($string); + assert_eq!(result, $result); + + let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*]; + if expected_params.len() > 0 { + assert_eq!(result.params().count(), expected_params.len()); + let all_params = result.params().zip(expected_params.iter()); + for ((key, val), &(ekey, eval)) in all_params { + assert_eq!(key, ekey); + assert_eq!(val, eval); + } + } + }); + + (from: $string:expr, into: $result:expr) + => (assert_parse_eq!(@full $string, $result, )); + (from: $string:expr, into: $result:expr, params: $(($key:expr, $val:expr)),*) + => (assert_parse_eq!(@full $string, $result, $(($key, $val)),*)); + } + + #[test] + fn check_does_parse() { + assert_parse!("text/html"); + assert_parse!("a/b"); + assert_parse!("*/*"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!(from: "text/html", into: MediaType::HTML); + assert_parse_eq!(from: "text/html; charset=utf-8", into: MediaType::HTML); + assert_parse_eq!(from: "text/html", into: MediaType::HTML); + + assert_parse_eq!(from: "a/b", into: MediaType::new("a", "b")); + assert_parse_eq!(from: "*/*", into: MediaType::Any); + assert_parse_eq!(from: "application/pdf", into: MediaType::PDF); + assert_parse_eq!(from: "application/json", into: MediaType::JSON); + assert_parse_eq!(from: "image/svg+xml", into: MediaType::SVG); + + assert_parse_eq!(from: "*/json", into: MediaType::new("*", "json")); + assert_parse_eq! { + from: "application/*; param=1", + into: MediaType::new("application", "*") + }; + } + + #[test] + fn check_param_eq() { + assert_parse_eq! { + from: "text/html; a=b; b=c; c=d", + into: MediaType::new("text", "html"), + params: ("a", "b"), ("b", "c"), ("c", "d") + }; + + assert_parse_eq! { + from: "text/html;a=b;b=c; c=d; d=e", + into: MediaType::new("text", "html"), + params: ("a", "b"), ("b", "c"), ("c", "d"), ("d", "e") + }; + + assert_parse_eq! { + from: "text/html; charset=utf-8", + into: MediaType::new("text", "html"), + params: ("charset", "utf-8") + }; + + assert_parse_eq! { + from: "application/*; param=1", + into: MediaType::new("application", "*"), + params: ("param", "1") + }; + + assert_parse_eq! { + from: "*/*;q=0.5;b=c;c=d", + into: MediaType::Any, + params: ("q", "0.5"), ("b", "c"), ("c", "d") + }; + + assert_parse_eq! { + from: "multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq", + into: MediaType::FormData, + params: ("boundary", "----WebKitFormBoundarypRshfItmvaC3aEuq") + }; + + assert_parse_eq! { + from: r#"*/*; a="hello, world!@#$%^&*();;hi""#, + into: MediaType::Any, + params: ("a", "hello, world!@#$%^&*();;hi") + }; + + assert_parse_eq! { + from: r#"application/json; a=";,;""#, + into: MediaType::JSON, + params: ("a", ";,;") + }; + + assert_parse_eq! { + from: r#"application/json; a=";,;"; b=c"#, + into: MediaType::JSON, + params: ("a", ";,;"), ("b", "c") + }; + + assert_parse_eq! { + from: r#"application/json; b=c; a=";.,.;""#, + into: MediaType::JSON, + params: ("b", "c"), ("a", ";.,.;") + }; + + assert_parse_eq! { + from: r#"*/*; a="a"; b="b"; a=a; b=b; c=c"#, + into: MediaType::Any, + params: ("a", "a"), ("b", "b"), ("a", "a"), ("b", "b"), ("c", "c") + }; + } + + #[test] + fn check_params_do_parse() { + assert_parse!("*/*; q=1; q=2"); + assert_parse!("*/*; q=1;q=2;q=3;a=v;c=1;da=1;sdlkldsadasd=uhisdcb89"); + assert_parse!("*/*; q=1; q=2"); + assert_parse!("*/*; q=1; q=2; a=b;c=d; e=f; a=s;a=e"); + assert_parse!("*/*; q=1; q=2 ; a=b"); + assert_parse!("*/*; q=1; q=2; hello=\"world !\""); + } + + #[test] + fn test_bad_parses() { + assert_no_parse!("application//json"); + assert_no_parse!("application///json"); + assert_no_parse!("a/b;"); + assert_no_parse!("*/*; a=b;;"); + assert_no_parse!("*/*; a=b;a"); + assert_no_parse!("*/*; a=b; "); + assert_no_parse!("*/*; a=b;"); + assert_no_parse!("*/*; a = b"); + assert_no_parse!("*/*; a= b"); + assert_no_parse!("*/*; a =b"); + assert_no_parse!(r#"*/*; a="b"#); + assert_no_parse!(r#"*/*; a="b; c=d"#); + assert_no_parse!(r#"*/*; a="b; c=d"#); + } } From ae201b1577972f507adc55b7287a4f5506b8a639 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 21 Mar 2017 20:34:01 -0700 Subject: [PATCH 045/297] Remove pub_restricted feature: it's stable! --- lib/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6ed3917a..c0b9f9c9 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -4,7 +4,6 @@ #![feature(associated_consts)] #![feature(const_fn)] #![feature(type_ascription)] -#![feature(pub_restricted)] #![feature(lookup_host)] #![feature(plugin)] From 22062d1bbe1510ad0f4d06c6972aef845c60653f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 22 Mar 2017 18:57:14 -0700 Subject: [PATCH 046/297] Remove spurious new line. --- lib/src/ext.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/ext.rs b/lib/src/ext.rs index 9fab8c8c..8813b741 100644 --- a/lib/src/ext.rs +++ b/lib/src/ext.rs @@ -17,4 +17,3 @@ pub trait ReadExt: io::Read { } impl ReadExt for T { } - From bf67a32cd91aac9bd0ad1156b34569e657e3a162 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 22 Mar 2017 18:57:46 -0700 Subject: [PATCH 047/297] Update minimum rustc version for codegen. --- codegen/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen/build.rs b/codegen/build.rs index 703af08b..b94bd736 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -8,7 +8,7 @@ use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. -const MIN_DATE: &'static str = "2017-03-19"; +const MIN_DATE: &'static str = "2017-03-22"; const MIN_VERSION: &'static str = "1.17.0-nightly"; // Convenience macro for writing to stderr. From 09550b6e7c27fcbccacd19c71f54dabb9c610687 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 22 Mar 2017 19:01:58 -0700 Subject: [PATCH 048/297] New version: 0.2.3. --- CHANGELOG.md | 21 +++++++++++++++++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5373f8d..6c3bce7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# Version 0.2.3 (Mar 22, 2017) + +## Fixes + + * Multiple header values for the same header name are now properly preserved + (#223). + +## Core + + * The `get_slice` and `get_table` methods were added to `Config`. + * The `pub_restricted` feature has been stabilized! + +## Codegen + + * Lints were updated for `2017-03-20` nightly. + * Minimum required `rustc` is `1.17.0-nightly (2017-03-22)`. + +## Infrastructure + + * The test script now denies trailing whitespace. + # Version 0.2.2 (Feb 26, 2017) ## Codegen diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 6f97d944..19b01d24 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.2" +version = "0.2.3" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.2", path = "../lib/" } +rocket = { version = "0.2.3", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 849e8ecf..1524ed58 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.2" +version = "0.2.3" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -22,7 +22,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.2", path = "../lib/" } +rocket = { version = "0.2.3", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 887faa61..8e44848d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.2" +version = "0.2.3" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -38,7 +38,7 @@ features = ["percent-encode", "secure"] [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.2", path = "../codegen" } +rocket_codegen = { version = "0.2.3", path = "../codegen" } [build-dependencies] ansi_term = "0.9" From e006f3f83efa826b3f451bd3515c83f07deef2c5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 23 Mar 2017 03:28:53 -0700 Subject: [PATCH 049/297] Spruce up MediaType docs. Use new Pear features in MediaType parser. --- lib/Cargo.toml | 4 +-- lib/src/http/media_type.rs | 42 +++++++++++++++++++++++++++++--- lib/src/http/parse/media_type.rs | 5 ---- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8e44848d..0482b748 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,8 +29,8 @@ time = "0.1" memchr = "1" base64 = "0.4" smallvec = "0.3" -pear = "0.0.3" -pear_codegen = "0.0.3" +pear = "0.0.5" +pear_codegen = "0.0.5" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index f31fb9f1..434c71ac 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -49,9 +49,10 @@ macro_rules! media_types { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*),+) => { $( - #[doc="[MediaType](struct.MediaType.html) for "] - #[doc=$str] - #[doc=": "] #[doc=$t] #[doc="/"] #[doc=$s] #[doc=""] + #[doc="Media type for "] #[doc=$str] #[doc=": "] + #[doc=$t] #[doc="/"] #[doc=$s] + $(#[doc="; "] #[doc=$k] #[doc=" = "] #[doc=$v])* + #[doc=""] #[allow(non_upper_case_globals)] pub const $name: MediaType = MediaType { source: None, @@ -60,6 +61,10 @@ macro_rules! media_types { params: MediaParams::Static(&[$((media_str!($k), media_str!($v))),*]) }; + #[doc="Returns `true` if `self` is the media type for "] + #[doc=$str] + #[doc=", "] + /// without considering parameters. #[inline(always)] pub fn $check(&self) -> bool { *self == MediaType::$name @@ -77,6 +82,32 @@ macro_rules! media_types { macro_rules! from_extension { ($($ext:expr => $name:ident),*) => ( + /// Returns the Media-Type associated with the extension `ext`. Not all + /// extensions are recognized. If an extensions is not recognized, then this + /// method returns a ContentType of `Any`. The currently recognized + /// extensions include + $(#[doc=$ext]#[doc=","])* + /// and is likely to grow. + /// + /// # Example + /// + /// A recognized content type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let xml = ContentType::from_extension("xml"); + /// assert!(xml.is_xml()); + /// ``` + /// + /// An unrecognized content type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let foo = ContentType::from_extension("foo"); + /// assert!(foo.is_any()); + /// ``` pub fn from_extension(ext: &str) -> Option { match ext { $(x if uncased_eq(x, $ext) => Some(MediaType::$name)),*, @@ -152,7 +183,7 @@ impl MediaType { } impl FromStr for MediaType { - // Ideally we'd return a `ParseError`, but that required a lifetime. + // Ideally we'd return a `ParseError`, but that requires a lifetime. type Err = String; #[inline] @@ -162,12 +193,14 @@ impl FromStr for MediaType { } impl PartialEq for MediaType { + #[inline(always)] fn eq(&self, other: &MediaType) -> bool { self.top() == other.top() && self.sub() == other.sub() } } impl Hash for MediaType { + #[inline] fn hash(&self, state: &mut H) { self.top().hash(state); self.sub().hash(state); @@ -180,6 +213,7 @@ impl Hash for MediaType { } impl fmt::Display for MediaType { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}/{}", self.top(), self.sub())?; for (key, val) in self.params() { diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index 7d96aa33..f29eb076 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -24,10 +24,6 @@ fn quoted_string<'a>(input: &mut &'a str) -> ParseResult<&'a str, &'a str> { inner } -macro_rules! switch_repeat { - ($input:expr, $($cases:tt)*) => (repeat!($input, switch!($($cases)*))) -} - #[parser] fn media_type<'a>(input: &mut &'a str, source: &'a str) -> ParseResult<&'a str, MediaType> { @@ -38,7 +34,6 @@ fn media_type<'a>(input: &mut &'a str, let mut params = SmallVec::new(); switch_repeat! { surrounded(|i| eat(i, ';'), is_whitespace) => { - skip_while(is_whitespace); let key = take_some_while(|c| is_valid_token(c) && c != '='); eat('='); From d2c49e02c3d5d3f5736fdab89c0754c0e14962d5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 23 Mar 2017 03:53:12 -0700 Subject: [PATCH 050/297] Fix 'Response::join' docs. --- lib/src/response/response.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 2302ebaf..68031529 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -1057,10 +1057,10 @@ impl<'r> Response<'r> { } } - // Sets `self`'s status and body to that of `other` if they are not already - // set in `self`. Any headers present in both `other` and `self` are - // adjoined. - // + /// Sets `self`'s status and body to that of `other` if they are not already + /// set in `self`. Any headers present in both `other` and `self` are + /// adjoined. + /// /// # Example /// /// ```rust From 605e8fdc0efbcd76ef2ea4d6d9599f5e97024867 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 23 Mar 2017 15:28:49 -0700 Subject: [PATCH 051/297] Update Tera to 0.8. --- contrib/Cargo.toml | 2 +- contrib/src/templates/tera_templates.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 1524ed58..d1becfc1 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -37,4 +37,4 @@ rmp-serde = { version = "^0.12", optional = true } handlebars = { version = "^0.25", optional = true, features = ["serde_type"] } glob = { version = "^0.2", optional = true } lazy_static = { version = "^0.2", optional = true } -tera = { version = "^0.7", optional = true } +tera = { version = "^0.8", optional = true } diff --git a/contrib/src/templates/tera_templates.rs b/contrib/src/templates/tera_templates.rs index 2e7e440d..581f1ae8 100644 --- a/contrib/src/templates/tera_templates.rs +++ b/contrib/src/templates/tera_templates.rs @@ -53,7 +53,7 @@ pub fn render(name: &str, _: &TemplateInfo, context: &T) -> Option return None; }; - match tera.value_render(name, context) { + match tera.render(name, context) { Ok(string) => Some(string), Err(e) => { error_!("Error rendering Tera template '{}'.", name); From 13359d4f508f1952e26f939917ee52d144788084 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 23 Mar 2017 22:41:42 -0700 Subject: [PATCH 052/297] Reformulate ContentType as a wrapper around MediaType. --- codegen/src/decorators/route.rs | 21 +- contrib/src/msgpack.rs | 53 ++-- lib/src/http/content_type.rs | 439 +++++++----------------------- lib/src/http/known_media_types.rs | 1 + lib/src/http/media_type.rs | 92 ++++++- lib/src/http/mod.rs | 4 +- lib/src/http/parse/media_type.rs | 13 + lib/src/router/collider.rs | 2 +- 8 files changed, 244 insertions(+), 381 deletions(-) diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index c147afbd..cb4d909b 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -24,16 +24,19 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { fn content_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { ct.map(|ct| { - let (top, sub) = (ct.ttype.as_str(), ct.subtype.as_str()); - quote_expr!(ecx, ::rocket::http::ContentType { - ttype: ::rocket::http::ascii::UncasedAscii { - string: ::std::borrow::Cow::Borrowed($top) + let (top, sub) = (ct.top().as_str(), ct.sub().as_str()); + quote_expr!(ecx, ::rocket::http::ContentType( + ::rocket::http::MediaType { + source: None, + top: ::rocket::http::IndexedStr::Concrete( + ::std::borrow::Cow::Borrowed($top) + ), + sub: ::rocket::http::IndexedStr::Concrete( + ::std::borrow::Cow::Borrowed($sub) + ), + params: ::rocket::http::MediaParams::Static(&[]) }, - subtype: ::rocket::http::ascii::UncasedAscii { - string: ::std::borrow::Cow::Borrowed($sub) - }, - params: None - }) + )) }) } diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index acf47520..f33ede0c 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -13,14 +13,14 @@ use serde::{Serialize, Deserialize}; pub use self::rmp_serde::decode::Error as MsgPackError; -/// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to easily -/// consume and respond with MessagePack data. +/// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to +/// easily consume and respond with MessagePack data. /// -/// If you're receiving MessagePack data, simply add a `data` parameter to your route -/// arguments and ensure the type of the parameter is a `MsgPack`, where `T` is -/// some type you'd like to parse from MessagePack. `T` must implement `Deserialize` -/// from [Serde](https://github.com/serde-rs/serde). The data is parsed from the -/// HTTP request body. +/// If you're receiving MessagePack data, simply add a `data` parameter to your +/// route arguments and ensure the type of the parameter is a `MsgPack`, +/// where `T` is some type you'd like to parse from MessagePack. `T` must +/// implement `Deserialize` from [Serde](https://github.com/serde-rs/serde). The +/// data is parsed from the HTTP request body. /// /// ```rust,ignore /// #[post("/users/", format = "application/msgpack", data = "")] @@ -29,16 +29,18 @@ pub use self::rmp_serde::decode::Error as MsgPackError; /// } /// ``` /// -/// You don't _need_ to use `format = "application/msgpack"`, but it _may_ be what -/// you want. Using `format = application/msgpack` means that any request that -/// doesn't specify "application/msgpack" as its first `Content-Type:` header -/// parameter will not be routed to this handler. By default, Rocket will accept a -/// Content Type of any of the following for MessagePack data: -/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or `bin/x-msgpack`. +/// You don't _need_ to use `format = "application/msgpack"`, but it _may_ be +/// what you want. Using `format = application/msgpack` means that any request +/// that doesn't specify "application/msgpack" as its first `Content-Type:` +/// header parameter will not be routed to this handler. By default, Rocket will +/// accept a Content Type of any of the following for MessagePack data: +/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or +/// `bin/x-msgpack`. /// -/// If you're responding with MessagePack data, return a `MsgPack` type, where `T` -/// implements `Serialize` from [Serde](https://github.com/serde-rs/serde). The -/// content type of the response is set to `application/msgpack` automatically. +/// If you're responding with MessagePack data, return a `MsgPack` type, +/// where `T` implements `Serialize` from +/// [Serde](https://github.com/serde-rs/serde). The content type of the response +/// is set to `application/msgpack` automatically. /// /// ```rust,ignore /// #[get("/users/")] @@ -55,12 +57,14 @@ impl MsgPack { /// Consumes the `MsgPack` wrapper and returns the wrapped item. /// /// # Example + /// /// ```rust /// # use rocket_contrib::MsgPack; /// let string = "Hello".to_string(); /// let my_msgpack = MsgPack(string); /// assert_eq!(my_msgpack.into_inner(), "Hello".to_string()); /// ``` + #[inline(always)] pub fn into_inner(self) -> T { self.0 } @@ -70,11 +74,12 @@ impl MsgPack { /// TODO: Determine this size from some configuration parameter. const MAX_SIZE: u64 = 1048576; -/// Accepted content types are: -/// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, and `bin/x-msgpack` +/// Accepted content types are: `application/msgpack`, `application/x-msgpack`, +/// `bin/msgpack`, and `bin/x-msgpack`. +#[inline(always)] fn is_msgpack_content_type(ct: &ContentType) -> bool { - (ct.ttype == "application" || ct.ttype == "bin") - && (ct.subtype == "msgpack" || ct.subtype == "x-msgpack") + (ct.top() == "application" || ct.top() == "bin") + && (ct.sub() == "msgpack" || ct.sub() == "x-msgpack") } impl FromData for MsgPack { @@ -103,9 +108,9 @@ impl FromData for MsgPack { } } -/// Serializes the wrapped value into MessagePack. Returns a response with Content-Type -/// MessagePack and a fixed-size body with the serialization. If serialization fails, an -/// `Err` of `Status::InternalServerError` is returned. +/// Serializes the wrapped value into MessagePack. Returns a response with +/// Content-Type `MsgPack` and a fixed-size body with the serialization. If +/// serialization fails, an `Err` of `Status::InternalServerError` is returned. impl Responder<'static> for MsgPack { fn respond(self) -> response::Result<'static> { rmp_serde::to_vec(&self.0).map_err(|e| { @@ -122,12 +127,14 @@ impl Responder<'static> for MsgPack { impl Deref for MsgPack { type Target = T; + #[inline(always)] fn deref<'a>(&'a self) -> &'a T { &self.0 } } impl DerefMut for MsgPack { + #[inline(always)] fn deref_mut<'a>(&'a mut self) -> &'a mut T { &mut self.0 } diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 7ec8e46c..8c00ab80 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -1,10 +1,10 @@ use std::borrow::{Borrow, Cow}; +use std::ops::Deref; use std::str::FromStr; use std::fmt; -use http::Header; +use http::{Header, MediaType}; use http::hyper::mime::Mime; -use http::ascii::{uncased_eq, UncasedAscii}; /// Representation of HTTP Content-Types. /// @@ -39,89 +39,59 @@ use http::ascii::{uncased_eq, UncasedAscii}; /// let response = Response::build().header(ContentType::HTML).finalize(); /// ``` #[derive(Debug, Clone, PartialEq, Hash)] -pub struct ContentType { - /// The "type" component of the Content-Type. - pub ttype: UncasedAscii<'static>, - /// The "subtype" component of the Content-Type. - pub subtype: UncasedAscii<'static>, - /// Semicolon-seperated parameters associated with the Content-Type. - pub params: Option> -} +pub struct ContentType(pub MediaType); -macro_rules! ctr_params { - () => (None); - ($param:expr) => (Some(UncasedAscii { string: Cow::Borrowed($param) })); -} - -macro_rules! ctrs { - ($($str:expr, $name:ident, $check_name:ident => - $top:expr, $sub:expr $(; $param:expr),*),+) => { +macro_rules! content_types { + ($($name:ident ($check:ident): $str:expr, $t:expr, + $s:expr $(; $k:expr => $v:expr)*),+) => { $( - #[doc="[ContentType](struct.ContentType.html) for "] - #[doc=$str] - #[doc=": "] - #[doc=$top] - #[doc="/"] - #[doc=$sub] - $(#[doc="; "] #[doc=$param])* + #[doc="Media type for "] #[doc=$str] #[doc=": "] + #[doc=$t] #[doc="/"] #[doc=$s] + $(#[doc="; "] #[doc=$k] #[doc=" = "] #[doc=$v])* #[doc=""] #[allow(non_upper_case_globals)] - pub const $name: ContentType = ContentType { - ttype: UncasedAscii { string: Cow::Borrowed($top) }, - subtype: UncasedAscii { string: Cow::Borrowed($sub) }, - params: ctr_params!($($param)*) - }; - )+ + pub const $name: ContentType = ContentType(MediaType::$name); - /// Returns `true` if this ContentType is known to Rocket, that is, - /// there is an associated constant for `self`. - pub fn is_known(&self) -> bool { - $(if self.$check_name() { return true })+ - false - } - - $( - #[doc="Returns `true` if `self` is a "] + #[doc="Returns `true` if `self` is the media type for "] #[doc=$str] - #[doc=" ContentType: "] - #[doc=$top] - #[doc="/"] - #[doc=$sub] - #[doc="."] - /// - /// Paramaters are not taken into account when doing this check. + #[doc=", "] + /// without considering parameters. #[inline(always)] - pub fn $check_name(&self) -> bool { - self.ttype == $top && self.subtype == $sub + pub fn $check(&self) -> bool { + *self == ContentType::$name } )+ + + /// Returns `true` if this `ContentType` is known to Rocket, that is, + /// there is an associated constant for `self`. + pub fn is_known(&self) -> bool { + $(if self.$check() { return true })+ + false + } }; } impl ContentType { - ctrs! { - "any", Any, is_any => "*", "*", - "HTML", HTML, is_html => "text", "html" ; "charset=utf-8", - "Plain", Plain, is_plain => "text", "plain" ; "charset=utf-8", - "JSON", JSON, is_json => "application", "json", - "MsgPack", MsgPack, is_msgpack => "application", "msgpack", - "form", Form, is_form => "application", "x-www-form-urlencoded", - "JavaScript", JavaScript, is_javascript => "application", "javascript", - "CSS", CSS, is_css => "text", "css" ; "charset=utf-8", - "data form", DataForm, is_data_form => "multipart", "form-data", - "XML", XML, is_xml => "text", "xml" ; "charset=utf-8", - "CSV", CSV, is_csv => "text", "csv" ; "charset=utf-8", - "PNG", PNG, is_png => "image", "png", - "GIF", GIF, is_gif => "image", "gif", - "BMP", BMP, is_bmp => "image", "bmp", - "JPEG", JPEG, is_jpeg => "image", "jpeg", - "WEBP", WEBP, is_webp => "image", "webp", - "SVG", SVG, is_svg => "image", "svg+xml", - "PDF", PDF, is_pdf => "application", "pdf", - "TTF", TTF, is_ttf => "application", "font-sfnt", - "OTF", OTF, is_otf => "application", "font-sfnt", - "WOFF", WOFF, is_woff => "application", "font-woff", - "WOFF2", WOFF2, is_woff2 => "font", "woff2" + /// Creates a new `ContentType` with top-level type `top` and subtype `sub`. + /// This should _only_ be used to construct uncommon or custom content + /// types. Use an associated constant for everything else. + /// + /// # Example + /// + /// Create a custom `application/x-person` content type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let custom = ContentType::new("application", "x-person"); + /// assert_eq!(custom.top(), "application"); + /// assert_eq!(custom.sub(), "x-person"); + /// ``` + #[inline(always)] + pub fn new(top: T, sub: S) -> ContentType + where T: Into>, S: Into> + { + ContentType(MediaType::new(top, sub)) } /// Returns the Content-Type associated with the extension `ext`. Not all @@ -150,121 +120,45 @@ impl ContentType { /// assert!(foo.is_any()); /// ``` pub fn from_extension(ext: &str) -> ContentType { - match ext { - x if uncased_eq(x, "txt") => ContentType::Plain, - x if uncased_eq(x, "html") => ContentType::HTML, - x if uncased_eq(x, "htm") => ContentType::HTML, - x if uncased_eq(x, "xml") => ContentType::XML, - x if uncased_eq(x, "csv") => ContentType::CSV, - x if uncased_eq(x, "js") => ContentType::JavaScript, - x if uncased_eq(x, "css") => ContentType::CSS, - x if uncased_eq(x, "json") => ContentType::JSON, - x if uncased_eq(x, "png") => ContentType::PNG, - x if uncased_eq(x, "gif") => ContentType::GIF, - x if uncased_eq(x, "bmp") => ContentType::BMP, - x if uncased_eq(x, "jpeg") => ContentType::JPEG, - x if uncased_eq(x, "jpg") => ContentType::JPEG, - x if uncased_eq(x, "webp") => ContentType::WEBP, - x if uncased_eq(x, "svg") => ContentType::SVG, - x if uncased_eq(x, "pdf") => ContentType::PDF, - x if uncased_eq(x, "ttf") => ContentType::TTF, - x if uncased_eq(x, "otf") => ContentType::OTF, - x if uncased_eq(x, "woff") => ContentType::WOFF, - x if uncased_eq(x, "woff2") => ContentType::WOFF2, - _ => ContentType::Any - } + MediaType::from_extension(ext) + .map(|mt| ContentType(mt)) + .unwrap_or(ContentType::Any) } - /// Creates a new `ContentType` with type `ttype` and subtype `subtype`. - /// This should _only_ be used to construct uncommon Content-Types or custom - /// Content-Types. Use an associated constant for common Content-Types. + /// Creates a new `ContentType` with top-level type `top`, subtype `sub`, + /// and parameters `ps`. This should _only_ be used to construct uncommon or + /// custom content types. Use an associated constant for everything else. /// /// # Example /// - /// Create a custom `application/x-person` Content-Type: + /// Create a custom `application/x-id; id=1` content type: /// /// ```rust /// use rocket::http::ContentType; /// - /// let custom = ContentType::new("application", "x-person"); - /// assert_eq!(custom.to_string(), "application/x-person".to_string()); - /// ``` - #[inline(always)] - pub fn new(ttype: T, subtype: S) -> ContentType - where T: Into>, S: Into> - { - ContentType::with_params::(ttype, subtype, None) - } - - /// Creates a new `ContentType` with type `ttype`, subtype `subtype`, and - /// optionally parameters `params`, a semicolon-seperated list of - /// parameters. This should be _only_ to construct uncommon Content-Types or - /// custom Content-Types. Use an associated constant for common - /// Content-Types. - /// - /// # Example - /// - /// Create a custom `application/x-id; id=1` Content-Type: - /// - /// ```rust - /// use rocket::http::ContentType; - /// - /// let id = ContentType::with_params("application", "x-id", Some("id=1")); + /// let id = ContentType::with_params("application", "x-id", Some(("id", "1"))); /// assert_eq!(id.to_string(), "application/x-id; id=1".to_string()); /// ``` - #[inline(always)] - pub fn with_params(ttype: T, subtype: S, params: Option

) -> ContentType - where T: Into>, - S: Into>, - P: Into> + /// + /// Create a custom `text/person; name=bob; weight=175` content type: + /// + /// ```rust + /// use rocket::http::ContentType; + /// + /// let params = vec![("name", "bob"), ("ref", "2382")]; + /// let mt = ContentType::with_params("text", "person", params); + /// assert_eq!(mt.to_string(), "text/person; name=bob; ref=2382".to_string()); + /// ``` + #[inline] + pub fn with_params(top: T, sub: S, ps: P) -> ContentType + where T: Into>, S: Into>, + K: Into>, V: Into>, + P: IntoIterator { - ContentType { - ttype: UncasedAscii::from(ttype), - subtype: UncasedAscii::from(subtype), - params: params.map(UncasedAscii::from) - } + ContentType(MediaType::with_params(top, sub, ps)) } - /// Returns an iterator over the (key, value) pairs of the Content-Type's - /// parameter list. The iterator will be empty if the Content-Type has no - /// parameters. - /// - /// # Example - /// - /// The `ContentType::Plain` type has one parameter: `charset=utf-8`: - /// - /// ```rust - /// use rocket::http::ContentType; - /// - /// let plain = ContentType::Plain; - /// let plain_params: Vec<_> = plain.params().collect(); - /// assert_eq!(plain_params, vec![("charset", "utf-8")]); - /// ``` - /// - /// The `ContentType::PNG` type has no parameters: - /// - /// ```rust - /// use rocket::http::ContentType; - /// - /// let png = ContentType::PNG; - /// assert_eq!(png.params().count(), 0); - /// ``` - #[inline(always)] - pub fn params<'a>(&'a self) -> impl Iterator + 'a { - let params = match self.params { - Some(ref params) => params.as_str(), - None => "" - }; - - params.split(';') - .filter_map(|param| { - let mut kv = param.split('='); - match (kv.next(), kv.next()) { - (Some(key), Some(val)) => Some((key.trim(), val.trim())), - _ => None - } - }) - } + known_media_types!(content_types); } impl Default for ContentType { @@ -275,8 +169,18 @@ impl Default for ContentType { } } +impl Deref for ContentType { + type Target = MediaType; + + #[inline(always)] + fn deref(&self) -> &MediaType { + &self.0 + } +} + #[doc(hidden)] impl> From for ContentType { + #[inline(always)] default fn from(mime: T) -> ContentType { let mime: Mime = mime.borrow().clone(); ContentType::from(mime) @@ -285,37 +189,21 @@ impl> From for ContentType { #[doc(hidden)] impl From for ContentType { + #[inline] fn from(mime: Mime) -> ContentType { - let params = match mime.2.len() { - 0 => None, - _ => { - Some(mime.2.into_iter() - .map(|(attr, value)| format!("{}={}", attr, value)) - .collect::>() - .join("; ")) - } - }; + // soooo inneficient. + let params = mime.2.into_iter() + .map(|(attr, value)| (attr.to_string(), value.to_string())) + .collect::>(); ContentType::with_params(mime.0.to_string(), mime.1.to_string(), params) } } -fn is_valid_char(c: char) -> bool { - match c { - '0'...'9' | 'A'...'Z' | '^'...'~' | '#'...'\'' - | '!' | '*' | '+' | '-' | '.' => true, - _ => false - } -} - -fn is_valid_token(string: &str) -> bool { - string.len() >= 1 && string.chars().all(is_valid_char) -} - impl FromStr for ContentType { - type Err = &'static str; + type Err = String; - /// Parses a ContentType from a given Content-Type header value. + /// Parses a `ContentType` from a given Content-Type header value. /// /// # Examples /// @@ -330,7 +218,7 @@ impl FromStr for ContentType { /// assert_eq!(json, ContentType::JSON); /// ``` /// - /// Parsing a content-type extension: + /// Parsing a content type extension: /// /// ```rust /// use std::str::FromStr; @@ -338,8 +226,8 @@ impl FromStr for ContentType { /// /// let custom = ContentType::from_str("application/x-custom").unwrap(); /// assert!(!custom.is_known()); - /// assert_eq!(custom.ttype, "application"); - /// assert_eq!(custom.subtype, "x-custom"); + /// assert_eq!(custom.top(), "application"); + /// assert_eq!(custom.sub(), "x-custom"); /// ``` /// /// Parsing an invalid Content-Type value: @@ -351,52 +239,9 @@ impl FromStr for ContentType { /// let custom = ContentType::from_str("application//x-custom"); /// assert!(custom.is_err()); /// ``` - fn from_str(raw: &str) -> Result { - let slash = match raw.find('/') { - Some(i) => i, - None => return Err("Missing / in MIME type."), - }; - - let top_s = &raw[..slash]; - let (sub_s, params) = match raw.find(';') { - Some(j) => (raw[(slash + 1)..j].trim_right(), Some(raw[(j + 1)..].trim_left())), - None => (&raw[(slash + 1)..], None), - }; - - if top_s.len() < 1 || sub_s.len() < 1 { - return Err("Empty string."); - } - - if !is_valid_token(top_s) || !is_valid_token(sub_s) { - return Err("Invalid characters in type or subtype."); - } - - let mut trimmed_params = vec![]; - for param in params.into_iter().flat_map(|p| p.split(';')) { - let param = param.trim_left(); - for (i, split) in param.split('=').enumerate() { - if split.trim() != split { - return Err("Whitespace not allowed around = character."); - } - - match i { - 0 => if !is_valid_token(split) { - return Err("Invalid parameter name."); - }, - 1 => if !((split.starts_with('"') && split.ends_with('"')) - || is_valid_token(split)) { - return Err("Invalid parameter value."); - }, - _ => return Err("Malformed parameter.") - } - } - - trimmed_params.push(param); - } - - let (ttype, subtype) = (top_s.to_string(), sub_s.to_string()); - let params = params.map(|_| trimmed_params.join(";")); - Ok(ContentType::with_params(ttype, subtype, params)) + #[inline(always)] + fn from_str(raw: &str) -> Result { + MediaType::from_str(raw).map(|mt| ContentType(mt)) } } @@ -411,115 +256,17 @@ impl fmt::Display for ContentType { /// let ct = format!("{}", ContentType::JSON); /// assert_eq!(ct, "application/json"); /// ``` + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}/{}", self.ttype, self.subtype)?; - - if let Some(ref params) = self.params { - write!(f, "; {}", params)?; - } - - Ok(()) + write!(f, "{}", self.0) } } /// Creates a new `Header` with name `Content-Type` and the value set to the /// HTTP rendering of this Content-Type. impl Into> for ContentType { - #[inline] + #[inline(always)] fn into(self) -> Header<'static> { Header::new("Content-Type", self.to_string()) } } - -#[cfg(test)] -mod test { - use super::ContentType; - use std::str::FromStr; - - macro_rules! assert_no_parse { - ($string:expr) => ({ - let result = ContentType::from_str($string); - if !result.is_err() { - println!("{} parsed unexpectedly!", $string); - } - - assert!(result.is_err()); - }); - } - - macro_rules! assert_parse { - ($string:expr) => ({ - let result = ContentType::from_str($string); - if let Err(e) = result { - println!("{:?} failed to parse: {}", $string, e); - } - - result.unwrap() - }); - - ($string:expr, $ct:expr) => ({ - let c = assert_parse!($string); - assert_eq!(c.ttype, $ct.ttype); - assert_eq!(c.subtype, $ct.subtype); - assert_eq!(c.params, $ct.params); - c - }) - } - - #[test] - fn test_simple() { - assert_parse!("application/json", ContentType::JSON); - assert_parse!("*/json", ContentType::new("*", "json")); - assert_parse!("text/html;charset=utf-8", ContentType::HTML); - assert_parse!("text/html ; charset=utf-8", ContentType::HTML); - assert_parse!("text/html ;charset=utf-8", ContentType::HTML); - assert_parse!("TEXT/html;charset=utf-8", ContentType::HTML); - assert_parse!("*/*", ContentType::Any); - assert_parse!("application/*", ContentType::new("application", "*")); - assert_parse!("image/svg+xml", ContentType::SVG); - } - - #[test] - fn test_params() { - assert_parse!("*/*;a=1;b=2;c=3;d=4", - ContentType::with_params("*", "*", Some("a=1;b=2;c=3;d=4"))); - assert_parse!("*/*; a=1; b=2; c=3;d=4", - ContentType::with_params("*", "*", Some("a=1;b=2;c=3;d=4"))); - assert_parse!("application/*;else=1", - ContentType::with_params("application", "*", Some("else=1"))); - assert_parse!("*/*;charset=utf-8;else=1", - ContentType::with_params("*", "*", Some("charset=utf-8;else=1"))); - assert_parse!("*/*; charset=utf-8; else=1", - ContentType::with_params("*", "*", Some("charset=utf-8;else=1"))); - assert_parse!("*/*; charset=\"utf-8\"; else=1", - ContentType::with_params("*", "*", Some("charset=\"utf-8\";else=1"))); - assert_parse!("multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq", - ContentType::with_params("multipart", "form-data", - Some("boundary=----WebKitFormBoundarypRshfItmvaC3aEuq"))); - } - - #[test] - fn test_bad_parses() { - assert_no_parse!("application//json"); - assert_no_parse!("application///json"); - assert_no_parse!("*&_/*)()"); - assert_no_parse!("/json"); - assert_no_parse!("text/"); - assert_no_parse!("text//"); - assert_no_parse!("/"); - assert_no_parse!("*/"); - assert_no_parse!("/*"); - assert_no_parse!("///"); - assert_no_parse!(""); - assert_no_parse!("*/*;"); - assert_no_parse!("*/*;a="); - assert_no_parse!("*/*;a= "); - assert_no_parse!("*/*;a=@#$%^&*()"); - assert_no_parse!("*/*;;"); - assert_no_parse!("*/*;=;"); - assert_no_parse!("*/*=;"); - assert_no_parse!("*/*=;="); - assert_no_parse!("*/*; a=b;"); - assert_no_parse!("*/*; a = b"); - } -} diff --git a/lib/src/http/known_media_types.rs b/lib/src/http/known_media_types.rs index 8731e1c0..1eb7706e 100644 --- a/lib/src/http/known_media_types.rs +++ b/lib/src/http/known_media_types.rs @@ -4,6 +4,7 @@ macro_rules! known_media_types { HTML (is_html): "HTML", "text", "html" ; "charset" => "utf-8", Plain (is_plain): "plaintext", "text", "plain" ; "charset" => "utf-8", JSON (is_json): "JSON", "application", "json", + MsgPack (is_msgpack): "MessagePack", "application", "msgpack", Form (is_form): "forms", "application", "x-www-form-urlencoded", JavaScript (is_javascript): "JavaScript", "application", "javascript", CSS (is_css): "CSS", "text", "css" ; "charset" => "utf-8", diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 434c71ac..53e09a31 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -22,7 +22,8 @@ pub enum MediaParams { Dynamic(SmallVec<[(IndexedStr, IndexedStr); 2]>) } -// TODO: impl PartialEq, Hash for `MediaType`. +// Describe a media type. In particular, describe its comparison and hashing +// semantics. #[derive(Debug, Clone)] pub struct MediaType { /// Storage for the entire media type string. This will be `Some` when the @@ -118,6 +119,21 @@ macro_rules! from_extension { } impl MediaType { + /// Creates a new `MediaType` with top-level type `top` and subtype `sub`. + /// This should _only_ be used to construct uncommon or custom media types. + /// Use an associated constant for everything else. + /// + /// # Example + /// + /// Create a custom `application/x-person` media type: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let custom = MediaType::new("application", "x-person"); + /// assert_eq!(custom.top(), "application"); + /// assert_eq!(custom.sub(), "x-person"); + /// ``` #[inline] pub fn new(top: T, sub: S) -> MediaType where T: Into>, S: Into> @@ -130,6 +146,30 @@ impl MediaType { } } + /// Creates a new `MediaType` with top-level type `top`, subtype `sub`, and + /// parameters `ps`. This should _only_ be used to construct uncommon or + /// custom media types. Use an associated constant for everything else. + /// + /// # Example + /// + /// Create a custom `application/x-id; id=1` media type: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let id = MediaType::with_params("application", "x-id", Some(("id", "1"))); + /// assert_eq!(id.to_string(), "application/x-id; id=1".to_string()); + /// ``` + /// + /// Create a custom `text/person; name=bob; weight=175` media type: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let params = vec![("name", "bob"), ("ref", "2382")]; + /// let mt = MediaType::with_params("text", "person", params); + /// assert_eq!(mt.to_string(), "text/person; name=bob; ref=2382".to_string()); + /// ``` #[inline] pub fn with_params(top: T, sub: S, ps: P) -> MediaType where T: Into>, S: Into>, @@ -154,16 +194,66 @@ impl MediaType { known_extensions!(from_extension); + /// Returns the top-level type for this media type. The return type, + /// `UncasedAsciiRef`, has caseless equality comparison and hashing. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let plain = MediaType::Plain; + /// assert_eq!(plain.top(), "text"); + /// assert_eq!(plain.top(), "TEXT"); + /// assert_eq!(plain.top(), "Text"); + /// ``` #[inline] pub fn top(&self) -> &UncasedAsciiRef { self.top.to_str(self.source.as_ref()).into() } + /// Returns the subtype for this media type. The return type, + /// `UncasedAsciiRef`, has caseless equality comparison and hashing. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let plain = MediaType::Plain; + /// assert_eq!(plain.sub(), "plain"); + /// assert_eq!(plain.sub(), "PlaIN"); + /// assert_eq!(plain.sub(), "pLaIn"); + /// ``` #[inline] pub fn sub(&self) -> &UncasedAsciiRef { self.sub.to_str(self.source.as_ref()).into() } + /// Returns an iterator over the (key, value) pairs of the media type's + /// parameter list. The iterator will be empty if the media type has no + /// parameters. + /// + /// # Example + /// + /// The `MediaType::Plain` type has one parameter: `charset=utf-8`: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let plain = MediaType::Plain; + /// let plain_params: Vec<_> = plain.params().collect(); + /// assert_eq!(plain_params, vec![("charset", "utf-8")]); + /// ``` + /// + /// The `MediaType::PNG` type has no parameters: + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let png = MediaType::PNG; + /// assert_eq!(png.params().count(), 0); + /// ``` #[inline] pub fn params<'a>(&'a self) -> impl Iterator + 'a { let param_slice = match self.params { diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index ee11c776..f853453d 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -22,12 +22,14 @@ mod parse; // We need to export this for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) #[doc(hidden)] pub mod ascii; +#[doc(hidden)] pub use self::parse::IndexedStr; +#[doc(hidden)] pub use self::media_type::MediaParams; pub use self::method::Method; pub use self::content_type::ContentType; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; -pub use self::media_type::*; +pub use self::media_type::MediaType; pub use self::cookies::*; pub use self::session::*; diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index f29eb076..fca83a6c 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -217,6 +217,14 @@ mod test { #[test] fn test_bad_parses() { + assert_no_parse!("*&_/*)()"); + assert_no_parse!("/json"); + assert_no_parse!("text/"); + assert_no_parse!("text//"); + assert_no_parse!("/"); + assert_no_parse!("*/"); + assert_no_parse!("/*"); + assert_no_parse!("///"); assert_no_parse!("application//json"); assert_no_parse!("application///json"); assert_no_parse!("a/b;"); @@ -230,5 +238,10 @@ mod test { assert_no_parse!(r#"*/*; a="b"#); assert_no_parse!(r#"*/*; a="b; c=d"#); assert_no_parse!(r#"*/*; a="b; c=d"#); + assert_no_parse!("*/*;a=@#$%^&*()"); + assert_no_parse!("*/*;;"); + assert_no_parse!("*/*;=;"); + assert_no_parse!("*/*=;"); + assert_no_parse!("*/*=;="); } } diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index ec2a72db..58a03485 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -77,7 +77,7 @@ impl<'a, 'b> Collider> for URI<'a> { impl Collider for ContentType { fn collides_with(&self, other: &ContentType) -> bool { let collide = |a, b| a == "*" || b == "*" || a == b; - collide(&self.ttype, &other.ttype) && collide(&self.subtype, &other.subtype) + collide(self.top(), other.top()) && collide(self.sub(), other.sub()) } } From c09644b270236951090a4b48e1c69460d435f12b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 27 Mar 2017 01:53:45 -0700 Subject: [PATCH 053/297] Add the Accept ContentType structure. --- lib/Cargo.toml | 4 +- lib/src/http/accept.rs | 151 +++++++++++++++++++++++++++++++ lib/src/http/mod.rs | 4 +- lib/src/http/parse/accept.rs | 102 +++++++++++++++++++++ lib/src/http/parse/media_type.rs | 8 +- lib/src/http/parse/mod.rs | 2 + 6 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 lib/src/http/accept.rs create mode 100644 lib/src/http/parse/accept.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 0482b748..16af9fb5 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,8 +29,8 @@ time = "0.1" memchr = "1" base64 = "0.4" smallvec = "0.3" -pear = "0.0.5" -pear_codegen = "0.0.5" +pear = "0.0.7" +pear_codegen = "0.0.7" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs new file mode 100644 index 00000000..feb221b1 --- /dev/null +++ b/lib/src/http/accept.rs @@ -0,0 +1,151 @@ +use http::MediaType; +use http::parse::parse_accept; + +use std::ops::Deref; +use std::str::FromStr; +use std::fmt; + +#[derive(Debug, PartialEq)] +pub struct WeightedMediaType(pub MediaType, pub Option); + +impl WeightedMediaType { + #[inline(always)] + pub fn media_type(&self) -> &MediaType { + &self.0 + } + + #[inline(always)] + pub fn weight(&self) -> Option { + self.1 + } + + #[inline(always)] + pub fn weight_or(&self, default: f32) -> f32 { + self.1.unwrap_or(default) + } + + #[inline(always)] + pub fn into_inner(self) -> MediaType { + self.0 + } +} + +impl Deref for WeightedMediaType { + type Target = MediaType; + + #[inline(always)] + fn deref(&self) -> &MediaType { + &self.0 + } +} + +/// The HTTP Accept header. +#[derive(Debug, PartialEq)] +pub struct Accept(pub Vec); + +static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); + +impl Accept { + pub fn preferred(&self) -> &WeightedMediaType { + // See https://tools.ietf.org/html/rfc7231#section-5.3.2. + let mut all = self.iter(); + let mut preferred = all.next().unwrap_or(&ANY); + for current in all { + if current.weight().is_none() && preferred.weight().is_some() { + preferred = current; + } else if current.weight_or(0.0) > preferred.weight_or(1.0) { + preferred = current; + } else if current.media_type() == preferred.media_type() { + if current.weight() == preferred.weight() { + let c_count = current.params().filter(|p| p.0 != "q").count(); + let p_count = preferred.params().filter(|p| p.0 != "q").count(); + if c_count > p_count { + preferred = current; + } + } + } + } + + preferred + } + + #[inline(always)] + pub fn first(&self) -> Option<&WeightedMediaType> { + if self.0.len() > 0 { + Some(&self.0[0]) + } else { + None + } + } + + #[inline(always)] + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter() + } + + #[inline(always)] + pub fn media_types<'a>(&'a self) -> impl Iterator + 'a { + self.0.iter().map(|weighted_mt| weighted_mt.media_type()) + } +} + +impl fmt::Display for Accept { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (i, media_type) in self.iter().enumerate() { + if i >= 1 { write!(f, ", ")?; } + write!(f, "{}", media_type.0)?; + } + + Ok(()) + } +} + +impl FromStr for Accept { + // Ideally we'd return a `ParseError`, but that requires a lifetime. + type Err = String; + + #[inline] + fn from_str(raw: &str) -> Result { + parse_accept(raw).map_err(|e| e.to_string()) + } +} + +#[cfg(test)] +mod test { + use http::{Accept, MediaType}; + + macro_rules! assert_preference { + ($string:expr, $expect:expr) => ( + let accept: Accept = $string.parse().expect("accept string parse"); + let expected: MediaType = $expect.parse().expect("media type parse"); + let preferred = accept.preferred(); + assert_eq!(preferred.media_type().to_string(), expected.to_string()); + ) + } + + #[test] + fn test_preferred() { + assert_preference!("text/*", "text/*"); + assert_preference!("text/*, text/html", "text/*"); + assert_preference!("text/*; q=0.1, text/html", "text/html"); + assert_preference!("text/*; q=1, text/html", "text/html"); + assert_preference!("text/html, text/*", "text/html"); + assert_preference!("text/html, text/*; q=1", "text/html"); + assert_preference!("text/html, text/*; q=0.1", "text/html"); + assert_preference!("text/html, application/json", "text/html"); + + assert_preference!("a/b; q=0.1, a/b; q=0.2", "a/b; q=0.2"); + assert_preference!("a/b; q=0.1, b/c; q=0.2", "b/c; q=0.2"); + assert_preference!("a/b; q=0.5, b/c; q=0.2", "a/b; q=0.5"); + + assert_preference!("a/b; q=0.5, b/c; q=0.2, c/d", "c/d"); + assert_preference!("a/b; q=0.5; v=1, a/b", "a/b"); + + assert_preference!("a/b; v=1, a/b; v=1; c=2", "a/b; v=1; c=2"); + assert_preference!("a/b; v=1; c=2, a/b; v=1", "a/b; v=1; c=2"); + assert_preference!("a/b; q=0.5; v=1, a/b; q=0.5; v=1; c=2", + "a/b; q=0.5; v=1; c=2"); + assert_preference!("a/b; q=0.6; v=1, a/b; q=0.5; v=1; c=2", + "a/b; q=0.6; v=1"); + } +} diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index f853453d..4332e47b 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -17,9 +17,10 @@ mod media_type; mod content_type; mod status; mod header; +mod accept; mod parse; -// We need to export this for codegen, but otherwise it's unnecessary. +// We need to export these for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) #[doc(hidden)] pub mod ascii; #[doc(hidden)] pub use self::parse::IndexedStr; @@ -27,6 +28,7 @@ mod parse; pub use self::method::Method; pub use self::content_type::ContentType; +pub use self::accept::{Accept, WeightedMediaType}; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs new file mode 100644 index 00000000..be6aa0f8 --- /dev/null +++ b/lib/src/http/parse/accept.rs @@ -0,0 +1,102 @@ +use pear::{ParseResult, ParseError}; +use pear::parsers::*; + +use http::parse::checkers::is_whitespace; +use http::parse::media_type::media_type; +use http::{MediaType, Accept, WeightedMediaType}; + +fn q_value<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { + match media_type.params().next() { + Some(("q", value)) if value.len() <= 4 => match value.parse::().ok() { + Some(q) if q > 1.0 => ParseError::custom("accept", "q value must be <= 1.0"), + Some(q) => ParseResult::Done(Some(q)), + None => ParseError::custom("accept", "q value must be float") + }, + _ => ParseResult::Done(None) + } +} + +#[parser] +fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> { + let mut media_types = vec![]; + repeat_while!(eat(','), { + skip_while(is_whitespace); + let media_type = media_type(input); + let weight = q_value(&media_type); + media_types.push(WeightedMediaType(media_type, weight)); + }); + + Accept(media_types) +} + +pub fn parse_accept(mut input: &str) -> Result> { + parse!(&mut input, (accept(), eof()).0).into() +} + +#[cfg(test)] +mod test { + use http::{Accept, MediaType, WeightedMediaType}; + use super::{ParseResult, parse_accept}; + + macro_rules! assert_no_parse { + ($string:expr) => ({ + let result: Result<_, _> = parse_accept($string).into(); + if result.is_ok() { panic!("{:?} parsed unexpectedly.", $string) } + }); + } + + macro_rules! assert_parse { + ($string:expr) => ({ + match parse_accept($string) { + Ok(accept) => accept, + Err(e) => panic!("{:?} failed to parse: {}", $string, e) + } + }); + } + + macro_rules! assert_parse_eq { + ($string:expr, [$($mt:expr),*]) => ({ + let expected = vec![$($mt),*]; + let result = assert_parse!($string); + for (i, wmt) in result.iter().enumerate() { + assert_eq!(wmt.media_type(), &expected[i]); + } + }); + } + + macro_rules! assert_quality_eq { + ($string:expr, [$($mt:expr),*]) => ({ + let expected = vec![$($mt),*]; + let result = assert_parse!($string); + for (i, wmt) in result.iter().enumerate() { + assert_eq!(wmt.media_type(), &expected[i]); + } + }); + } + + #[test] + fn check_does_parse() { + assert_parse!("text/html"); + assert_parse!("*/*, a/b; q=1.0; v=1, application/something, a/b"); + assert_parse!("a/b, b/c"); + assert_parse!("text/*"); + assert_parse!("text/*; q=1"); + assert_parse!("text/*; q=1; level=2"); + assert_parse!("audio/*; q=0.2, audio/basic"); + assert_parse!("text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); + assert_parse!("text/*, text/html, text/html;level=1, */*"); + assert_parse!("text/*;q=0.3, text/html;q=0.7, text/html;level=1, \ + text/html;level=2;q=0.4, */*;q=0.5"); + } + + #[test] + fn check_parse_eq() { + assert_parse_eq!("text/html", [MediaType::HTML]); + assert_parse_eq!("text/html, application/json", + [MediaType::HTML, MediaType::JSON]); + assert_parse_eq!("text/html; charset=utf-8; v=1, application/json", + [MediaType::HTML, MediaType::JSON]); + assert_parse_eq!("text/html, text/html; q=0.1, text/html; q=0.2", + [MediaType::HTML, MediaType::HTML, MediaType::HTML]); + } +} diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index fca83a6c..d471ead0 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -25,7 +25,7 @@ fn quoted_string<'a>(input: &mut &'a str) -> ParseResult<&'a str, &'a str> { } #[parser] -fn media_type<'a>(input: &mut &'a str, +pub fn media_type<'a>(input: &mut &'a str, source: &'a str) -> ParseResult<&'a str, MediaType> { let top = take_some_while(|c| is_valid_token(c) && c != '/'); eat('/'); @@ -77,8 +77,7 @@ mod test { macro_rules! assert_parse { ($string:expr) => ({ - let result: Result<_, _> = parse_media_type($string).into(); - match result { + match parse_media_type($string) { Ok(media_type) => media_type, Err(e) => panic!("{:?} failed to parse: {}", $string, e) } @@ -90,9 +89,6 @@ mod test { let result = assert_parse!($string); assert_eq!(result, $result); - let result = assert_parse!($string); - assert_eq!(result, $result); - let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*]; if expected_params.len() > 0 { assert_eq!(result.params().count(), expected_params.len()); diff --git a/lib/src/http/parse/mod.rs b/lib/src/http/parse/mod.rs index f1913347..7ef09082 100644 --- a/lib/src/http/parse/mod.rs +++ b/lib/src/http/parse/mod.rs @@ -1,6 +1,8 @@ mod media_type; +mod accept; mod indexed_str; mod checkers; pub use self::indexed_str::*; pub use self::media_type::*; +pub use self::accept::*; From 9160483554ef49586802bab66e112598efd8a509 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 27 Mar 2017 03:52:26 -0700 Subject: [PATCH 054/297] A route with unspecified query parameters accepts any. This is a breaking change. It modifies collisions with respect to query parameters as well as the default ranking of routes. A route that does not specify query parameters will now match against requests with _and without_ query parameters, assuming all other elements of the route match as well. A route that _does_ specify query parameters will only match requests with query parameters; this remains true. To accommodate this change in the most natural manner possible, the default rankings of routes have changed as illustrated below: |-------------+-------+----------+---------------| | static path | query | new rank | previous rank | |-------------+-------+----------+---------------| | yes | yes | -4 | 0 | | yes | no | -3 | 0 | | no | yes | -2 | 1 | | no | no | -1 | 1 | |-------------+-------+----------+---------------| In other words, the most specific routes, with preference for paths over queries, are ranked highest (lower number). --- examples/static_files/src/tests.rs | 6 ++ lib/src/http/parse/accept.rs | 4 +- lib/src/router/collider.rs | 97 ++++++++++++++++++++---------- lib/src/router/mod.rs | 47 +++++++++++++++ lib/src/router/route.rs | 19 ++++-- 5 files changed, 134 insertions(+), 39 deletions(-) diff --git a/examples/static_files/src/tests.rs b/examples/static_files/src/tests.rs index 85626ee4..4b28b41f 100644 --- a/examples/static_files/src/tests.rs +++ b/examples/static_files/src/tests.rs @@ -34,20 +34,26 @@ fn read_file_content(path: &str) -> Vec { #[test] fn test_index_html() { test_query_file("/", "static/index.html", Status::Ok); + test_query_file("/?v=1", "static/index.html", Status::Ok); + test_query_file("/?this=should&be=ignored", "static/index.html", Status::Ok); } #[test] fn test_hidden_file() { test_query_file("/hidden/hi.txt", "static/hidden/hi.txt", Status::Ok); + test_query_file("/hidden/hi.txt?v=1", "static/hidden/hi.txt", Status::Ok); + test_query_file("/hidden/hi.txt?v=1&a=b", "static/hidden/hi.txt", Status::Ok); } #[test] fn test_icon_file() { test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok); + test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok); } #[test] fn test_invalid_path() { test_query_file("/thou_shalt_not_exist", None, Status::NotFound); test_query_file("/thou/shalt/not/exist", None, Status::NotFound); + test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound); } diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index be6aa0f8..d01a01b9 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -35,8 +35,8 @@ pub fn parse_accept(mut input: &str) -> Result> { #[cfg(test)] mod test { - use http::{Accept, MediaType, WeightedMediaType}; - use super::{ParseResult, parse_accept}; + use http::MediaType; + use super::parse_accept; macro_rules! assert_no_parse { ($string:expr) => ({ diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index 58a03485..6404207b 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -50,12 +50,9 @@ impl<'a> Collider for &'a str { } } +// This _only_ checks the `path` component of the URI. impl<'a, 'b> Collider> for URI<'a> { fn collides_with(&self, other: &URI<'b>) -> bool { - if self.query().is_some() != other.query().is_some() { - return false; - } - for (seg_a, seg_b) in self.segments().zip(other.segments()) { if seg_a.ends_with("..>") || seg_b.ends_with("..>") { return true; @@ -83,8 +80,13 @@ impl Collider for ContentType { // This implementation is used at initialization to check if two user routes // collide before launching. Format collisions works like this: -// * If route a specifies format, it only gets requests for that format. -// * If a route doesn't specify format, it gets requests for any format. +// * If route specifies format, it only gets requests for that format. +// * If route doesn't specify format, it gets requests for any format. +// Query collisions work like this: +// * If route specifies qeury, it only gets request that have queries. +// * If route doesn't specify qeury, requests with and without queries match. +// As a result, as long as everything else collides, whether a route has a query +// or not is irrelevant: it will collide. impl Collider for Route { fn collides_with(&self, b: &Route) -> bool { self.method == b.method @@ -101,12 +103,16 @@ impl Collider for Route { // This implementation is used at runtime to check if a given request is // intended for this Route. Format collisions works like this: -// * If route a specifies format, it only gets requests for that format. -// * If a route doesn't specify format, it gets requests for any format. +// * If route specifies format, it only gets requests for that format. +// * If route doesn't specify format, it gets requests for any format. +// Query collisions work like this: +// * If route specifies a query, it only gets request that have queries. +// * If route doesn't specify query, requests with & without queries collide. impl<'r> Collider> for Route { fn collides_with(&self, req: &Request<'r>) -> bool { self.method == req.method() - && req.uri().collides_with(&self.path) + && self.path.collides_with(req.uri()) + && self.path.query().map_or(true, |_| req.uri().query().is_some()) // FIXME: On payload requests, check Content-Type, else Accept. && match (req.content_type().as_ref(), self.format.as_ref()) { (Some(ct_a), Some(ct_b)) => ct_a.collides_with(ct_b), @@ -198,7 +204,6 @@ mod tests { assert!(unranked_collide("/", "///a///")); } - #[test] fn query_collisions() { assert!(unranked_collide("/?", "/?")); @@ -207,6 +212,11 @@ mod tests { assert!(unranked_collide("/?", "/?")); assert!(unranked_collide("/a/b/c?", "/a/b/c?")); assert!(unranked_collide("//b/c?", "/a/b/?")); + assert!(unranked_collide("/?", "/")); + assert!(unranked_collide("/a?", "/a")); + assert!(unranked_collide("/a?", "/a")); + assert!(unranked_collide("/a/b?", "/a/b")); + assert!(unranked_collide("/a/b", "/a/b?")); } #[test] @@ -231,12 +241,11 @@ mod tests { #[test] fn query_non_collisions() { - assert!(!unranked_collide("/?", "/")); + assert!(!unranked_collide("/a?", "/b")); + assert!(!unranked_collide("/a/b", "/a?")); + assert!(!unranked_collide("/a/b/c?", "/a/b/c/d")); + assert!(!unranked_collide("/a/hello", "/a/?")); assert!(!unranked_collide("/?", "/hi")); - assert!(!unranked_collide("/?", "/a")); - assert!(!unranked_collide("/a?", "/a")); - assert!(!unranked_collide("/a/b?", "/a/b")); - assert!(!unranked_collide("/a/b", "/a/b/?")); } #[test] @@ -353,7 +362,7 @@ mod tests { assert!(!r_ct_ct_collide(Get, "text/html", Get, "text/css")); } - fn req_route_collide(m1: Method, ct1: S1, m2: Method, ct2: S2) -> bool + fn req_route_ct_collide(m1: Method, ct1: S1, m2: Method, ct2: S2) -> bool where S1: Into>, S2: Into> { let mut req = Request::new(m1, "/"); @@ -371,23 +380,49 @@ mod tests { #[test] fn test_req_route_ct_collisions() { - assert!(req_route_collide(Get, "application/json", Get, "application/json")); - assert!(req_route_collide(Get, "application/json", Get, "application/*")); - assert!(req_route_collide(Get, "application/json", Get, "*/json")); - assert!(req_route_collide(Get, "text/html", Get, "text/html")); - assert!(req_route_collide(Get, "text/html", Get, "*/*")); + assert!(req_route_ct_collide(Get, "application/json", Get, "application/json")); + assert!(req_route_ct_collide(Get, "application/json", Get, "application/*")); + assert!(req_route_ct_collide(Get, "application/json", Get, "*/json")); + assert!(req_route_ct_collide(Get, "text/html", Get, "text/html")); + assert!(req_route_ct_collide(Get, "text/html", Get, "*/*")); - assert!(req_route_collide(Get, "text/html", Get, None)); - assert!(req_route_collide(Get, None, Get, None)); - assert!(req_route_collide(Get, "application/json", Get, None)); - assert!(req_route_collide(Get, "x-custom/anything", Get, None)); + assert!(req_route_ct_collide(Get, "text/html", Get, None)); + assert!(req_route_ct_collide(Get, None, Get, None)); + assert!(req_route_ct_collide(Get, "application/json", Get, None)); + assert!(req_route_ct_collide(Get, "x-custom/anything", Get, None)); - assert!(!req_route_collide(Get, "application/json", Get, "text/html")); - assert!(!req_route_collide(Get, "application/json", Get, "text/*")); - assert!(!req_route_collide(Get, "application/json", Get, "*/xml")); + assert!(!req_route_ct_collide(Get, "application/json", Get, "text/html")); + assert!(!req_route_ct_collide(Get, "application/json", Get, "text/*")); + assert!(!req_route_ct_collide(Get, "application/json", Get, "*/xml")); - assert!(!req_route_collide(Get, None, Get, "text/html")); - assert!(!req_route_collide(Get, None, Get, "*/*")); - assert!(!req_route_collide(Get, None, Get, "application/json")); + assert!(!req_route_ct_collide(Get, None, Get, "text/html")); + assert!(!req_route_ct_collide(Get, None, Get, "*/*")); + assert!(!req_route_ct_collide(Get, None, Get, "application/json")); + } + + fn req_route_path_collide(a: &'static str, b: &'static str) -> bool { + let req = Request::new(Get, a.to_string()); + let route = Route::ranked(0, Get, b.to_string(), dummy_handler); + route.collides_with(&req) + } + + #[test] + fn test_req_route_query_collisions() { + assert!(req_route_path_collide("/a/b?a=b", "/a/b?")); + assert!(req_route_path_collide("/a/b?a=b", "//b?")); + assert!(req_route_path_collide("/a/b?a=b", "//?")); + assert!(req_route_path_collide("/a/b?a=b", "/a/?")); + assert!(req_route_path_collide("/?b=c", "/?")); + + assert!(req_route_path_collide("/a/b?a=b", "/a/b")); + assert!(req_route_path_collide("/a/b", "/a/b")); + assert!(req_route_path_collide("/a/b/c/d?", "/a/b/c/d")); + assert!(req_route_path_collide("/a/b/c/d?v=1&v=2", "/a/b/c/d")); + + assert!(!req_route_path_collide("/a/b", "/a/b?")); + assert!(!req_route_path_collide("/a/b/c", "/a/b?")); + assert!(!req_route_path_collide("/a?b=c", "/a/b?")); + assert!(!req_route_path_collide("/?b=c", "/a/b?")); + assert!(!req_route_path_collide("/?b=c", "/a?")); } } diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index b5c4e9de..48495630 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -245,6 +245,14 @@ mod test { assert_ranked_routes!(&["//b", "/hi/c"], "/hi/c", "/hi/c"); assert_ranked_routes!(&["//", "/hi/a"], "/hi/c", "//"); assert_ranked_routes!(&["/hi/a", "/hi/"], "/hi/c", "/hi/"); + assert_ranked_routes!(&["/a", "/a?"], "/a?b=c", "/a?"); + assert_ranked_routes!(&["/a", "/a?"], "/a", "/a"); + assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/a", "/a"); + assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/b", "/"); + assert_ranked_routes!(&["/a", "/", "/a?", "/?"], + "/b?v=1", "/?"); + assert_ranked_routes!(&["/a", "/", "/a?", "/?"], + "/a?b=c", "/a?"); } fn ranked_collisions(routes: &[(isize, &'static str)]) -> bool { @@ -341,6 +349,45 @@ mod test { ); } + macro_rules! assert_default_ranked_routing { + (to: $to:expr, with: $routes:expr, expect: $($want:expr),+) => ({ + let router = router_with_routes(&$routes); + let routed_to = matches(&router, Get, $to); + let expected = &[$($want),+]; + assert!(routed_to.len() == expected.len()); + for (got, expected) in routed_to.iter().zip(expected.iter()) { + assert_eq!(got.path.as_str() as &str, expected as &str); + } + }) + } + + #[test] + fn test_default_ranked_routing() { + assert_default_ranked_routing!( + to: "a/b?v=1", + with: ["a/", "a/b"], + expect: "a/b", "a/" + ); + + assert_default_ranked_routing!( + to: "a/b?v=1", + with: ["a/", "a/b", "a/b?"], + expect: "a/b?", "a/b", "a/" + ); + + assert_default_ranked_routing!( + to: "a/b?v=1", + with: ["a/", "a/b", "a/b?", "a/?"], + expect: "a/b?", "a/b", "a/?", "a/" + ); + + assert_default_ranked_routing!( + to: "a/b", + with: ["a/", "a/b", "a/b?", "a/?"], + expect: "a/b", "a/" + ); + } + fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool { println!("Testing: {} (expect: {:?})", path, expected); route(router, Get, path).map_or(false, |route| { diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index 4796ef38..14a123ba 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -23,10 +23,16 @@ pub struct Route { pub format: Option, } -fn default_rank(path: &str) -> isize { - // The rank for a given path is 0 if it is a static route (it doesn't - // contain any dynamic ) or 1 if it is dynamic. - path.contains('<') as isize +#[inline(always)] +fn default_rank(uri: &URI) -> isize { + // static path, query = -4; static path, no query = -3 + // dynamic path, query = -2; dynamic path, no query = -1 + match (!uri.path().contains('<'), uri.query().is_some()) { + (true, true) => -4, + (true, false) => -3, + (false, true) => -2, + (false, false) => -1, + } } impl Route { @@ -37,11 +43,12 @@ impl Route { pub fn new(m: Method, path: S, handler: Handler) -> Route where S: AsRef { + let uri = URI::from(path.as_ref().to_string()); Route { method: m, handler: handler, - rank: default_rank(path.as_ref()), - path: URI::from(path.as_ref().to_string()), + rank: default_rank(&uri), + path: uri, format: None, } } From 1fb1cdfc588d471d6969e5a3a1d46718e9be4f38 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 28 Mar 2017 00:12:59 -0700 Subject: [PATCH 055/297] Use MediaType instead of ContentType for Route format. --- codegen/src/decorators/route.rs | 34 +++-- codegen/src/parser/route.rs | 18 +-- .../bad-value-types-in-attribute.rs | 2 +- ...ontent-type.rs => malformed-media-type.rs} | 16 +-- ...-content-type.rs => unknown-media-type.rs} | 6 +- lib/src/codegen.rs | 4 +- lib/src/router/collider.rs | 128 +++++++++--------- lib/src/router/route.rs | 8 +- 8 files changed, 107 insertions(+), 109 deletions(-) rename codegen/tests/compile-fail/{malformed-content-type.rs => malformed-media-type.rs} (68%) rename codegen/tests/compile-fail/{unknown-content-type.rs => unknown-media-type.rs} (79%) diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index cb4d909b..e00e5fdd 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -14,7 +14,7 @@ use syntax::ext::build::AstBuilder; use syntax::parse::token; use syntax::ptr::P; -use rocket::http::{Method, ContentType}; +use rocket::http::{Method, MediaType}; fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { quote_enum!(ecx, method => ::rocket::http::Method { @@ -22,21 +22,19 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { }) } -fn content_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { +fn media_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option> { ct.map(|ct| { let (top, sub) = (ct.top().as_str(), ct.sub().as_str()); - quote_expr!(ecx, ::rocket::http::ContentType( - ::rocket::http::MediaType { - source: None, - top: ::rocket::http::IndexedStr::Concrete( - ::std::borrow::Cow::Borrowed($top) - ), - sub: ::rocket::http::IndexedStr::Concrete( - ::std::borrow::Cow::Borrowed($sub) - ), - params: ::rocket::http::MediaParams::Static(&[]) - }, - )) + quote_expr!(ecx, ::rocket::http::MediaType { + source: None, + top: ::rocket::http::IndexedStr::Concrete( + ::std::borrow::Cow::Borrowed($top) + ), + sub: ::rocket::http::IndexedStr::Concrete( + ::std::borrow::Cow::Borrowed($sub) + ), + params: ::rocket::http::MediaParams::Static(&[]) + }) }) } @@ -224,10 +222,10 @@ impl RouteGenerateExt for RouteParams { let path = &self.uri.node.as_str(); let method = method_to_path(ecx, self.method.node); let format = self.format.as_ref().map(|kv| kv.value().clone()); - let content_type = option_as_expr(ecx, &content_type_to_expr(ecx, format)); + let media_type = option_as_expr(ecx, &media_type_to_expr(ecx, format)); let rank = option_as_expr(ecx, &self.rank); - (path, method, content_type, rank) + (path, method, media_type, rank) } } @@ -267,7 +265,7 @@ fn generic_route_decorator(known_method: Option>, // Generate and emit the static route info that uses the just generated // function as its handler. A proper Rocket route will be created from this. let struct_name = user_fn_name.prepend(ROUTE_STRUCT_PREFIX); - let (path, method, content_type, rank) = route.explode(ecx); + let (path, method, media_type, rank) = route.explode(ecx); let static_route_info_item = quote_item!(ecx, #[allow(non_upper_case_globals)] pub static $struct_name: ::rocket::StaticRouteInfo = @@ -275,7 +273,7 @@ fn generic_route_decorator(known_method: Option>, method: $method, path: $path, handler: $route_fn_name, - format: $content_type, + format: $media_type, rank: $rank, }; ).expect("static route info"); diff --git a/codegen/src/parser/route.rs b/codegen/src/parser/route.rs index fc8bc00a..e6d305e4 100644 --- a/codegen/src/parser/route.rs +++ b/codegen/src/parser/route.rs @@ -9,7 +9,7 @@ use utils::{span, MetaItemExt, SpanExt, is_valid_ident}; use super::{Function, ParamIter}; use super::keyvalue::KVSpanned; use super::uri::validate_uri; -use rocket::http::{Method, ContentType}; +use rocket::http::{Method, MediaType}; use rocket::http::uri::URI; /// This structure represents the parsed `route` attribute. @@ -25,7 +25,7 @@ pub struct RouteParams { pub uri: Spanned>, pub data_param: Option>, pub query_param: Option>, - pub format: Option>, + pub format: Option>, pub rank: Option>, } @@ -258,25 +258,25 @@ fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { -1 } -fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> ContentType { +fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned) -> MediaType { if let LitKind::Str(ref s, _) = *kv.value() { - if let Ok(ct) = ContentType::from_str(&s.as_str()) { + if let Ok(ct) = MediaType::from_str(&s.as_str()) { if !ct.is_known() { - let msg = format!("'{}' is not a known content-type", s); + let msg = format!("'{}' is not a known media type", s); ecx.span_warn(kv.value.span, &msg); } return ct; } else { - ecx.span_err(kv.value.span, "malformed content-type"); + ecx.span_err(kv.value.span, "malformed media type"); } } - ecx.struct_span_err(kv.span, r#"`format` must be a "content/type""#) + ecx.struct_span_err(kv.span, r#"`format` must be a "media/type""#) .help(r#"format, if specified, must be a key-value pair where the key is `format` and the value is a string representing the - content-type accepted. e.g: format = "application/json""#) + media type accepted. e.g: format = "application/json""#) .emit(); - ContentType::Any + MediaType::Any } diff --git a/codegen/tests/compile-fail/bad-value-types-in-attribute.rs b/codegen/tests/compile-fail/bad-value-types-in-attribute.rs index b6ca9ec9..0f6b4a47 100644 --- a/codegen/tests/compile-fail/bad-value-types-in-attribute.rs +++ b/codegen/tests/compile-fail/bad-value-types-in-attribute.rs @@ -12,7 +12,7 @@ fn get1() -> &'static str { "hi" } #[get(path = "/", rank = "2")] //~ ERROR must be an int fn get2() -> &'static str { "hi" } -#[get(path = "/", format = 100)] //~ ERROR must be a "content/type" +#[get(path = "/", format = 100)] //~ ERROR must be a "media/type" fn get3() -> &'static str { "hi" } fn main() { diff --git a/codegen/tests/compile-fail/malformed-content-type.rs b/codegen/tests/compile-fail/malformed-media-type.rs similarity index 68% rename from codegen/tests/compile-fail/malformed-content-type.rs rename to codegen/tests/compile-fail/malformed-media-type.rs index ac215685..37596b8f 100644 --- a/codegen/tests/compile-fail/malformed-content-type.rs +++ b/codegen/tests/compile-fail/malformed-media-type.rs @@ -4,35 +4,35 @@ extern crate rocket; #[get("/", format = "applicationx-custom")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn one() -> &'static str { "hi" } #[get("/", format = "")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn two() -> &'static str { "hi" } #[get("/", format = "//")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn three() -> &'static str { "hi" } #[get("/", format = "/")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn four() -> &'static str { "hi" } #[get("/", format = "a/")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn five() -> &'static str { "hi" } #[get("/", format = "/a")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn six() -> &'static str { "hi" } #[get("/", format = "/a/")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn seven() -> &'static str { "hi" } #[get("/", format = "a/b/")] //~ ERROR malformed -//~^ ERROR `format` must be a "content/type" +//~^ ERROR `format` must be a "media/type" fn eight() -> &'static str { "hi" } fn main() { } diff --git a/codegen/tests/compile-fail/unknown-content-type.rs b/codegen/tests/compile-fail/unknown-media-type.rs similarity index 79% rename from codegen/tests/compile-fail/unknown-content-type.rs rename to codegen/tests/compile-fail/unknown-media-type.rs index a78c7ae1..32426dc4 100644 --- a/codegen/tests/compile-fail/unknown-content-type.rs +++ b/codegen/tests/compile-fail/unknown-media-type.rs @@ -3,13 +3,13 @@ extern crate rocket; -#[get("/", format = "application/x-custom")] //~ WARNING not a known content-type +#[get("/", format = "application/x-custom")] //~ WARNING not a known media type fn one() -> &'static str { "hi" } -#[get("/", format = "x-custom/plain")] //~ WARNING not a known content-type +#[get("/", format = "x-custom/plain")] //~ WARNING not a known media type fn two() -> &'static str { "hi" } -#[get("/", format = "x-custom/x-custom")] //~ WARNING not a known content-type +#[get("/", format = "x-custom/x-custom")] //~ WARNING not a known media type fn three() -> &'static str { "hi" } // Make the test fail here so we can actually check for the warnings above. diff --git a/lib/src/codegen.rs b/lib/src/codegen.rs index 8116037b..7741584a 100644 --- a/lib/src/codegen.rs +++ b/lib/src/codegen.rs @@ -1,10 +1,10 @@ use handler::{Handler, ErrorHandler}; -use http::{Method, ContentType}; +use http::{Method, MediaType}; pub struct StaticRouteInfo { pub method: Method, pub path: &'static str, - pub format: Option, + pub format: Option, pub handler: Handler, pub rank: Option, } diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index 6404207b..f08b2534 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -1,7 +1,7 @@ use super::Route; use http::uri::URI; -use http::ContentType; +use http::MediaType; use request::Request; /// The Collider trait is used to determine if two items that can be routed on @@ -71,8 +71,8 @@ impl<'a, 'b> Collider> for URI<'a> { } } -impl Collider for ContentType { - fn collides_with(&self, other: &ContentType) -> bool { +impl Collider for MediaType { + fn collides_with(&self, other: &MediaType) -> bool { let collide = |a, b| a == "*" || b == "*" || a == b; collide(self.top(), other.top()) && collide(self.sub(), other.sub()) } @@ -93,7 +93,7 @@ impl Collider for Route { && self.rank == b.rank && self.path.collides_with(&b.path) && match (self.format.as_ref(), b.format.as_ref()) { - (Some(ct_a), Some(ct_b)) => ct_a.collides_with(ct_b), + (Some(mt_a), Some(mt_b)) => mt_a.collides_with(mt_b), (Some(_), None) => true, (None, Some(_)) => true, (None, None) => true @@ -115,7 +115,7 @@ impl<'r> Collider> for Route { && self.path.query().map_or(true, |_| req.uri().query().is_some()) // FIXME: On payload requests, check Content-Type, else Accept. && match (req.content_type().as_ref(), self.format.as_ref()) { - (Some(ct_a), Some(ct_b)) => ct_a.collides_with(ct_b), + (Some(mt_a), Some(mt_b)) => mt_a.collides_with(mt_b), (Some(_), None) => true, (None, Some(_)) => false, (None, None) => true @@ -132,7 +132,7 @@ mod tests { use data::Data; use handler::Outcome; use router::route::Route; - use http::{Method, ContentType}; + use http::{Method, MediaType, ContentType}; use http::uri::URI; use http::Method::*; @@ -301,41 +301,41 @@ mod tests { assert!(!s_s_collide("/a/hi/", "/a/hi/")); } - fn ct_ct_collide(ct1: &str, ct2: &str) -> bool { - let ct_a = ContentType::from_str(ct1).expect(ct1); - let ct_b = ContentType::from_str(ct2).expect(ct2); - ct_a.collides_with(&ct_b) + fn mt_mt_collide(mt1: &str, mt2: &str) -> bool { + let mt_a = MediaType::from_str(mt1).expect(mt1); + let mt_b = MediaType::from_str(mt2).expect(mt2); + mt_a.collides_with(&mt_b) } #[test] fn test_content_type_colliions() { - assert!(ct_ct_collide("application/json", "application/json")); - assert!(ct_ct_collide("*/json", "application/json")); - assert!(ct_ct_collide("*/*", "application/json")); - assert!(ct_ct_collide("application/*", "application/json")); - assert!(ct_ct_collide("application/*", "*/json")); - assert!(ct_ct_collide("something/random", "something/random")); + assert!(mt_mt_collide("application/json", "application/json")); + assert!(mt_mt_collide("*/json", "application/json")); + assert!(mt_mt_collide("*/*", "application/json")); + assert!(mt_mt_collide("application/*", "application/json")); + assert!(mt_mt_collide("application/*", "*/json")); + assert!(mt_mt_collide("something/random", "something/random")); - assert!(!ct_ct_collide("text/*", "application/*")); - assert!(!ct_ct_collide("*/text", "*/json")); - assert!(!ct_ct_collide("*/text", "application/test")); - assert!(!ct_ct_collide("something/random", "something_else/random")); - assert!(!ct_ct_collide("something/random", "*/else")); - assert!(!ct_ct_collide("*/random", "*/else")); - assert!(!ct_ct_collide("something/*", "random/else")); + assert!(!mt_mt_collide("text/*", "application/*")); + assert!(!mt_mt_collide("*/text", "*/json")); + assert!(!mt_mt_collide("*/text", "application/test")); + assert!(!mt_mt_collide("something/random", "something_else/random")); + assert!(!mt_mt_collide("something/random", "*/else")); + assert!(!mt_mt_collide("*/random", "*/else")); + assert!(!mt_mt_collide("something/*", "random/else")); } - fn r_ct_ct_collide(m1: Method, ct1: S1, m2: Method, ct2: S2) -> bool + fn r_mt_mt_collide(m1: Method, mt1: S1, m2: Method, mt2: S2) -> bool where S1: Into>, S2: Into> { let mut route_a = Route::new(m1, "/", dummy_handler); - if let Some(ct_str) = ct1.into() { - route_a.format = Some(ct_str.parse::().unwrap()); + if let Some(mt_str) = mt1.into() { + route_a.format = Some(mt_str.parse::().unwrap()); } let mut route_b = Route::new(m2, "/", dummy_handler); - if let Some(ct_str) = ct2.into() { - route_b.format = Some(ct_str.parse::().unwrap()); + if let Some(mt_str) = mt2.into() { + route_b.format = Some(mt_str.parse::().unwrap()); } route_a.collides_with(&route_b) @@ -343,61 +343,61 @@ mod tests { #[test] fn test_route_content_type_colliions() { - assert!(r_ct_ct_collide(Get, "application/json", Get, "application/json")); - assert!(r_ct_ct_collide(Get, "*/json", Get, "application/json")); - assert!(r_ct_ct_collide(Get, "*/json", Get, "application/*")); - assert!(r_ct_ct_collide(Get, "text/html", Get, "text/*")); - assert!(r_ct_ct_collide(Get, "any/thing", Get, "*/*")); + assert!(r_mt_mt_collide(Get, "application/json", Get, "application/json")); + assert!(r_mt_mt_collide(Get, "*/json", Get, "application/json")); + assert!(r_mt_mt_collide(Get, "*/json", Get, "application/*")); + assert!(r_mt_mt_collide(Get, "text/html", Get, "text/*")); + assert!(r_mt_mt_collide(Get, "any/thing", Get, "*/*")); - assert!(r_ct_ct_collide(Get, None, Get, "text/*")); - assert!(r_ct_ct_collide(Get, None, Get, "text/html")); - assert!(r_ct_ct_collide(Get, None, Get, "*/*")); - assert!(r_ct_ct_collide(Get, "text/html", Get, None)); - assert!(r_ct_ct_collide(Get, "*/*", Get, None)); - assert!(r_ct_ct_collide(Get, "application/json", Get, None)); + assert!(r_mt_mt_collide(Get, None, Get, "text/*")); + assert!(r_mt_mt_collide(Get, None, Get, "text/html")); + assert!(r_mt_mt_collide(Get, None, Get, "*/*")); + assert!(r_mt_mt_collide(Get, "text/html", Get, None)); + assert!(r_mt_mt_collide(Get, "*/*", Get, None)); + assert!(r_mt_mt_collide(Get, "application/json", Get, None)); - assert!(!r_ct_ct_collide(Get, "text/html", Get, "application/*")); - assert!(!r_ct_ct_collide(Get, "application/html", Get, "text/*")); - assert!(!r_ct_ct_collide(Get, "*/json", Get, "text/html")); - assert!(!r_ct_ct_collide(Get, "text/html", Get, "text/css")); + assert!(!r_mt_mt_collide(Get, "text/html", Get, "application/*")); + assert!(!r_mt_mt_collide(Get, "application/html", Get, "text/*")); + assert!(!r_mt_mt_collide(Get, "*/json", Get, "text/html")); + assert!(!r_mt_mt_collide(Get, "text/html", Get, "text/css")); } - fn req_route_ct_collide(m1: Method, ct1: S1, m2: Method, ct2: S2) -> bool + fn req_route_mt_collide(m1: Method, mt1: S1, m2: Method, mt2: S2) -> bool where S1: Into>, S2: Into> { let mut req = Request::new(m1, "/"); - if let Some(ct_str) = ct1.into() { - req.replace_header(ct_str.parse::().unwrap()); + if let Some(mt_str) = mt1.into() { + req.replace_header(mt_str.parse::().unwrap()); } let mut route = Route::new(m2, "/", dummy_handler); - if let Some(ct_str) = ct2.into() { - route.format = Some(ct_str.parse::().unwrap()); + if let Some(mt_str) = mt2.into() { + route.format = Some(mt_str.parse::().unwrap()); } route.collides_with(&req) } #[test] - fn test_req_route_ct_collisions() { - assert!(req_route_ct_collide(Get, "application/json", Get, "application/json")); - assert!(req_route_ct_collide(Get, "application/json", Get, "application/*")); - assert!(req_route_ct_collide(Get, "application/json", Get, "*/json")); - assert!(req_route_ct_collide(Get, "text/html", Get, "text/html")); - assert!(req_route_ct_collide(Get, "text/html", Get, "*/*")); + fn test_req_route_mt_collisions() { + assert!(req_route_mt_collide(Get, "application/json", Get, "application/json")); + assert!(req_route_mt_collide(Get, "application/json", Get, "application/*")); + assert!(req_route_mt_collide(Get, "application/json", Get, "*/json")); + assert!(req_route_mt_collide(Get, "text/html", Get, "text/html")); + assert!(req_route_mt_collide(Get, "text/html", Get, "*/*")); - assert!(req_route_ct_collide(Get, "text/html", Get, None)); - assert!(req_route_ct_collide(Get, None, Get, None)); - assert!(req_route_ct_collide(Get, "application/json", Get, None)); - assert!(req_route_ct_collide(Get, "x-custom/anything", Get, None)); + assert!(req_route_mt_collide(Get, "text/html", Get, None)); + assert!(req_route_mt_collide(Get, None, Get, None)); + assert!(req_route_mt_collide(Get, "application/json", Get, None)); + assert!(req_route_mt_collide(Get, "x-custom/anything", Get, None)); - assert!(!req_route_ct_collide(Get, "application/json", Get, "text/html")); - assert!(!req_route_ct_collide(Get, "application/json", Get, "text/*")); - assert!(!req_route_ct_collide(Get, "application/json", Get, "*/xml")); + assert!(!req_route_mt_collide(Get, "application/json", Get, "text/html")); + assert!(!req_route_mt_collide(Get, "application/json", Get, "text/*")); + assert!(!req_route_mt_collide(Get, "application/json", Get, "*/xml")); - assert!(!req_route_ct_collide(Get, None, Get, "text/html")); - assert!(!req_route_ct_collide(Get, None, Get, "*/*")); - assert!(!req_route_ct_collide(Get, None, Get, "application/json")); + assert!(!req_route_mt_collide(Get, None, Get, "text/html")); + assert!(!req_route_mt_collide(Get, None, Get, "*/*")); + assert!(!req_route_mt_collide(Get, None, Get, "application/json")); } fn req_route_path_collide(a: &'static str, b: &'static str) -> bool { diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index 14a123ba..95678ba6 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -6,10 +6,10 @@ use term_painter::Color::*; use codegen::StaticRouteInfo; use handler::Handler; -use http::{Method, ContentType}; +use http::{Method, MediaType}; use http::uri::URI; -/// A route: a method, its handler, path, rank, and format/content type. +/// A route: a method, its handler, path, rank, and format/media type. pub struct Route { /// The method this route matches against. pub method: Method, @@ -19,8 +19,8 @@ pub struct Route { pub path: URI<'static>, /// The rank of this route. Lower ranks have higher priorities. pub rank: isize, - /// The Content-Type this route matches against. - pub format: Option, + /// The media type this route matches against. + pub format: Option, } #[inline(always)] From fb29b37f30476db0ac85909c2436537826618db1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 28 Mar 2017 03:10:18 -0700 Subject: [PATCH 056/297] Reorganize extra request state. Add 'accept' and 'accept_first' methods to Request. --- lib/Cargo.toml | 4 +- lib/src/http/mod.rs | 3 +- lib/src/http/parse/accept.rs | 2 +- lib/src/http/parse/media_type.rs | 7 ++- lib/src/request/request.rs | 100 +++++++++++++++++++------------ lib/src/request/state.rs | 19 +++--- lib/src/rocket.rs | 5 +- 7 files changed, 81 insertions(+), 59 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 16af9fb5..3d55342f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,8 +29,8 @@ time = "0.1" memchr = "1" base64 = "0.4" smallvec = "0.3" -pear = "0.0.7" -pear_codegen = "0.0.7" +pear = "0.0.8" +pear_codegen = "0.0.8" [dependencies.cookie] version = "0.7.2" diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 4332e47b..1d256db0 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -18,7 +18,8 @@ mod content_type; mod status; mod header; mod accept; -mod parse; + +pub(crate) mod parse; // We need to export these for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index d01a01b9..2733f0fe 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -21,7 +21,7 @@ fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> { let mut media_types = vec![]; repeat_while!(eat(','), { skip_while(is_whitespace); - let media_type = media_type(input); + let media_type = media_type(); let weight = q_value(&media_type); media_types.push(WeightedMediaType(media_type, weight)); }); diff --git a/lib/src/http/parse/media_type.rs b/lib/src/http/parse/media_type.rs index d471ead0..45684fab 100644 --- a/lib/src/http/parse/media_type.rs +++ b/lib/src/http/parse/media_type.rs @@ -25,8 +25,9 @@ fn quoted_string<'a>(input: &mut &'a str) -> ParseResult<&'a str, &'a str> { } #[parser] -pub fn media_type<'a>(input: &mut &'a str, - source: &'a str) -> ParseResult<&'a str, MediaType> { +pub fn media_type<'a>(input: &mut &'a str) -> ParseResult<&'a str, MediaType> { + let source: &str = *input; + let top = take_some_while(|c| is_valid_token(c) && c != '/'); eat('/'); let sub = take_some_while(is_valid_token); @@ -58,7 +59,7 @@ pub fn media_type<'a>(input: &mut &'a str, } pub fn parse_media_type(mut input: &str) -> Result> { - parse!(&mut input, (media_type(input), eof()).0).into() + parse!(&mut input, (media_type(), eof()).0).into() } #[cfg(test)] diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index bab853f3..2c9b7312 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -13,9 +13,23 @@ use super::{FromParam, FromSegments}; use router::Route; use http::uri::{URI, Segments}; -use http::{Method, ContentType, Header, HeaderMap, Cookies, Session, CookieJar, Key}; +use http::{Method, Header, HeaderMap, Cookies, Session, CookieJar, Key}; +use http::{ContentType, Accept, MediaType}; +use http::parse::media_type; use http::hyper; +struct PresetState<'r> { + key: &'r Key, + managed_state: &'r Container, +} + +struct RequestState<'r> { + preset: Option>, + params: RefCell>, + cookies: RefCell, + session: RefCell, +} + /// The type of an incoming web request. /// /// This should be used sparingly in Rocket applications. In particular, it @@ -26,13 +40,9 @@ use http::hyper; pub struct Request<'r> { method: Method, uri: URI<'r>, - key: Option<&'r Key>, headers: HeaderMap<'r>, remote: Option, - params: RefCell>, - cookies: RefCell, - session: RefCell, - state: Option<&'r Container>, + extra: RequestState<'r> } impl<'r> Request<'r> { @@ -49,17 +59,19 @@ impl<'r> Request<'r> { /// # #[allow(unused_variables)] /// let request = Request::new(Method::Get, "/uri"); /// ``` + #[inline(always)] pub fn new>>(method: Method, uri: U) -> Request<'r> { Request { method: method, uri: uri.into(), headers: HeaderMap::new(), - key: None, remote: None, - params: RefCell::new(Vec::new()), - cookies: RefCell::new(CookieJar::new()), - session: RefCell::new(CookieJar::new()), - state: None + extra: RequestState { + preset: None, + params: RefCell::new(Vec::new()), + cookies: RefCell::new(CookieJar::new()), + session: RefCell::new(CookieJar::new()), + } } } @@ -132,7 +144,7 @@ impl<'r> Request<'r> { #[inline(always)] pub fn set_uri<'u: 'r, U: Into>>(&mut self, uri: U) { self.uri = uri.into(); - self.params = RefCell::new(Vec::new()); + *self.extra.params.borrow_mut() = Vec::new(); } /// Returns the address of the remote connection that initiated this @@ -172,7 +184,6 @@ impl<'r> Request<'r> { /// /// assert_eq!(request.remote(), Some(localhost)); /// ``` - #[doc(hidden)] #[inline(always)] pub fn set_remote(&mut self, address: SocketAddr) { self.remote = Some(address); @@ -257,7 +268,7 @@ impl<'r> Request<'r> { /// ``` #[inline] pub fn cookies(&self) -> Cookies { - match self.cookies.try_borrow_mut() { + match self.extra.cookies.try_borrow_mut() { Ok(jar) => Cookies::new(jar), Err(_) => { error_!("Multiple `Cookies` instances are active at once."); @@ -271,14 +282,14 @@ impl<'r> Request<'r> { #[inline] pub fn session(&self) -> Session { - match self.session.try_borrow_mut() { - Ok(jar) => Session::new(jar, self.key.unwrap()), + match self.extra.session.try_borrow_mut() { + Ok(jar) => Session::new(jar, self.preset().key), Err(_) => { error_!("Multiple `Session` instances are active at once."); info_!("An instance of `Session` must be dropped before another \ can be retrieved."); warn_!("The retrieved `Session` instance will be empty."); - Session::empty(self.key.unwrap()) + Session::empty(self.preset().key) } } } @@ -286,13 +297,13 @@ impl<'r> Request<'r> { /// Replace all of the cookies in `self` with those in `jar`. #[inline] pub(crate) fn set_cookies(&mut self, jar: CookieJar) { - self.cookies = RefCell::new(jar); + self.extra.cookies = RefCell::new(jar); } /// Replace all of the session cookie in `self` with those in `jar`. #[inline] pub(crate) fn set_session(&mut self, jar: CookieJar) { - self.session = RefCell::new(jar); + self.extra.session = RefCell::new(jar); } /// Returns `Some` of the Content-Type header of `self`. If the header is @@ -312,8 +323,17 @@ impl<'r> Request<'r> { /// ``` #[inline(always)] pub fn content_type(&self) -> Option { - self.headers().get_one("Content-Type") - .and_then(|value| value.parse().ok()) + self.headers().get_one("Content-Type").and_then(|value| value.parse().ok()) + } + + #[inline(always)] + pub fn accept(&self) -> Option { + self.headers().get_one("Accept").and_then(|v| v.parse().ok()) + } + + #[inline(always)] + pub fn accept_first(&self) -> Option { + self.headers().get_one("Accept").and_then(|mut v| media_type(&mut v).ok()) } /// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from @@ -349,14 +369,14 @@ impl<'r> Request<'r> { /// TODO: Figure out the mount path from here. #[inline] pub(crate) fn set_params(&self, route: &Route) { - *self.params.borrow_mut() = route.get_param_indexes(self.uri()); + *self.extra.params.borrow_mut() = route.get_param_indexes(self.uri()); } /// Get the `n`th path parameter as a string, if it exists. This is used by /// codegen. #[doc(hidden)] pub fn get_param_str(&self, n: usize) -> Option<&str> { - let params = self.params.borrow(); + let params = self.extra.params.borrow(); if n >= params.len() { debug!("{} is >= param count {}", n, params.len()); return None; @@ -401,7 +421,7 @@ impl<'r> Request<'r> { /// exist. Used by codegen. #[doc(hidden)] pub fn get_raw_segments(&self, n: usize) -> Option { - let params = self.params.borrow(); + let params = self.extra.params.borrow(); if n >= params.len() { debug!("{} is >= param (segments) count {}", n, params.len()); return None; @@ -418,21 +438,27 @@ impl<'r> Request<'r> { } /// Get the managed state container, if it exists. For internal use only! - #[inline] - pub(crate) fn get_state(&self) -> Option<&'r Container> { - self.state + /// FIXME: Expose? + #[inline(always)] + pub(crate) fn get_state(&self) -> Option<&'r T> { + self.preset().managed_state.try_get() } - /// Set the state. For internal use only! - #[inline] - pub(crate) fn set_state(&mut self, state: &'r Container) { - self.state = Some(state); + #[inline(always)] + fn preset(&self) -> &PresetState<'r> { + match self.extra.preset { + Some(ref state) => state, + None => { + error_!("Internal Rocket error: preset state is unset!"); + panic!("Please report this error to the GitHub issue tracker."); + } + } } - /// Set the session key. For internal use only! - #[inline] - pub(crate) fn set_key(&mut self, key: &'r Key) { - self.key = Some(key); + /// Set the precomputed state. For internal use only! + #[inline(always)] + pub(crate) fn set_preset_state(&mut self, key: &'r Key, state: &'r Container) { + self.extra.preset = Some(PresetState { key, managed_state: state }); } /// Convert from Hyper types into a Rocket Request. @@ -455,6 +481,7 @@ impl<'r> Request<'r> { // Construct the request object. let mut request = Request::new(method, uri); + request.set_remote(h_addr); // Set the request cookies, if they exist. if let Some(cookie_headers) = h_headers.get_raw("Cookie") { @@ -491,9 +518,6 @@ impl<'r> Request<'r> { } } - // Set the remote address. - request.set_remote(h_addr); - Ok(request) } } diff --git a/lib/src/request/state.rs b/lib/src/request/state.rs index c76d9fc3..328cdb79 100644 --- a/lib/src/request/state.rs +++ b/lib/src/request/state.rs @@ -59,6 +59,7 @@ impl<'r, T: Send + Sync + 'static> State<'r, T> { /// Using this method is typically unnecessary as `State` implements `Deref` /// with a `Target` of `T`. This means Rocket will automatically coerce a /// `State` to an `&T` when the types call for it. + #[inline(always)] pub fn inner(&self) -> &'r T { self.0 } @@ -68,19 +69,14 @@ impl<'r, T: Send + Sync + 'static> State<'r, T> { impl<'a, 'r, T: Send + Sync + 'static> FromRequest<'a, 'r> for State<'r, T> { type Error = (); + #[inline(always)] fn from_request(req: &'a Request<'r>) -> request::Outcome, ()> { - if let Some(state) = req.get_state() { - match state.try_get::() { - Some(state) => Outcome::Success(State(state)), - None => { - error_!("Attempted to retrieve unmanaged state!"); - Outcome::Failure((Status::InternalServerError, ())) - } + match req.get_state::() { + Some(state) => Outcome::Success(State(state)), + None => { + error_!("Attempted to retrieve unmanaged state!"); + Outcome::Failure((Status::InternalServerError, ())) } - } else { - error_!("Internal Rocket error: managed state is unset!"); - error_!("Please report this error in the Rocket GitHub issue tracker."); - Outcome::Failure((Status::InternalServerError, ())) } } } @@ -88,6 +84,7 @@ impl<'a, 'r, T: Send + Sync + 'static> FromRequest<'a, 'r> for State<'r, T> { impl<'r, T: Send + Sync + 'static> Deref for State<'r, T> { type Target = T; + #[inline(always)] fn deref(&self) -> &T { self.0 } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 0ac1e753..eec24c54 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -182,9 +182,8 @@ impl Rocket { -> Response<'r> { info!("{}:", request); - // Inform the request about the state and session key. - request.set_state(&self.state); - request.set_key(&self.config.session_key()); + // Inform the request about all of the precomputed state. + request.set_preset_state(&self.config.session_key(), &self.state); // Do a bit of preprocessing before routing. self.preprocess_request(request, &data); From c58ca894b78eacce484316bd4856f7efca4efb8d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 04:08:53 -0700 Subject: [PATCH 057/297] Initial implementation of content negotiation via `Accept`. This is a breaking change. This commit changes the meaning of the `format` route attribute when used on non-payload carrying requests (GET, HEAD, CONNECT, TRACE, and OPTIONS) so that it matches against the preferred media type in the `Accept` header of the request. The preferred media type is computed according to the HTTP 1.1 RFC, barring a few specificty rules to come. --- examples/content_types/src/main.rs | 32 +++++---- examples/content_types/src/tests.rs | 33 +++++---- lib/Cargo.toml | 2 +- lib/src/data/from_data.rs | 19 ++++- lib/src/http/accept.rs | 105 ++++++++++++++++++++++------ lib/src/http/content_type.rs | 16 ++++- lib/src/http/known_media_types.rs | 4 +- lib/src/http/media_type.rs | 21 +++--- lib/src/http/mod.rs | 45 ++++++++++++ lib/src/http/parse/accept.rs | 6 +- lib/src/request/request.rs | 19 ++++- lib/src/router/collider.rs | 74 +++++++++++++------- 12 files changed, 277 insertions(+), 99 deletions(-) diff --git a/examples/content_types/src/main.rs b/examples/content_types/src/main.rs index c8cd7d90..d9f76d27 100644 --- a/examples/content_types/src/main.rs +++ b/examples/content_types/src/main.rs @@ -9,33 +9,37 @@ extern crate serde_derive; #[cfg(test)] mod tests; use rocket::Request; -use rocket::http::ContentType; use rocket::response::content; #[derive(Debug, Serialize, Deserialize)] struct Person { name: String, - age: i8, + age: u8, } -// This shows how to manually serialize some JSON, but in a real application, -// we'd use the JSON contrib type. +// In a `GET` request and all other non-payload supporting request types, the +// preferred media type in the Accept header is matched against the `format` in +// the route attribute. #[get("//", format = "application/json")] -fn hello(content_type: ContentType, name: String, age: i8) -> content::JSON { - let person = Person { - name: name, - age: age, - }; +fn get_hello(name: String, age: u8) -> content::JSON { + // In a real application, we'd use the JSON contrib type. + let person = Person { name: name, age: age, }; + content::JSON(serde_json::to_string(&person).unwrap()) +} - println!("ContentType: {}", content_type); +// In a `POST` request and all other payload supporting request types, the +// content type is matched against the `format` in the route attribute. +#[post("/", format = "text/plain", data = "")] +fn post_hello(age: u8, name: String) -> content::JSON { + let person = Person { name: name, age: age, }; content::JSON(serde_json::to_string(&person).unwrap()) } #[error(404)] fn not_found(request: &Request) -> content::HTML { - let html = match request.content_type() { - Some(ref ct) if !ct.is_json() => { - format!("

This server only supports JSON requests, not '{}'.

", ct) + let html = match request.format() { + Some(ref mt) if !mt.is_json() && !mt.is_plain() => { + format!("

'{}' requests are not supported.

", mt) } _ => format!("

Sorry, '{}' is an invalid path! Try \ /hello/<name>/<age> instead.

", @@ -47,7 +51,7 @@ fn not_found(request: &Request) -> content::HTML { fn main() { rocket::ignite() - .mount("/hello", routes![hello]) + .mount("/hello", routes![get_hello, post_hello]) .catch(errors![not_found]) .launch(); } diff --git a/examples/content_types/src/tests.rs b/examples/content_types/src/tests.rs index b7b0f254..5e9f5e85 100644 --- a/examples/content_types/src/tests.rs +++ b/examples/content_types/src/tests.rs @@ -1,14 +1,16 @@ use super::rocket; use super::serde_json; use super::Person; -use rocket::http::{ContentType, Method, Status}; +use rocket::http::{Accept, ContentType, Header, MediaType, Method, Status}; use rocket::testing::MockRequest; -fn test(uri: &str, content_type: ContentType, status: Status, body: String) { +fn test(method: Method, uri: &str, header: H, status: Status, body: String) + where H: Into> +{ let rocket = rocket::ignite() - .mount("/hello", routes![super::hello]) + .mount("/hello", routes![super::get_hello, super::post_hello]) .catch(errors![super::not_found]); - let mut request = MockRequest::new(Method::Get, uri).header(content_type); + let mut request = MockRequest::new(method, uri).header(header); let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), status); @@ -17,24 +19,29 @@ fn test(uri: &str, content_type: ContentType, status: Status, body: String) { #[test] fn test_hello() { - let person = Person { - name: "Michael".to_string(), - age: 80, - }; + let person = Person { name: "Michael".to_string(), age: 80, }; let body = serde_json::to_string(&person).unwrap(); - test("/hello/Michael/80", ContentType::JSON, Status::Ok, body); + test(Method::Get, "/hello/Michael/80", Accept::JSON, Status::Ok, body.clone()); + test(Method::Get, "/hello/Michael/80", Accept::Any, Status::Ok, body.clone()); + + // No `Accept` header is an implicit */*. + test(Method::Get, "/hello/Michael/80", ContentType::XML, Status::Ok, body); + + let person = Person { name: "".to_string(), age: 99, }; + let body = serde_json::to_string(&person).unwrap(); + test(Method::Post, "/hello/99", ContentType::Plain, Status::Ok, body); } #[test] fn test_hello_invalid_content_type() { - let body = format!("

This server only supports JSON requests, not '{}'.

", - ContentType::HTML); - test("/hello/Michael/80", ContentType::HTML, Status::NotFound, body); + let b = format!("

'{}' requests are not supported.

", MediaType::HTML); + test(Method::Get, "/hello/Michael/80", Accept::HTML, Status::NotFound, b.clone()); + test(Method::Post, "/hello/80", ContentType::HTML, Status::NotFound, b); } #[test] fn test_404() { let body = "

Sorry, '/unknown' is an invalid path! Try \ /hello/<name>/<age> instead.

"; - test("/unknown", ContentType::JSON, Status::NotFound, body.to_string()); + test(Method::Get, "/unknown", Accept::JSON, Status::NotFound, body.to_string()); } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 3d55342f..093fb60c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -28,7 +28,7 @@ state = "0.2" time = "0.1" memchr = "1" base64 = "0.4" -smallvec = "0.3" +smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" } pear = "0.0.8" pear_codegen = "0.0.8" diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index 329378bc..ee756269 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -1,3 +1,5 @@ +use std::io::Read; + use outcome::{self, IntoOutcome}; use outcome::Outcome::*; use http::Status; @@ -115,13 +117,13 @@ impl<'a, S, E> IntoOutcome for Result { /// // Split the string into two pieces at ':'. /// let (name, age) = match string.find(':') { /// Some(i) => (&string[..i], &string[(i + 1)..]), -/// None => return Failure((Status::BadRequest, "Missing ':'.".into())) +/// None => return Failure((Status::UnprocessableEntity, "':'".into())) /// }; /// /// // Parse the age. /// let age: u16 = match age.parse() { /// Ok(age) => age, -/// Err(_) => return Failure((Status::BadRequest, "Bad age.".into())) +/// Err(_) => return Failure((Status::UnprocessableEntity, "Age".into())) /// }; /// /// // Return successfully. @@ -180,3 +182,16 @@ impl FromData for Option { } } } + +impl FromData for String { + type Error = (); + + // FIXME: Doc. + fn from_data(_: &Request, data: Data) -> Outcome { + let mut string = String::new(); + match data.open().read_to_string(&mut string) { + Ok(_) => Success(string), + Err(_) => Failure((Status::UnprocessableEntity, ())) + } + } +} diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs index feb221b1..3d669126 100644 --- a/lib/src/http/accept.rs +++ b/lib/src/http/accept.rs @@ -1,19 +1,16 @@ -use http::MediaType; -use http::parse::parse_accept; - use std::ops::Deref; use std::str::FromStr; use std::fmt; -#[derive(Debug, PartialEq)] +use smallvec::SmallVec; + +use http::{Header, IntoCollection, MediaType}; +use http::parse::parse_accept; + +#[derive(Debug, Clone, PartialEq)] pub struct WeightedMediaType(pub MediaType, pub Option); impl WeightedMediaType { - #[inline(always)] - pub fn media_type(&self) -> &MediaType { - &self.0 - } - #[inline(always)] pub fn weight(&self) -> Option { self.1 @@ -25,11 +22,23 @@ impl WeightedMediaType { } #[inline(always)] - pub fn into_inner(self) -> MediaType { + pub fn media_type(&self) -> &MediaType { + &self.0 + } + + #[inline(always)] + pub fn into_media_type(self) -> MediaType { self.0 } } +impl From for WeightedMediaType { + #[inline(always)] + fn from(media_type: MediaType) -> WeightedMediaType { + WeightedMediaType(media_type, None) + } +} + impl Deref for WeightedMediaType { type Target = MediaType; @@ -39,14 +48,55 @@ impl Deref for WeightedMediaType { } } -/// The HTTP Accept header. -#[derive(Debug, PartialEq)] -pub struct Accept(pub Vec); +// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`. +#[derive(Debug, PartialEq, Clone)] +pub enum AcceptParams { + Static(&'static [WeightedMediaType]), + Dynamic(SmallVec<[WeightedMediaType; 1]>) +} -static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); +/// The HTTP Accept header. +#[derive(Debug, Clone, PartialEq)] +pub struct Accept(AcceptParams); + +macro_rules! accept_constructor { + ($($name:ident ($check:ident): $str:expr, $t:expr, + $s:expr $(; $k:expr => $v:expr)*),+) => { + $( + #[doc="An `Accept` header with the single media type for "] + #[doc=$str] #[doc=": "] + #[doc=$t] #[doc="/"] #[doc=$s] + #[doc=""] + #[allow(non_upper_case_globals)] + pub const $name: Accept = Accept( + AcceptParams::Static(&[WeightedMediaType(MediaType::$name, None)]) + ); + )+ + }; +} + +impl> From for Accept { + #[inline(always)] + fn from(items: T) -> Accept { + Accept(AcceptParams::Dynamic(items.mapped(|item| item.into()))) + } +} impl Accept { + #[inline(always)] + pub fn new>(items: T) -> Accept { + Accept(AcceptParams::Dynamic(items.into_collection())) + } + + // FIXME: IMPLEMENT THIS. + // #[inline(always)] + // pub fn add>(&mut self, media_type: M) { + // self.0.push(media_type.into()); + // } + pub fn preferred(&self) -> &WeightedMediaType { + static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); + // // See https://tools.ietf.org/html/rfc7231#section-5.3.2. let mut all = self.iter(); let mut preferred = all.next().unwrap_or(&ANY); @@ -55,6 +105,7 @@ impl Accept { preferred = current; } else if current.weight_or(0.0) > preferred.weight_or(1.0) { preferred = current; + // FIXME: Prefer text/html over text/*, for example. } else if current.media_type() == preferred.media_type() { if current.weight() == preferred.weight() { let c_count = current.params().filter(|p| p.0 != "q").count(); @@ -71,22 +122,25 @@ impl Accept { #[inline(always)] pub fn first(&self) -> Option<&WeightedMediaType> { - if self.0.len() > 0 { - Some(&self.0[0]) - } else { - None - } + self.iter().next() } #[inline(always)] pub fn iter<'a>(&'a self) -> impl Iterator + 'a { - self.0.iter() + let slice = match self.0 { + AcceptParams::Static(slice) => slice, + AcceptParams::Dynamic(ref vec) => &vec[..], + }; + + slice.iter() } #[inline(always)] pub fn media_types<'a>(&'a self) -> impl Iterator + 'a { - self.0.iter().map(|weighted_mt| weighted_mt.media_type()) + self.iter().map(|weighted_mt| weighted_mt.media_type()) } + + known_media_types!(accept_constructor); } impl fmt::Display for Accept { @@ -110,6 +164,15 @@ impl FromStr for Accept { } } +/// Creates a new `Header` with name `Accept` and the value set to the HTTP +/// rendering of this `Accept` header. +impl Into> for Accept { + #[inline(always)] + fn into(self) -> Header<'static> { + Header::new("Accept", self.to_string()) + } +} + #[cfg(test)] mod test { use http::{Accept, MediaType}; diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 8c00ab80..276dc2c0 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use std::str::FromStr; use std::fmt; -use http::{Header, MediaType}; +use http::{IntoCollection, Header, MediaType}; use http::hyper::mime::Mime; /// Representation of HTTP Content-Types. @@ -136,7 +136,7 @@ impl ContentType { /// ```rust /// use rocket::http::ContentType; /// - /// let id = ContentType::with_params("application", "x-id", Some(("id", "1"))); + /// let id = ContentType::with_params("application", "x-id", ("id", "1")); /// assert_eq!(id.to_string(), "application/x-id; id=1".to_string()); /// ``` /// @@ -153,11 +153,21 @@ impl ContentType { pub fn with_params(top: T, sub: S, ps: P) -> ContentType where T: Into>, S: Into>, K: Into>, V: Into>, - P: IntoIterator + P: IntoCollection<(K, V)> { ContentType(MediaType::with_params(top, sub, ps)) } + #[inline(always)] + pub fn media_type(&self) -> &MediaType { + &self.0 + } + + #[inline(always)] + pub fn into_media_type(self) -> MediaType { + self.0 + } + known_media_types!(content_types); } diff --git a/lib/src/http/known_media_types.rs b/lib/src/http/known_media_types.rs index 1eb7706e..b28a7799 100644 --- a/lib/src/http/known_media_types.rs +++ b/lib/src/http/known_media_types.rs @@ -1,8 +1,8 @@ macro_rules! known_media_types { ($cont:ident) => ($cont! { - Any (is_any): "any Content-Type", "*", "*", + Any (is_any): "any media type", "*", "*", HTML (is_html): "HTML", "text", "html" ; "charset" => "utf-8", - Plain (is_plain): "plaintext", "text", "plain" ; "charset" => "utf-8", + Plain (is_plain): "plain text", "text", "plain" ; "charset" => "utf-8", JSON (is_json): "JSON", "application", "json", MsgPack (is_msgpack): "MessagePack", "application", "msgpack", Form (is_form): "forms", "application", "x-www-form-urlencoded", diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 53e09a31..07a8b7f4 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::fmt; use std::hash::{Hash, Hasher}; +use http::IntoCollection; use http::ascii::{uncased_eq, UncasedAsciiRef}; use http::parse::{IndexedStr, parse_media_type}; @@ -17,7 +18,6 @@ struct MediaParam { // FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`. #[derive(Debug, Clone)] pub enum MediaParams { - Empty, Static(&'static [(IndexedStr, IndexedStr)]), Dynamic(SmallVec<[(IndexedStr, IndexedStr); 2]>) } @@ -142,7 +142,7 @@ impl MediaType { source: None, top: IndexedStr::Concrete(top.into()), sub: IndexedStr::Concrete(sub.into()), - params: MediaParams::Empty, + params: MediaParams::Static(&[]), } } @@ -157,7 +157,7 @@ impl MediaType { /// ```rust /// use rocket::http::MediaType; /// - /// let id = MediaType::with_params("application", "x-id", Some(("id", "1"))); + /// let id = MediaType::with_params("application", "x-id", ("id", "1")); /// assert_eq!(id.to_string(), "application/x-id; id=1".to_string()); /// ``` /// @@ -174,15 +174,13 @@ impl MediaType { pub fn with_params(top: T, sub: S, ps: P) -> MediaType where T: Into>, S: Into>, K: Into>, V: Into>, - P: IntoIterator + P: IntoCollection<(K, V)> { - let mut params = SmallVec::new(); - for (key, val) in ps { - params.push(( - IndexedStr::Concrete(key.into()), - IndexedStr::Concrete(val.into()) - )) - } + let params = ps.mapped(|(key, val)| ( + IndexedStr::Concrete(key.into()), + IndexedStr::Concrete(val.into()) + )); + MediaType { source: None, @@ -259,7 +257,6 @@ impl MediaType { let param_slice = match self.params { MediaParams::Static(slice) => slice, MediaParams::Dynamic(ref vec) => &vec[..], - MediaParams::Empty => &[] }; param_slice.iter() diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 1d256db0..94999693 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -36,3 +36,48 @@ pub use self::header::{Header, HeaderMap}; pub use self::media_type::MediaType; pub use self::cookies::*; pub use self::session::*; + +use smallvec::{Array, SmallVec}; + +pub trait IntoCollection { + fn into_collection>(self) -> SmallVec
; + fn mapped U, A: Array>(self, f: F) -> SmallVec; +} + +impl IntoCollection for T { + #[inline] + fn into_collection>(self) -> SmallVec { + let mut vec = SmallVec::new(); + vec.push(self); + vec + } + + #[inline(always)] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + f(self).into_collection() + } +} + +impl IntoCollection for Vec { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + SmallVec::from_vec(self) + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.into_iter().map(|item| f(item)).collect() + } +} + +impl<'a, T: Clone> IntoCollection for &'a [T] { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + self.iter().cloned().collect() + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.iter().cloned().map(|item| f(item)).collect() + } +} diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index 2733f0fe..f40c77ad 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -5,7 +5,7 @@ use http::parse::checkers::is_whitespace; use http::parse::media_type::media_type; use http::{MediaType, Accept, WeightedMediaType}; -fn q_value<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { +fn q<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { match media_type.params().next() { Some(("q", value)) if value.len() <= 4 => match value.parse::().ok() { Some(q) if q > 1.0 => ParseError::custom("accept", "q value must be <= 1.0"), @@ -22,11 +22,11 @@ fn accept<'a>(input: &mut &'a str) -> ParseResult<&'a str, Accept> { repeat_while!(eat(','), { skip_while(is_whitespace); let media_type = media_type(); - let weight = q_value(&media_type); + let weight = q(&media_type); media_types.push(WeightedMediaType(media_type, weight)); }); - Accept(media_types) + Accept::new(media_types) } pub fn parse_accept(mut input: &str) -> Result> { diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 2c9b7312..6d607463 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -323,6 +323,8 @@ impl<'r> Request<'r> { /// ``` #[inline(always)] pub fn content_type(&self) -> Option { + // FIXME: Don't reparse each time! Use RC? Smarter than that? + // Use state::Storage! self.headers().get_one("Content-Type").and_then(|value| value.parse().ok()) } @@ -336,6 +338,20 @@ impl<'r> Request<'r> { self.headers().get_one("Accept").and_then(|mut v| media_type(&mut v).ok()) } + #[inline(always)] + pub fn format(&self) -> Option { + if self.method.supports_payload() { + self.content_type().map(|ct| ct.into_media_type()) + } else { + // FIXME: Should we be using `accept_first` or `preferred`? Or + // should we be checking neither and instead pass things through + // where the client accepts the thing at all? + self.accept() + .map(|accept| accept.preferred().media_type().clone()) + .or(Some(MediaType::Any)) + } + } + /// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from /// the request. Returns `Error::NoKey` if `n` is greater than the number of /// params. Returns `Error::BadParse` if the parameter type `T` can't be @@ -438,9 +454,8 @@ impl<'r> Request<'r> { } /// Get the managed state container, if it exists. For internal use only! - /// FIXME: Expose? #[inline(always)] - pub(crate) fn get_state(&self) -> Option<&'r T> { + pub fn get_state(&self) -> Option<&'r T> { self.preset().managed_state.try_get() } diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index f08b2534..a38b408c 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -113,12 +113,13 @@ impl<'r> Collider> for Route { self.method == req.method() && self.path.collides_with(req.uri()) && self.path.query().map_or(true, |_| req.uri().query().is_some()) - // FIXME: On payload requests, check Content-Type, else Accept. - && match (req.content_type().as_ref(), self.format.as_ref()) { - (Some(mt_a), Some(mt_b)) => mt_a.collides_with(mt_b), - (Some(_), None) => true, - (None, Some(_)) => false, - (None, None) => true + // FIXME: Avoid calling `format` is `self.format` == None. + && match self.format.as_ref() { + Some(mt_a) => match req.format().as_ref() { + Some(mt_b) => mt_a.collides_with(mt_b), + None => false + }, + None => true } } } @@ -132,7 +133,7 @@ mod tests { use data::Data; use handler::Outcome; use router::route::Route; - use http::{Method, MediaType, ContentType}; + use http::{Method, MediaType, ContentType, Accept}; use http::uri::URI; use http::Method::*; @@ -362,15 +363,19 @@ mod tests { assert!(!r_mt_mt_collide(Get, "text/html", Get, "text/css")); } - fn req_route_mt_collide(m1: Method, mt1: S1, m2: Method, mt2: S2) -> bool + fn req_route_mt_collide(m: Method, mt1: S1, mt2: S2) -> bool where S1: Into>, S2: Into> { - let mut req = Request::new(m1, "/"); + let mut req = Request::new(m, "/"); if let Some(mt_str) = mt1.into() { - req.replace_header(mt_str.parse::().unwrap()); + if m.supports_payload() { + req.replace_header(mt_str.parse::().unwrap()); + } else { + req.replace_header(mt_str.parse::().unwrap()); + } } - let mut route = Route::new(m2, "/", dummy_handler); + let mut route = Route::new(m, "/", dummy_handler); if let Some(mt_str) = mt2.into() { route.format = Some(mt_str.parse::().unwrap()); } @@ -380,24 +385,41 @@ mod tests { #[test] fn test_req_route_mt_collisions() { - assert!(req_route_mt_collide(Get, "application/json", Get, "application/json")); - assert!(req_route_mt_collide(Get, "application/json", Get, "application/*")); - assert!(req_route_mt_collide(Get, "application/json", Get, "*/json")); - assert!(req_route_mt_collide(Get, "text/html", Get, "text/html")); - assert!(req_route_mt_collide(Get, "text/html", Get, "*/*")); + assert!(req_route_mt_collide(Post, "application/json", "application/json")); + assert!(req_route_mt_collide(Post, "application/json", "application/*")); + assert!(req_route_mt_collide(Post, "application/json", "*/json")); + assert!(req_route_mt_collide(Post, "text/html", "*/*")); - assert!(req_route_mt_collide(Get, "text/html", Get, None)); - assert!(req_route_mt_collide(Get, None, Get, None)); - assert!(req_route_mt_collide(Get, "application/json", Get, None)); - assert!(req_route_mt_collide(Get, "x-custom/anything", Get, None)); + assert!(req_route_mt_collide(Get, "application/json", "application/json")); + assert!(req_route_mt_collide(Get, "text/html", "text/html")); + assert!(req_route_mt_collide(Get, "text/html", "*/*")); + assert!(req_route_mt_collide(Get, None, "text/html")); + assert!(req_route_mt_collide(Get, None, "*/*")); + assert!(req_route_mt_collide(Get, None, "application/json")); - assert!(!req_route_mt_collide(Get, "application/json", Get, "text/html")); - assert!(!req_route_mt_collide(Get, "application/json", Get, "text/*")); - assert!(!req_route_mt_collide(Get, "application/json", Get, "*/xml")); + assert!(req_route_mt_collide(Post, "text/html", None)); + assert!(req_route_mt_collide(Post, "application/json", None)); + assert!(req_route_mt_collide(Post, "x-custom/anything", None)); + assert!(req_route_mt_collide(Post, None, None)); - assert!(!req_route_mt_collide(Get, None, Get, "text/html")); - assert!(!req_route_mt_collide(Get, None, Get, "*/*")); - assert!(!req_route_mt_collide(Get, None, Get, "application/json")); + assert!(req_route_mt_collide(Get, "text/html", None)); + assert!(req_route_mt_collide(Get, "application/json", None)); + assert!(req_route_mt_collide(Get, "x-custom/anything", None)); + assert!(req_route_mt_collide(Get, None, None)); + + assert!(req_route_mt_collide(Get, "text/html, text/plain", "text/html")); + assert!(req_route_mt_collide(Get, "text/html; q=0.5, text/xml", "text/xml")); + + assert!(!req_route_mt_collide(Post, "application/json", "text/html")); + assert!(!req_route_mt_collide(Post, "application/json", "text/*")); + assert!(!req_route_mt_collide(Post, "application/json", "*/xml")); + assert!(!req_route_mt_collide(Get, "application/json", "text/html")); + assert!(!req_route_mt_collide(Get, "application/json", "text/*")); + assert!(!req_route_mt_collide(Get, "application/json", "*/xml")); + + assert!(!req_route_mt_collide(Post, None, "text/html")); + assert!(!req_route_mt_collide(Post, None, "*/*")); + assert!(!req_route_mt_collide(Post, None, "application/json")); } fn req_route_path_collide(a: &'static str, b: &'static str) -> bool { From b102a6a497c93563dc0933491c67cf1e58cb0acd Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 04:21:18 -0700 Subject: [PATCH 058/297] Implement FromRequest for Accept. --- lib/src/request/from_request.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 9ce5d925..9d4b416b 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -5,7 +5,7 @@ use outcome::{self, IntoOutcome}; use request::Request; use outcome::Outcome::*; -use http::{Status, ContentType, Method, Cookies, Session}; +use http::{Status, ContentType, Accept, Method, Cookies, Session}; use http::uri::URI; /// Type alias for the `Outcome` of a `FromRequest` conversion. @@ -220,6 +220,17 @@ impl<'a, 'r> FromRequest<'a, 'r> for Session<'a> { } } +impl<'a, 'r> FromRequest<'a, 'r> for Accept { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + match request.accept() { + Some(accept) => Success(accept), + None => Forward(()) + } + } +} + impl<'a, 'r> FromRequest<'a, 'r> for ContentType { type Error = (); From 7d48944080d209544ff8d015c31a8b41e6baf344 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 18:18:30 -0700 Subject: [PATCH 059/297] Handle specificity based preferences in Accept. Allow 3 decimals in q parameter. --- lib/src/http/accept.rs | 46 +++++++++++++++-------- lib/src/http/media_type.rs | 72 ++++++++++++++++++++++++++++++++++++ lib/src/http/parse/accept.rs | 4 +- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs index 3d669126..dbb811e6 100644 --- a/lib/src/http/accept.rs +++ b/lib/src/http/accept.rs @@ -96,23 +96,28 @@ impl Accept { pub fn preferred(&self) -> &WeightedMediaType { static ANY: WeightedMediaType = WeightedMediaType(MediaType::Any, None); - // + // See https://tools.ietf.org/html/rfc7231#section-5.3.2. let mut all = self.iter(); let mut preferred = all.next().unwrap_or(&ANY); - for current in all { - if current.weight().is_none() && preferred.weight().is_some() { - preferred = current; - } else if current.weight_or(0.0) > preferred.weight_or(1.0) { - preferred = current; - // FIXME: Prefer text/html over text/*, for example. - } else if current.media_type() == preferred.media_type() { - if current.weight() == preferred.weight() { - let c_count = current.params().filter(|p| p.0 != "q").count(); - let p_count = preferred.params().filter(|p| p.0 != "q").count(); - if c_count > p_count { - preferred = current; - } + for media_type in all { + if media_type.weight().is_none() && preferred.weight().is_some() { + // Media types without a `q` parameter are preferred. + preferred = media_type; + } else if media_type.weight_or(0.0) > preferred.weight_or(1.0) { + // Prefer media types with a greater weight, but if one doesn't + // have a weight, prefer the one we already have. + preferred = media_type; + } else if media_type.specificity() > preferred.specificity() { + // Prefer more specific media types over less specific ones. IE: + // text/html over application/*. + preferred = media_type; + } else if media_type == preferred { + // Finally, all other things being equal, prefer a media type + // with more parameters over one with fewer. IE: text/html; a=b + // over text/html. + if media_type.params().count() > preferred.params().count() { + preferred = media_type; } } } @@ -120,6 +125,8 @@ impl Accept { preferred } + // */html, text/plain + #[inline(always)] pub fn first(&self) -> Option<&WeightedMediaType> { self.iter().next() @@ -189,13 +196,22 @@ mod test { #[test] fn test_preferred() { assert_preference!("text/*", "text/*"); - assert_preference!("text/*, text/html", "text/*"); + assert_preference!("text/*, text/html", "text/html"); assert_preference!("text/*; q=0.1, text/html", "text/html"); assert_preference!("text/*; q=1, text/html", "text/html"); assert_preference!("text/html, text/*", "text/html"); + assert_preference!("text/*, text/html", "text/html"); assert_preference!("text/html, text/*; q=1", "text/html"); + assert_preference!("text/html; q=1, text/html", "text/html"); assert_preference!("text/html, text/*; q=0.1", "text/html"); + assert_preference!("text/html, application/json", "text/html"); + assert_preference!("text/html, application/json; q=1", "text/html"); + assert_preference!("application/json; q=1, text/html", "text/html"); + + assert_preference!("text/*, application/json", "application/json"); + assert_preference!("*/*, text/*", "text/*"); + assert_preference!("*/*, text/*, text/plain", "text/plain"); assert_preference!("a/b; q=0.1, a/b; q=0.2", "a/b; q=0.2"); assert_preference!("a/b; q=0.1, b/c; q=0.2", "b/c; q=0.2"); diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 07a8b7f4..a354046a 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -228,6 +228,78 @@ impl MediaType { self.sub.to_str(self.source.as_ref()).into() } + /// Returns a `u8` representing how specific the top-level type and subtype + /// of this media type are. + /// + /// The return value is either `0`, `1`, or `2`, where `2` is the most + /// specific. A `0` is returned when both the top and sublevel types are + /// `*`. A `1` is returned when only one of the top or sublevel types is + /// `*`, and a `2` is returned when neither the top or sublevel types are + /// `*`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let mt = MediaType::Plain; + /// assert_eq!(mt.specificity(), 2); + /// + /// let mt = MediaType::new("text", "*"); + /// assert_eq!(mt.specificity(), 1); + /// + /// let mt = MediaType::Any; + /// assert_eq!(mt.specificity(), 0); + /// ``` + #[inline] + pub fn specificity(&self) -> u8 { + (self.top() != "*") as u8 + (self.sub() != "*") as u8 + } + + /// Compares `self` with `other` and returns `true` if `self` and `other` + /// are exactly equal to eachother, including with respect to their + /// parameters. + /// + /// This is different from the `PartialEq` implementation in that it + /// considers parameters. If `PartialEq` returns false, this function is + /// guaranteed to return false. Similarly, if this function returns `true`, + /// `PartialEq` is guaranteed to return true. However, if `PartialEq` + /// returns `true`, this function may or may not return `true`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::MediaType; + /// + /// let plain = MediaType::Plain; + /// let plain2 = MediaType::with_params("text", "plain", ("charset", "utf-8")); + /// let just_plain = MediaType::new("text", "plain"); + /// + /// // The `PartialEq` implementation doesn't consider parameters. + /// assert!(plain == just_plain); + /// assert!(just_plain == plain2); + /// assert!(plain == plain2); + /// + /// // While `exact_eq` does. + /// assert!(!plain.exact_eq(&just_plain)); + /// assert!(!plain2.exact_eq(&just_plain)); + /// assert!(plain.exact_eq(&plain2)); + /// ``` + pub fn exact_eq(&self, other: &MediaType) -> bool { + self == other && { + let (mut a_params, mut b_params) = (self.params(), other.params()); + loop { + match (a_params.next(), b_params.next()) { + (Some(a), Some(b)) if a != b => return false, + (Some(_), Some(_)) => continue, + (Some(_), None) => return false, + (None, Some(_)) => return false, + (None, None) => return true + } + } + } + } + /// Returns an iterator over the (key, value) pairs of the media type's /// parameter list. The iterator will be empty if the media type has no /// parameters. diff --git a/lib/src/http/parse/accept.rs b/lib/src/http/parse/accept.rs index f40c77ad..127aa778 100644 --- a/lib/src/http/parse/accept.rs +++ b/lib/src/http/parse/accept.rs @@ -7,8 +7,8 @@ use http::{MediaType, Accept, WeightedMediaType}; fn q<'a>(_: &'a str, media_type: &MediaType) -> ParseResult<&'a str, Option> { match media_type.params().next() { - Some(("q", value)) if value.len() <= 4 => match value.parse::().ok() { - Some(q) if q > 1.0 => ParseError::custom("accept", "q value must be <= 1.0"), + Some(("q", value)) if value.len() <= 5 => match value.parse::().ok() { + Some(q) if q > 1. => ParseError::custom("accept", "q value must be <= 1"), Some(q) => ParseResult::Done(Some(q)), None => ParseError::custom("accept", "q value must be float") }, From 8f997a2a3909a8f8c2c3ebf2ad88cf291d8cd4ee Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 19:05:49 -0700 Subject: [PATCH 060/297] Rewrite some markdown for commonmark. --- lib/src/request/form/from_form_value.rs | 5 ++--- lib/src/request/param.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/request/form/from_form_value.rs b/lib/src/request/form/from_form_value.rs index 0d6c7d68..87c7310b 100644 --- a/lib/src/request/form/from_form_value.rs +++ b/lib/src/request/form/from_form_value.rs @@ -53,9 +53,8 @@ use http::uri::URI; /// Rocket implements `FromFormValue` for many standard library types. Their /// behavior is documented here. /// -/// * **f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64** -/// -/// **IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr** +/// * **f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64 +/// IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr** /// /// A value is validated successfully if the `from_str` method for the given /// type returns successfully. Otherwise, the raw form value is returned as diff --git a/lib/src/request/param.rs b/lib/src/request/param.rs index 30006a79..be6a8b99 100644 --- a/lib/src/request/param.rs +++ b/lib/src/request/param.rs @@ -73,9 +73,8 @@ use http::uri::{URI, Segments, SegmentError}; /// Rocket implements `FromParam` for several standard library types. Their /// behavior is documented here. /// -/// * **f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, bool** -/// -/// **IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr** +/// * **f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, bool +/// IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr** /// /// A value is parse successfully if the `from_str` method from the given /// type returns successfully. Otherwise, the raw path segment is returned From cb21fbf6af0d4fc12001c7b84095959f024e5c1c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 21:06:15 -0700 Subject: [PATCH 061/297] Small typo: parse -> parsed. --- lib/src/request/param.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/request/param.rs b/lib/src/request/param.rs index be6a8b99..b7049725 100644 --- a/lib/src/request/param.rs +++ b/lib/src/request/param.rs @@ -76,7 +76,7 @@ use http::uri::{URI, Segments, SegmentError}; /// * **f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, bool /// IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr** /// -/// A value is parse successfully if the `from_str` method from the given +/// A value is parsed successfully if the `from_str` method from the given /// type returns successfully. Otherwise, the raw path segment is returned /// in the `Err` value. /// From 2e78afbc93af31f62690c9b7b91a3d4fed5ff123 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 29 Mar 2017 23:36:54 -0700 Subject: [PATCH 062/297] Don't use a cache for Travis to prevent memory exhaustion. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ea846006..bf7e9331 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: rust -cache: cargo +# cache: cargo # linking failed with cache due to memory exhaustion. why? rust: - nightly script: ./scripts/test.sh From ff3193a22ad95a38015eed4cd2d1e84052eec41e Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 16 Mar 2017 17:48:04 -0700 Subject: [PATCH 063/297] Fix spelling in Method docs: ff -> if. --- lib/src/http/method.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/http/method.rs b/lib/src/http/method.rs index 27f5597f..529e261d 100644 --- a/lib/src/http/method.rs +++ b/lib/src/http/method.rs @@ -40,7 +40,7 @@ impl Method { } } - /// Returns `true` ff an HTTP request with the method represented by `self` + /// Returns `true` if an HTTP request with the method represented by `self` /// supports a payload. /// /// # Example From d4b9360f57cc9a5825019edcc1a04dedaa8dc677 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 00:14:45 -0700 Subject: [PATCH 064/297] Remove the '...' in the launch message. --- lib/src/rocket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index eec24c54..c2ee2993 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -557,7 +557,7 @@ impl Rocket { Err(e) => return LaunchError::from(e) }; - info!("🚀 {} {}{}...", + info!("🚀 {} {}{}", White.paint("Rocket has launched from"), White.bold().paint("http://"), White.bold().paint(&full_addr)); From dd3c03a83a635482238d7fd6bb76f20664771294 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 02:01:49 -0700 Subject: [PATCH 065/297] Use sudo: required to get a VM with higher specs on Travis. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bf7e9331..22adfdd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: rust -# cache: cargo # linking failed with cache due to memory exhaustion. why? +sudo: required # so we get a VM with higher specs +cache: cargo rust: - nightly script: ./scripts/test.sh From 301257623c9d069e88c6b31554f080a58eaa5d78 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 00:41:05 -0700 Subject: [PATCH 066/297] Update diesel and rusqlite example dependencies. --- Cargo.toml | 2 +- examples/raw_sqlite/Cargo.toml | 2 +- examples/todo/Cargo.toml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 130c84ed..47d91dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,5 +32,5 @@ members = [ "examples/state", "examples/uuid", "examples/session", - # "examples/raw_sqlite", + "examples/raw_sqlite", ] diff --git a/examples/raw_sqlite/Cargo.toml b/examples/raw_sqlite/Cargo.toml index 293ddd6c..9df1c4b5 100644 --- a/examples/raw_sqlite/Cargo.toml +++ b/examples/raw_sqlite/Cargo.toml @@ -6,7 +6,7 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -rusqlite = "0.9" +rusqlite = "0.10" [dev-dependencies] rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index abc6c276..2aa64a7c 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -10,9 +10,9 @@ serde = "0.9" serde_json = "0.9" serde_derive = "0.9" r2d2 = "0.7" -diesel = { version = "0.10", features = ["sqlite"] } -diesel_codegen = { version = "0.10", features = ["sqlite"] } -r2d2-diesel = { version = "0.10", features = ["sqlite"] } +diesel = { version = "0.12", features = ["sqlite"] } +diesel_codegen = { version = "0.12", features = ["sqlite"] } +r2d2-diesel = "0.12" [dependencies.rocket_contrib] path = "../../contrib" From b49c89af7a0dbc9346b5bbe4acef11507610c36b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 15:38:51 -0700 Subject: [PATCH 067/297] Rename UncasedAscii and UncasedAsciiRef to Uncased and UncasedStr. --- lib/src/config/mod.rs | 2 +- lib/src/http/header.rs | 22 ++-- lib/src/http/media_type.rs | 10 +- lib/src/http/method.rs | 3 +- lib/src/http/mod.rs | 2 +- lib/src/http/{ascii.rs => uncased.rs} | 144 +++++++++++++------------- 6 files changed, 91 insertions(+), 92 deletions(-) rename lib/src/http/{ascii.rs => uncased.rs} (59%) diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index d7caf835..401eeec7 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -172,7 +172,7 @@ use self::Environment::*; use self::environment::CONFIG_ENV; use self::toml_ext::parse_simple_toml_value; use logger::{self, LoggingLevel}; -use http::ascii::uncased_eq; +use http::uncased::uncased_eq; static INIT: Once = ONCE_INIT; static mut CONFIG: Option = None; diff --git a/lib/src/http/header.rs b/lib/src/http/header.rs index 2d203e13..24685b7d 100644 --- a/lib/src/http/header.rs +++ b/lib/src/http/header.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use std::borrow::{Borrow, Cow}; use std::fmt; -use http::ascii::{UncasedAscii, UncasedAsciiRef}; +use http::uncased::{Uncased, UncasedStr}; /// Simple representation of an HTTP header. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Header<'h> { /// The name of the header. - pub name: UncasedAscii<'h>, + pub name: Uncased<'h>, /// The value of the header. pub value: Cow<'h, str>, } @@ -46,7 +46,7 @@ impl<'h> Header<'h> { where N: Into>, V: Into> { Header { - name: UncasedAscii::new(name), + name: Uncased::new(name), value: value.into() } } @@ -109,7 +109,7 @@ impl<'h> fmt::Display for Header<'h> { /// A collection of headers, mapping a header name to its many ordered values. #[derive(Clone, Debug, PartialEq, Default)] pub struct HeaderMap<'h> { - headers: HashMap, Vec>> + headers: HashMap, Vec>> } impl<'h> HeaderMap<'h> { @@ -134,7 +134,7 @@ impl<'h> HeaderMap<'h> { /// ``` #[inline] pub fn contains(&self, name: &str) -> bool { - self.headers.get(name.into() : &UncasedAsciiRef).is_some() + self.headers.get(name.into() : &UncasedStr).is_some() } /// Returns the number of _values_ stored in the map. @@ -199,7 +199,7 @@ impl<'h> HeaderMap<'h> { #[inline] pub fn get<'a>(&'a self, name: &str) -> impl Iterator { self.headers - .get(name.into() : &UncasedAsciiRef) + .get(name.into() : &UncasedStr) .into_iter() .flat_map(|values| values.iter().map(|val| val.borrow())) } @@ -237,7 +237,7 @@ impl<'h> HeaderMap<'h> { /// ``` #[inline] pub fn get_one<'a>(&'a self, name: &str) -> Option<&'a str> { - self.headers.get(name.into() : &UncasedAsciiRef) + self.headers.get(name.into() : &UncasedStr) .and_then(|values| { if values.len() >= 1 { Some(values[0].borrow()) } else { None } @@ -329,7 +329,7 @@ impl<'h> HeaderMap<'h> { pub fn replace_all<'n, 'v: 'h, H>(&mut self, name: H, values: Vec>) where 'n: 'h, H: Into> { - self.headers.insert(UncasedAscii::new(name), values); + self.headers.insert(Uncased::new(name), values); } /// Adds `header` into the map. If a header with `header.name` was @@ -404,7 +404,7 @@ impl<'h> HeaderMap<'h> { pub fn add_all<'n, H>(&mut self, name: H, values: &mut Vec>) where 'n:'h, H: Into> { - self.headers.entry(UncasedAscii::new(name)) + self.headers.entry(Uncased::new(name)) .or_insert(vec![]) .append(values) } @@ -427,7 +427,7 @@ impl<'h> HeaderMap<'h> { /// assert_eq!(map.len(), 1); #[inline(always)] pub fn remove(&mut self, name: &str) { - self.headers.remove(name.into() : &UncasedAsciiRef); + self.headers.remove(name.into() : &UncasedStr); } /// Removes all of the headers stored in this map and returns a vector @@ -504,7 +504,7 @@ impl<'h> HeaderMap<'h> { /// should likely not be used. #[inline] pub(crate) fn into_iter_raw(self) - -> impl Iterator, Vec>)> { + -> impl Iterator, Vec>)> { self.headers.into_iter() } } diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index a354046a..081b330d 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -4,7 +4,7 @@ use std::fmt; use std::hash::{Hash, Hasher}; use http::IntoCollection; -use http::ascii::{uncased_eq, UncasedAsciiRef}; +use http::uncased::{uncased_eq, UncasedStr}; use http::parse::{IndexedStr, parse_media_type}; use smallvec::SmallVec; @@ -193,7 +193,7 @@ impl MediaType { known_extensions!(from_extension); /// Returns the top-level type for this media type. The return type, - /// `UncasedAsciiRef`, has caseless equality comparison and hashing. + /// `UncasedStr`, has caseless equality comparison and hashing. /// /// # Example /// @@ -206,12 +206,12 @@ impl MediaType { /// assert_eq!(plain.top(), "Text"); /// ``` #[inline] - pub fn top(&self) -> &UncasedAsciiRef { + pub fn top(&self) -> &UncasedStr { self.top.to_str(self.source.as_ref()).into() } /// Returns the subtype for this media type. The return type, - /// `UncasedAsciiRef`, has caseless equality comparison and hashing. + /// `UncasedStr`, has caseless equality comparison and hashing. /// /// # Example /// @@ -224,7 +224,7 @@ impl MediaType { /// assert_eq!(plain.sub(), "pLaIn"); /// ``` #[inline] - pub fn sub(&self) -> &UncasedAsciiRef { + pub fn sub(&self) -> &UncasedStr { self.sub.to_str(self.source.as_ref()).into() } diff --git a/lib/src/http/method.rs b/lib/src/http/method.rs index 529e261d..a61c9128 100644 --- a/lib/src/http/method.rs +++ b/lib/src/http/method.rs @@ -1,10 +1,9 @@ use std::fmt; use std::str::FromStr; - use error::Error; use http::hyper; -use http::ascii::uncased_eq; +use http::uncased::uncased_eq; use self::Method::*; diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 94999693..62e0780b 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -23,7 +23,7 @@ pub(crate) mod parse; // We need to export these for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) -#[doc(hidden)] pub mod ascii; +pub mod uncased; #[doc(hidden)] pub use self::parse::IndexedStr; #[doc(hidden)] pub use self::media_type::MediaParams; diff --git a/lib/src/http/ascii.rs b/lib/src/http/uncased.rs similarity index 59% rename from lib/src/http/ascii.rs rename to lib/src/http/uncased.rs index 4f25318f..1a7dc2f1 100644 --- a/lib/src/http/ascii.rs +++ b/lib/src/http/uncased.rs @@ -15,64 +15,64 @@ use std::fmt; /// created from an `&str` as follows: /// /// ```rust,ignore -/// use rocket::http::ascii::UncasedAsciiRef; +/// use rocket::http::ascii::UncasedStr; /// -/// let ascii_ref: &UncasedAsciiRef = "Hello, world!".into(); +/// let ascii_ref: &UncasedStr = "Hello, world!".into(); /// ``` #[derive(Debug)] -pub struct UncasedAsciiRef(str); +pub struct UncasedStr(str); -impl UncasedAsciiRef { +impl UncasedStr { pub fn as_str(&self) -> &str { &self.0 } } -impl PartialEq for UncasedAsciiRef { +impl PartialEq for UncasedStr { #[inline(always)] - fn eq(&self, other: &UncasedAsciiRef) -> bool { + fn eq(&self, other: &UncasedStr) -> bool { self.0.eq_ignore_ascii_case(&other.0) } } -impl PartialEq for UncasedAsciiRef { +impl PartialEq for UncasedStr { #[inline(always)] fn eq(&self, other: &str) -> bool { self.0.eq_ignore_ascii_case(other) } } -impl PartialEq for str { +impl PartialEq for str { #[inline(always)] - fn eq(&self, other: &UncasedAsciiRef) -> bool { + fn eq(&self, other: &UncasedStr) -> bool { other.0.eq_ignore_ascii_case(self) } } -impl<'a> PartialEq<&'a str> for UncasedAsciiRef { +impl<'a> PartialEq<&'a str> for UncasedStr { #[inline(always)] fn eq(&self, other: & &'a str) -> bool { self.0.eq_ignore_ascii_case(other) } } -impl<'a> PartialEq for &'a str { +impl<'a> PartialEq for &'a str { #[inline(always)] - fn eq(&self, other: &UncasedAsciiRef) -> bool { + fn eq(&self, other: &UncasedStr) -> bool { other.0.eq_ignore_ascii_case(self) } } -impl<'a> From<&'a str> for &'a UncasedAsciiRef { +impl<'a> From<&'a str> for &'a UncasedStr { #[inline(always)] - fn from(string: &'a str) -> &'a UncasedAsciiRef { + fn from(string: &'a str) -> &'a UncasedStr { unsafe { ::std::mem::transmute(string) } } } -impl Eq for UncasedAsciiRef { } +impl Eq for UncasedStr { } -impl Hash for UncasedAsciiRef { +impl Hash for UncasedStr { #[inline(always)] fn hash(&self, hasher: &mut H) { for byte in self.0.bytes() { @@ -81,14 +81,14 @@ impl Hash for UncasedAsciiRef { } } -impl PartialOrd for UncasedAsciiRef { +impl PartialOrd for UncasedStr { #[inline(always)] - fn partial_cmp(&self, other: &UncasedAsciiRef) -> Option { + fn partial_cmp(&self, other: &UncasedStr) -> Option { Some(self.cmp(other)) } } -impl Ord for UncasedAsciiRef { +impl Ord for UncasedStr { fn cmp(&self, other: &Self) -> Ordering { let self_chars = self.0.chars().map(|c| c.to_ascii_lowercase()); let other_chars = other.0.chars().map(|c| c.to_ascii_lowercase()); @@ -96,7 +96,7 @@ impl Ord for UncasedAsciiRef { } } -impl fmt::Display for UncasedAsciiRef { +impl fmt::Display for UncasedStr { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) @@ -105,23 +105,23 @@ impl fmt::Display for UncasedAsciiRef { /// An uncased (case-preserving) ASCII string. #[derive(Clone, Debug)] -pub struct UncasedAscii<'s> { +pub struct Uncased<'s> { pub string: Cow<'s, str> } -impl<'s> UncasedAscii<'s> { +impl<'s> Uncased<'s> { /// Creates a new UncaseAscii string. /// /// # Example /// /// ```rust,ignore - /// use rocket::http::ascii::UncasedAscii; + /// use rocket::http::ascii::Uncased; /// /// let uncased_ascii = UncasedAScii::new("Content-Type"); /// ``` #[inline(always)] - pub fn new>>(string: S) -> UncasedAscii<'s> { - UncasedAscii { string: string.into() } + pub fn new>>(string: S) -> Uncased<'s> { + Uncased { string: string.into() } } /// Converts `self` into an owned `String`, allocating if necessary, @@ -144,115 +144,115 @@ impl<'s> UncasedAscii<'s> { } } -impl<'a> Deref for UncasedAscii<'a> { - type Target = UncasedAsciiRef; +impl<'a> Deref for Uncased<'a> { + type Target = UncasedStr; #[inline(always)] - fn deref(&self) -> &UncasedAsciiRef { + fn deref(&self) -> &UncasedStr { self.as_str().into() } } -impl<'a> AsRef for UncasedAscii<'a>{ +impl<'a> AsRef for Uncased<'a>{ #[inline(always)] - fn as_ref(&self) -> &UncasedAsciiRef { + fn as_ref(&self) -> &UncasedStr { self.as_str().into() } } -impl<'a> Borrow for UncasedAscii<'a> { +impl<'a> Borrow for Uncased<'a> { #[inline(always)] - fn borrow(&self) -> &UncasedAsciiRef { + fn borrow(&self) -> &UncasedStr { self.as_str().into() } } -impl<'s, 'c: 's> From<&'c str> for UncasedAscii<'s> { +impl<'s, 'c: 's> From<&'c str> for Uncased<'s> { #[inline(always)] fn from(string: &'c str) -> Self { - UncasedAscii::new(string) + Uncased::new(string) } } -impl From for UncasedAscii<'static> { +impl From for Uncased<'static> { #[inline(always)] fn from(string: String) -> Self { - UncasedAscii::new(string) + Uncased::new(string) } } -impl<'s, 'c: 's> From> for UncasedAscii<'s> { +impl<'s, 'c: 's> From> for Uncased<'s> { #[inline(always)] fn from(string: Cow<'c, str>) -> Self { - UncasedAscii::new(string) + Uncased::new(string) } } -impl<'s, 'c: 's, T: Into>> From for UncasedAscii<'s> { +impl<'s, 'c: 's, T: Into>> From for Uncased<'s> { #[inline(always)] default fn from(string: T) -> Self { - UncasedAscii::new(string) + Uncased::new(string) } } -impl<'a, 'b> PartialOrd> for UncasedAscii<'a> { +impl<'a, 'b> PartialOrd> for Uncased<'a> { #[inline(always)] - fn partial_cmp(&self, other: &UncasedAscii<'b>) -> Option { + fn partial_cmp(&self, other: &Uncased<'b>) -> Option { self.as_ref().partial_cmp(other.as_ref()) } } -impl<'a> Ord for UncasedAscii<'a> { +impl<'a> Ord for Uncased<'a> { fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) } } -impl<'s> fmt::Display for UncasedAscii<'s> { +impl<'s> fmt::Display for Uncased<'s> { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.string.fmt(f) } } -impl<'a, 'b> PartialEq> for UncasedAscii<'a> { +impl<'a, 'b> PartialEq> for Uncased<'a> { #[inline(always)] - fn eq(&self, other: &UncasedAscii<'b>) -> bool { + fn eq(&self, other: &Uncased<'b>) -> bool { self.as_ref().eq(other.as_ref()) } } -impl<'a> PartialEq for UncasedAscii<'a> { +impl<'a> PartialEq for Uncased<'a> { #[inline(always)] fn eq(&self, other: &str) -> bool { self.as_ref().eq(other) } } -impl<'b> PartialEq> for str { +impl<'b> PartialEq> for str { #[inline(always)] - fn eq(&self, other: &UncasedAscii<'b>) -> bool { + fn eq(&self, other: &Uncased<'b>) -> bool { other.as_ref().eq(self) } } -impl<'a, 'b> PartialEq<&'b str> for UncasedAscii<'a> { +impl<'a, 'b> PartialEq<&'b str> for Uncased<'a> { #[inline(always)] fn eq(&self, other: & &'b str) -> bool { self.as_ref().eq(other) } } -impl<'a, 'b> PartialEq> for &'a str { +impl<'a, 'b> PartialEq> for &'a str { #[inline(always)] - fn eq(&self, other: &UncasedAscii<'b>) -> bool { + fn eq(&self, other: &Uncased<'b>) -> bool { other.as_ref().eq(self) } } -impl<'s> Eq for UncasedAscii<'s> { } +impl<'s> Eq for Uncased<'s> { } -impl<'s> Hash for UncasedAscii<'s> { +impl<'s> Hash for Uncased<'s> { #[inline(always)] fn hash(&self, hasher: &mut H) { self.as_ref().hash(hasher) @@ -264,14 +264,14 @@ impl<'s> Hash for UncasedAscii<'s> { /// does it in a much faster way. #[inline(always)] pub fn uncased_eq, S2: AsRef>(s1: S1, s2: S2) -> bool { - let ascii_ref_1: &UncasedAsciiRef = s1.as_ref().into(); - let ascii_ref_2: &UncasedAsciiRef = s2.as_ref().into(); + let ascii_ref_1: &UncasedStr = s1.as_ref().into(); + let ascii_ref_2: &UncasedStr = s2.as_ref().into(); ascii_ref_1 == ascii_ref_2 } #[cfg(test)] mod tests { - use super::UncasedAscii; + use super::Uncased; use std::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; @@ -289,8 +289,8 @@ mod tests { for i in 0..strings.len() { for j in i..strings.len() { let (str_a, str_b) = (strings[i], strings[j]); - let ascii_a = UncasedAscii::from(str_a); - let ascii_b = UncasedAscii::from(str_b); + let ascii_a = Uncased::from(str_a); + let ascii_b = Uncased::from(str_b); assert_eq!(ascii_a, ascii_b); assert_eq!(hash(&ascii_a), hash(&ascii_b)); assert_eq!(ascii_a, str_a); @@ -312,20 +312,20 @@ mod tests { #[test] fn test_case_cmp() { - assert!(UncasedAscii::from("foobar") == UncasedAscii::from("FOOBAR")); - assert!(UncasedAscii::from("a") == UncasedAscii::from("A")); + assert!(Uncased::from("foobar") == Uncased::from("FOOBAR")); + assert!(Uncased::from("a") == Uncased::from("A")); - assert!(UncasedAscii::from("a") < UncasedAscii::from("B")); - assert!(UncasedAscii::from("A") < UncasedAscii::from("B")); - assert!(UncasedAscii::from("A") < UncasedAscii::from("b")); + assert!(Uncased::from("a") < Uncased::from("B")); + assert!(Uncased::from("A") < Uncased::from("B")); + assert!(Uncased::from("A") < Uncased::from("b")); - assert!(UncasedAscii::from("aa") > UncasedAscii::from("a")); - assert!(UncasedAscii::from("aa") > UncasedAscii::from("A")); - assert!(UncasedAscii::from("AA") > UncasedAscii::from("a")); - assert!(UncasedAscii::from("AA") > UncasedAscii::from("a")); - assert!(UncasedAscii::from("Aa") > UncasedAscii::from("a")); - assert!(UncasedAscii::from("Aa") > UncasedAscii::from("A")); - assert!(UncasedAscii::from("aA") > UncasedAscii::from("a")); - assert!(UncasedAscii::from("aA") > UncasedAscii::from("A")); + assert!(Uncased::from("aa") > Uncased::from("a")); + assert!(Uncased::from("aa") > Uncased::from("A")); + assert!(Uncased::from("AA") > Uncased::from("a")); + assert!(Uncased::from("AA") > Uncased::from("a")); + assert!(Uncased::from("Aa") > Uncased::from("a")); + assert!(Uncased::from("Aa") > Uncased::from("A")); + assert!(Uncased::from("aA") > Uncased::from("a")); + assert!(Uncased::from("aA") > Uncased::from("A")); } } From 709acf18a4fb67b98e02295381e4c3a969acb061 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 17:52:02 -0700 Subject: [PATCH 068/297] Initial implementation of RawStr. --- lib/src/http/mod.rs | 2 + lib/src/http/raw_str.rs | 234 ++++++++++++++++++++++++++++++++++++++++ lib/src/http/uncased.rs | 1 + lib/src/http/uri.rs | 2 +- 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 lib/src/http/raw_str.rs diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 62e0780b..1225a01f 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -18,6 +18,7 @@ mod content_type; mod status; mod header; mod accept; +mod raw_str; pub(crate) mod parse; @@ -32,6 +33,7 @@ pub use self::content_type::ContentType; pub use self::accept::{Accept, WeightedMediaType}; pub use self::status::{Status, StatusClass}; pub use self::header::{Header, HeaderMap}; +pub use self::raw_str::RawStr; pub use self::media_type::MediaType; pub use self::cookies::*; diff --git a/lib/src/http/raw_str.rs b/lib/src/http/raw_str.rs new file mode 100644 index 00000000..ba4a5ede --- /dev/null +++ b/lib/src/http/raw_str.rs @@ -0,0 +1,234 @@ +use std::ops::{Deref, DerefMut}; +use std::borrow::Cow; +use std::convert::AsRef; +use std::cmp::Ordering; +use std::ascii::AsciiExt; +use std::str::Utf8Error; +use std::fmt; + +use url; + +use http::uncased::UncasedStr; + +/// A reference to a raw HTTP string. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RawStr(str); + +impl RawStr { + #[inline(always)] + pub fn from_str<'a>(string: &'a str) -> &'a RawStr { + string.into() + } + + #[inline(always)] + pub fn as_str(&self) -> &str { + self + } + + #[inline(always)] + pub fn as_uncased_str(&self) -> &UncasedStr { + self.as_str().into() + } + + /// Returns a URL-decoded version of the string. If the percent encoded + /// values are not valid UTF-8, an `Err` is returned. + #[inline] + pub fn percent_decode(&self) -> Result, Utf8Error> { + url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8() + } + + /// Returns a URL-decoded version of the path. Any invalid UTF-8 + /// percent-encoded byte sequences will be replaced � U+FFFD, the + /// replacement character. + #[inline] + pub fn percent_decode_lossy(&self) -> Cow { + url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8_lossy() + } + + /// Do some HTML escaping. + /// + /// # Example + /// + /// Strings with HTML sequences are escaped: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str: &RawStr = "Hi!".into(); + /// let escaped = raw_str.html_escape(); + /// assert_eq!(escaped, "<b>Hi!</b>"); + /// + /// let raw_str: &RawStr = "Hello, world!".into(); + /// let escaped = raw_str.html_escape(); + /// assert_eq!(escaped, "Hello, <i>world!</i>"); + /// ``` + /// + /// Strings without HTML sequences remain untouched: + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str: &RawStr = "Hello!".into(); + /// let escaped = raw_str.html_escape(); + /// assert_eq!(escaped, "Hello!"); + /// + /// let raw_str: &RawStr = "大阪".into(); + /// let escaped = raw_str.html_escape(); + /// assert_eq!(escaped, "大阪"); + /// ``` + pub fn html_escape(&self) -> Cow { + let mut escaped = false; + let mut allocated = Vec::new(); // this is allocation free + for c in self.as_bytes() { + match *c { + b'&' | b'<' | b'>' | b'"' | b'\'' | b'/' | b'`' => { + if !escaped { + let i = (c as *const u8 as usize) - (self.as_ptr() as usize); + allocated = Vec::with_capacity(self.len() * 2); + allocated.extend_from_slice(&self.as_bytes()[..i]); + } + + match *c { + b'&' => allocated.extend_from_slice(b"&"), + b'<' => allocated.extend_from_slice(b"<"), + b'>' => allocated.extend_from_slice(b">"), + b'"' => allocated.extend_from_slice(b"""), + b'\'' => allocated.extend_from_slice(b"'"), + b'/' => allocated.extend_from_slice(b"/"), + // Old versions of IE treat a ` as a '. + b'`' => allocated.extend_from_slice(b"`"), + _ => unreachable!() + } + + escaped = true; + } + _ if escaped => allocated.push(*c), + _ => { } + } + } + + if escaped { + unsafe { Cow::Owned(String::from_utf8_unchecked(allocated)) } + } else { + Cow::Borrowed(self.as_str()) + } + } +} + +impl<'a> From<&'a str> for &'a RawStr { + #[inline(always)] + fn from(string: &'a str) -> &'a RawStr { + unsafe { ::std::mem::transmute(string) } + } +} + +impl PartialEq for RawStr { + #[inline(always)] + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq for RawStr { + #[inline(always)] + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } +} + +impl<'a> PartialEq for &'a RawStr { + #[inline(always)] + fn eq(&self, other: &String) -> bool { + self.as_str() == other.as_str() + } +} + +impl PartialOrd for RawStr { + #[inline(always)] + fn partial_cmp(&self, other: &str) -> Option { + (self as &str).partial_cmp(other) + } +} + +impl AsRef for RawStr { + #[inline(always)] + fn as_ref(&self) -> &str { + self + } +} + +impl AsRef<[u8]> for RawStr { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl ToString for RawStr { + #[inline(always)] + fn to_string(&self) -> String { + String::from(self.as_str()) + } +} + +impl AsciiExt for RawStr { + type Owned = String; + + #[inline(always)] + fn is_ascii(&self) -> bool { (self as &str).is_ascii() } + + #[inline(always)] + fn to_ascii_uppercase(&self) -> String { (self as &str).to_ascii_uppercase() } + + #[inline(always)] + fn to_ascii_lowercase(&self) -> String { (self as &str).to_ascii_lowercase() } + + #[inline(always)] + fn make_ascii_uppercase(&mut self) { (self as &mut str).make_ascii_uppercase() } + + #[inline(always)] + fn make_ascii_lowercase(&mut self) { (self as &mut str).make_ascii_lowercase() } + + #[inline(always)] + fn eq_ignore_ascii_case(&self, o: &RawStr) -> bool { + (self as &str).eq_ignore_ascii_case(o as &str) + } +} + +impl Deref for RawStr { + type Target = str; + + #[inline(always)] + fn deref(&self) -> &str { + &self.0 + } +} + +impl DerefMut for RawStr { + #[inline(always)] + fn deref_mut(&mut self) -> &mut str { + &mut self.0 + } +} + +impl fmt::Display for RawStr { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::RawStr; + + #[test] + fn can_compare() { + let raw_str = RawStr::from_str("abc"); + assert_eq!(raw_str, "abc"); + assert_eq!("abc", raw_str.as_str()); + assert_eq!(raw_str, RawStr::from_str("abc")); + assert_eq!(raw_str, "abc".to_string()); + assert_eq!("abc".to_string(), raw_str.as_str()); + } +} diff --git a/lib/src/http/uncased.rs b/lib/src/http/uncased.rs index 1a7dc2f1..b156f01a 100644 --- a/lib/src/http/uncased.rs +++ b/lib/src/http/uncased.rs @@ -23,6 +23,7 @@ use std::fmt; pub struct UncasedStr(str); impl UncasedStr { + #[inline(always)] pub fn as_str(&self) -> &str { &self.0 } diff --git a/lib/src/http/uri.rs b/lib/src/http/uri.rs index a38630ff..d57557ca 100644 --- a/lib/src/http/uri.rs +++ b/lib/src/http/uri.rs @@ -216,7 +216,7 @@ impl<'a> URI<'a> { /// let decoded_path = URI::percent_decode(uri.path().as_bytes()).expect("decoded"); /// assert_eq!(decoded_path, "/Hello, world!"); /// ``` - pub fn percent_decode(string: &[u8]) -> Result, Utf8Error> { + pub fn percent_decode(string: &[u8]) -> Result, Utf8Error> { let decoder = url::percent_encoding::percent_decode(string); decoder.decode_utf8() } From df19ef74db07a951d11aaaaaa119e3e0b0b8b296 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 18:15:15 -0700 Subject: [PATCH 069/297] Add RawStr::url_decode. --- lib/src/http/raw_str.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/src/http/raw_str.rs b/lib/src/http/raw_str.rs index ba4a5ede..5c81e0a1 100644 --- a/lib/src/http/raw_str.rs +++ b/lib/src/http/raw_str.rs @@ -30,17 +30,40 @@ impl RawStr { self.as_str().into() } - /// Returns a URL-decoded version of the string. If the percent encoded - /// values are not valid UTF-8, an `Err` is returned. + /// Returns a URL-decoded version of the string. This is identical to + /// percent decoding except that '+' characters are converted into spaces. + /// This is the encoding used by form values. + /// + /// If the percent encoded values are not valid UTF-8, an `Err` is returned. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::RawStr; + /// + /// let raw_str: &RawStr = "Hello%2C+world%21".into(); + /// let decoded = raw_str.url_decode(); + /// assert_eq!(decoded, Ok("Hello, world!".to_string())); + /// ``` #[inline] + pub fn url_decode(&self) -> Result { + let replaced = self.replace("+", " "); + RawStr::from_str(replaced.as_str()) + .percent_decode() + .map(|cow| cow.into_owned()) + } + + /// Returns a percent-decoded version of the string. If the percent encoded + /// values are not valid UTF-8, an `Err` is returned. + #[inline(always)] pub fn percent_decode(&self) -> Result, Utf8Error> { url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8() } - /// Returns a URL-decoded version of the path. Any invalid UTF-8 + /// Returns a percent-decoded version of the string. Any invalid UTF-8 /// percent-encoded byte sequences will be replaced � U+FFFD, the /// replacement character. - #[inline] + #[inline(always)] pub fn percent_decode_lossy(&self) -> Cow { url::percent_encoding::percent_decode(self.as_bytes()).decode_utf8_lossy() } From 10306c3b7ed218684930939b39404c93d5d0c5e3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 18:15:36 -0700 Subject: [PATCH 070/297] Clarify segment handling for '..'. --- lib/src/request/param.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/request/param.rs b/lib/src/request/param.rs index b7049725..bf3f9807 100644 --- a/lib/src/request/param.rs +++ b/lib/src/request/param.rs @@ -291,7 +291,7 @@ impl<'a> FromSegments<'a> for Segments<'a> { /// For security purposes, if a segment meets any of the following conditions, /// an `Err` is returned indicating the condition met: /// -/// * Decoded segment starts with any of: `.`, `*` +/// * Decoded segment starts with any of: `.` (except `..`), `*` /// * Decoded segment ends with any of: `:`, `>`, `<` /// * Decoded segment contains any of: `/` /// * On Windows, decoded segment contains any of: '\' From f7bc1ce24fff9c4636daaccf35e94e3bae6437be Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 19:47:14 -0700 Subject: [PATCH 071/297] Update codegen for latest nightly. --- codegen/build.rs | 4 ++-- codegen/src/utils/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codegen/build.rs b/codegen/build.rs index b94bd736..4946eabd 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -8,8 +8,8 @@ use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. -const MIN_DATE: &'static str = "2017-03-22"; -const MIN_VERSION: &'static str = "1.17.0-nightly"; +const MIN_DATE: &'static str = "2017-03-30"; +const MIN_VERSION: &'static str = "1.18.0-nightly"; // Convenience macro for writing to stderr. macro_rules! printerr { diff --git a/codegen/src/utils/mod.rs b/codegen/src/utils/mod.rs index 41664c53..cb9960d6 100644 --- a/codegen/src/utils/mod.rs +++ b/codegen/src/utils/mod.rs @@ -17,7 +17,7 @@ use syntax::parse::token::Token; use syntax::tokenstream::TokenTree; use syntax::ast::{Item, Expr}; use syntax::ext::base::{Annotatable, ExtCtxt}; -use syntax::codemap::{spanned, Span, Spanned, DUMMY_SP}; +use syntax::codemap::{Span, Spanned, DUMMY_SP}; use syntax::ext::quote::rt::ToTokens; use syntax::print::pprust::item_to_string; use syntax::ptr::P; @@ -26,7 +26,7 @@ use syntax::ast::{Attribute, Lifetime, LifetimeDef, Ty}; use syntax::attr::HasAttrs; pub fn span(t: T, span: Span) -> Spanned { - spanned(span.lo, span.hi, t) + Spanned { node: t, span: span } } pub fn sep_by_tok(ecx: &ExtCtxt, things: &[T], token: Token) -> Vec From f57d984e2e544dfd2cae6b662f4c9ad87f277ea2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 19:49:43 -0700 Subject: [PATCH 072/297] New version: 0.2.4. --- CHANGELOG.md | 7 +++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3bce7c..872c49cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version 0.2.4 (Mar 30, 2017) + +## Codegen + + * Codegen was updated for `2017-03-30` nightly. + * Minimum required `rustc` is `1.18.0-nightly (2017-03-30)`. + # Version 0.2.3 (Mar 22, 2017) ## Fixes diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 19b01d24..b6b4dbde 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.3" +version = "0.2.4" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.3", path = "../lib/" } +rocket = { version = "0.2.4", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index d1becfc1..493df989 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.3" +version = "0.2.4" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -22,7 +22,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.3", path = "../lib/" } +rocket = { version = "0.2.4", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 093fb60c..366c945c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.3" +version = "0.2.4" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -38,7 +38,7 @@ features = ["percent-encode", "secure"] [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.3", path = "../codegen" } +rocket_codegen = { version = "0.2.4", path = "../codegen" } [build-dependencies] ansi_term = "0.9" From 0c44e4464151d736ea2806a1ff8321919e6b9aa1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 23:06:53 -0700 Subject: [PATCH 073/297] Use the `RawStr` type for all form raw strings. This is a breaking change. This commit introduces `RawStr` to forms. In particular, after this commit, the `&str` type no longer implements `FromFormValue`, and so it cannot be used as a field in forms. Instad, the `&RawStr` can be used. The `FormItems` iterator now returns an `(&RawStr, &RawStr)` pair. --- codegen/src/decorators/derive_form.rs | 27 ++++---- codegen/src/decorators/route.rs | 2 +- codegen/tests/run-pass/complete-decorator.rs | 4 +- codegen/tests/run-pass/derive_form.rs | 21 +++--- contrib/src/uuid.rs | 6 +- examples/extended_validation/src/main.rs | 18 ++--- examples/extended_validation/src/tests.rs | 8 +-- examples/form_kitchen_sink/src/main.rs | 9 +-- examples/forms/src/main.rs | 8 +-- examples/query_params/src/main.rs | 4 +- lib/src/lib.rs | 1 + lib/src/request/form/form_items.rs | 61 ++++++++++++----- lib/src/request/form/from_form_value.rs | 69 +++++++++++--------- lib/src/request/form/mod.rs | 8 ++- lib/src/rocket.rs | 9 +-- 15 files changed, 145 insertions(+), 110 deletions(-) diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index 13bc34f7..0c0408e7 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -68,7 +68,11 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, is_unsafe: false, supports_unions: false, span: span, - attributes: Vec::new(), + // We add this attribute because some `FromFormValue` implementations + // can't fail. This is indicated via the `!` type. Rust checks if a + // match is made with something of that type, and since we always emit + // an `Err` match, we'll get this lint warning. + attributes: vec![quote_attr!(ecx, #[allow(unreachable_code)])], path: ty::Path { path: vec!["rocket", "request", "FromForm"], lifetime: lifetime_var, @@ -178,7 +182,8 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct let id_str = ident_string.as_str(); arms.push(quote_tokens!(cx, $id_str => { - $ident = match ::rocket::request::FromFormValue::from_form_value(v) { + let r = ::rocket::http::RawStr::from_str(v); + $ident = match ::rocket::request::FromFormValue::from_form_value(r) { Ok(v) => Some(v), Err(e) => { println!(" => Error parsing form val '{}': {:?}", @@ -194,9 +199,9 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // and use the $arms generated above. stmts.push(quote_stmt!(cx, for (k, v) in $arg { - match k { + match k.as_str() { $arms - field if field == "_method" => { + "_method" => { /* This is a Rocket-specific field. If the user hasn't asked * for it, just let it go by without error. This should stay * in sync with Rocket::preprocess. */ @@ -214,19 +219,13 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // that each parameter actually is Some() or has a default value. let mut failure_conditions = vec![]; - // Start with `false` in case there are no fields. - failure_conditions.push(quote_tokens!(cx, false)); - for &(ref ident, ref ty) in (&fields_and_types).iter() { - // Pushing an "||" (or) between every condition. - failure_conditions.push(quote_tokens!(cx, ||)); - failure_conditions.push(quote_tokens!(cx, if $ident.is_none() && <$ty as ::rocket::request::FromFormValue>::default().is_none() { println!(" => '{}' did not parse.", stringify!($ident)); - true - } else { false } + $return_err_stmt; + } )); } @@ -245,9 +244,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // the structure. let self_ident = substr.type_ident; let final_block = quote_block!(cx, { - if $failure_conditions { - $return_err_stmt; - } + $failure_conditions Ok($self_ident { $result_fields }) }); diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index e00e5fdd..fc562abb 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -81,7 +81,7 @@ impl RouteGenerateExt for RouteParams { Err(_) => return ::rocket::Outcome::Forward(_data) }; - if !items.exhausted() { + if !items.exhaust() { println!(" => The query string {:?} is malformed.", $form_string); return ::rocket::Outcome::Failure(::rocket::http::Status::BadRequest); } diff --git a/codegen/tests/run-pass/complete-decorator.rs b/codegen/tests/run-pass/complete-decorator.rs index 1819ec5e..5fcc75c3 100644 --- a/codegen/tests/run-pass/complete-decorator.rs +++ b/codegen/tests/run-pass/complete-decorator.rs @@ -3,12 +3,12 @@ extern crate rocket; -use rocket::http::Cookies; +use rocket::http::{Cookies, RawStr}; use rocket::request::Form; #[derive(FromForm)] struct User<'a> { - name: &'a str, + name: &'a RawStr, nickname: String, } diff --git a/codegen/tests/run-pass/derive_form.rs b/codegen/tests/run-pass/derive_form.rs index 993be36d..08c2756b 100644 --- a/codegen/tests/run-pass/derive_form.rs +++ b/codegen/tests/run-pass/derive_form.rs @@ -4,6 +4,7 @@ extern crate rocket; use rocket::request::{FromForm, FromFormValue, FormItems}; +use rocket::http::RawStr; #[derive(Debug, PartialEq, FromForm)] struct TodoTask { @@ -20,8 +21,8 @@ enum FormOption { impl<'v> FromFormValue<'v> for FormOption { type Error = &'v str; - fn from_form_value(v: &'v str) -> Result { - let variant = match v { + fn from_form_value(v: &'v RawStr) -> Result { + let variant = match v.as_str() { "a" => FormOption::A, "b" => FormOption::B, "c" => FormOption::C, @@ -37,19 +38,19 @@ struct FormInput<'r> { checkbox: bool, number: usize, radio: FormOption, - password: &'r str, + password: &'r RawStr, textarea: String, select: FormOption, } #[derive(Debug, PartialEq, FromForm)] struct DefaultInput<'r> { - arg: Option<&'r str>, + arg: Option<&'r RawStr>, } #[derive(Debug, PartialEq, FromForm)] struct ManualMethod<'r> { - _method: Option<&'r str>, + _method: Option<&'r RawStr>, done: bool } @@ -61,13 +62,13 @@ struct UnpresentCheckbox { #[derive(Debug, PartialEq, FromForm)] struct UnpresentCheckboxTwo<'r> { checkbox: bool, - something: &'r str + something: &'r RawStr } fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option { let mut items = FormItems::from(string); let result = T::from_form_items(items.by_ref()); - if !items.exhausted() { + if !items.exhaust() { panic!("Invalid form input."); } @@ -103,7 +104,7 @@ fn main() { checkbox: false, number: 10, radio: FormOption::C, - password: "testing", + password: "testing".into(), textarea: "".to_string(), select: FormOption::A, })); @@ -117,7 +118,7 @@ fn main() { // Ensure _method can be captured if desired. let manual: Option = parse("_method=put&done=true"); assert_eq!(manual, Some(ManualMethod { - _method: Some("put"), + _method: Some("put".into()), done: true })); @@ -138,6 +139,6 @@ fn main() { let manual: Option = parse("something=hello"); assert_eq!(manual, Some(UnpresentCheckboxTwo { checkbox: false, - something: "hello" + something: "hello".into() })); } diff --git a/contrib/src/uuid.rs b/contrib/src/uuid.rs index 795be11e..a12232b4 100644 --- a/contrib/src/uuid.rs +++ b/contrib/src/uuid.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use std::ops::Deref; use rocket::request::{FromParam, FromFormValue}; +use rocket::http::RawStr; pub use self::uuid_ext::ParseError as UuidParseError; @@ -89,11 +90,12 @@ impl<'a> FromParam<'a> for UUID { } impl<'v> FromFormValue<'v> for UUID { - type Error = &'v str; + type Error = &'v RawStr; /// A value is successfully parsed if `form_value` is a properly formatted /// UUID. Otherwise, the raw form value is returned. - fn from_form_value(form_value: &'v str) -> Result { + #[inline(always)] + fn from_form_value(form_value: &'v RawStr) -> Result { form_value.parse().map_err(|_| form_value) } } diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index 347ac9da..75963d59 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -4,11 +4,11 @@ extern crate rocket; mod files; -#[cfg(test)] -mod tests; +#[cfg(test)] mod tests; use rocket::response::Redirect; use rocket::request::{Form, FromFormValue}; +use rocket::http::RawStr; #[derive(Debug)] struct StrongPassword<'r>(&'r str); @@ -18,7 +18,7 @@ struct AdultAge(isize); #[derive(FromForm)] struct UserLogin<'r> { - username: &'r str, + username: &'r RawStr, password: Result, &'static str>, age: Result, } @@ -26,11 +26,11 @@ struct UserLogin<'r> { impl<'v> FromFormValue<'v> for StrongPassword<'v> { type Error = &'static str; - fn from_form_value(v: &'v str) -> Result { + fn from_form_value(v: &'v RawStr) -> Result { if v.len() < 8 { - Err("Too short!") + Err("too short!") } else { - Ok(StrongPassword(v)) + Ok(StrongPassword(v.as_str())) } } } @@ -38,15 +38,15 @@ impl<'v> FromFormValue<'v> for StrongPassword<'v> { impl<'v> FromFormValue<'v> for AdultAge { type Error = &'static str; - fn from_form_value(v: &'v str) -> Result { + fn from_form_value(v: &'v RawStr) -> Result { let age = match isize::from_form_value(v) { Ok(v) => v, - Err(_) => return Err("Age value is not a number."), + Err(_) => return Err("value is not a number."), }; match age > 20 { true => Ok(AdultAge(age)), - false => Err("Must be at least 21."), + false => Err("must be at least 21."), } } } diff --git a/examples/extended_validation/src/tests.rs b/examples/extended_validation/src/tests.rs index 46e21717..8d096be4 100644 --- a/examples/extended_validation/src/tests.rs +++ b/examples/extended_validation/src/tests.rs @@ -37,14 +37,14 @@ fn test_invalid_user() { #[test] fn test_invalid_password() { test_login("Sergio", "password1", "30", Status::Ok, "Wrong password!"); - test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: Too short!"); + test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: too short!"); } #[test] fn test_invalid_age() { - test_login("Sergio", "password", "20", Status::Ok, "Must be at least 21."); - test_login("Sergio", "password", "-100", Status::Ok, "Must be at least 21."); - test_login("Sergio", "password", "hi", Status::Ok, "Age value is not a number"); + test_login("Sergio", "password", "20", Status::Ok, "must be at least 21."); + test_login("Sergio", "password", "-100", Status::Ok, "must be at least 21."); + test_login("Sergio", "password", "hi", Status::Ok, "value is not a number"); } fn check_bad_form(form_str: &str, status: Status) { diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index 001c137f..9563b696 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -3,9 +3,10 @@ extern crate rocket; +use std::io; use rocket::request::{Form, FromFormValue}; use rocket::response::NamedFile; -use std::io; +use rocket::http::RawStr; // TODO: Make deriving `FromForm` for this enum possible. #[derive(Debug)] @@ -14,10 +15,10 @@ enum FormOption { } impl<'v> FromFormValue<'v> for FormOption { - type Error = &'v str; + type Error = &'v RawStr; - fn from_form_value(v: &'v str) -> Result { - let variant = match v { + fn from_form_value(v: &'v RawStr) -> Result { + let variant = match v.as_str() { "a" => FormOption::A, "b" => FormOption::B, "c" => FormOption::C, diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 6ab3c601..5f7e4ed9 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -8,12 +8,13 @@ mod files; use rocket::request::Form; use rocket::response::Redirect; +use rocket::http::RawStr; #[derive(FromForm)] struct UserLogin<'r> { - username: &'r str, + username: &'r RawStr, password: String, - age: Result, + age: Result, } #[post("/login", data = "")] @@ -36,9 +37,8 @@ fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result { } } - #[get("/user/")] -fn user_page(username: &str) -> String { +fn user_page(username: String) -> String { format!("This is {}'s page.", username) } diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs index 37059bf6..3c48a4bd 100644 --- a/examples/query_params/src/main.rs +++ b/examples/query_params/src/main.rs @@ -6,8 +6,8 @@ extern crate rocket; #[cfg(test)] mod tests; #[derive(FromForm)] -struct Person<'r> { - name: &'r str, +struct Person { + name: String, age: Option } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c0b9f9c9..f69710f8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -6,6 +6,7 @@ #![feature(type_ascription)] #![feature(lookup_host)] #![feature(plugin)] +#![feature(never_type)] #![plugin(pear_codegen)] diff --git a/lib/src/request/form/form_items.rs b/lib/src/request/form/form_items.rs index daa92650..ab26d0e4 100644 --- a/lib/src/request/form/form_items.rs +++ b/lib/src/request/form/form_items.rs @@ -1,17 +1,20 @@ use memchr::memchr2; +use http::RawStr; + /// Iterator over the key/value pairs of a given HTTP form string. /// /// **Note:** The returned key/value pairs are _not_ URL decoded. To URL decode -/// the raw strings, use `String::from_form_value`: +/// the raw strings, use the +/// [`url_decode`](/rocket/http/struct.RawStr.html#method.url_decode) method: /// /// ```rust /// use rocket::request::{FormItems, FromFormValue}; /// /// let form_string = "greeting=Hello%2C+Mark%21&username=jake%2Fother"; /// for (key, value) in FormItems::from(form_string) { -/// let decoded_value = String::from_form_value(value); -/// match key { +/// let decoded_value = value.url_decode(); +/// match key.as_str() { /// "greeting" => assert_eq!(decoded_value, Ok("Hello, Mark!".into())), /// "username" => assert_eq!(decoded_value, Ok("jake/other".into())), /// _ => unreachable!() @@ -26,7 +29,7 @@ use memchr::memchr2; /// for completion via the [completed](#method.completed) method, which returns /// `true` if the iterator parsed the entire string that was passed to it. The /// iterator can also attempt to parse any remaining contents via -/// [exhausted](#method.exhausted); this method returns `true` if exhaustion +/// [exhaust](#method.exhaust); this method returns `true` if exhaustion /// succeeded. /// /// This iterator guarantees that all valid form strings are parsed to @@ -57,13 +60,20 @@ use memchr::memchr2; /// /// let form_string = "greeting=hello&username=jake"; /// let mut items = FormItems::from(form_string); -/// assert_eq!(items.next(), Some(("greeting", "hello"))); -/// assert_eq!(items.next(), Some(("username", "jake"))); +/// +/// let next = items.next().unwrap(); +/// assert_eq!(next.0, "greeting"); +/// assert_eq!(next.1, "hello"); +/// +/// let next = items.next().unwrap(); +/// assert_eq!(next.0, "username"); +/// assert_eq!(next.1, "jake"); +/// /// assert_eq!(items.next(), None); /// assert!(items.completed()); /// ``` pub struct FormItems<'f> { - string: &'f str, + string: &'f RawStr, next_index: usize } @@ -117,7 +127,7 @@ impl<'f> FormItems<'f> { /// /// assert!(items.next().is_some()); /// assert_eq!(items.completed(), false); - /// assert_eq!(items.exhausted(), true); + /// assert_eq!(items.exhaust(), true); /// assert_eq!(items.completed(), true); /// ``` /// @@ -130,10 +140,11 @@ impl<'f> FormItems<'f> { /// /// assert!(items.next().is_some()); /// assert_eq!(items.completed(), false); - /// assert_eq!(items.exhausted(), false); + /// assert_eq!(items.exhaust(), false); /// assert_eq!(items.completed(), false); /// ``` - pub fn exhausted(&mut self) -> bool { + #[inline] + pub fn exhaust(&mut self) -> bool { while let Some(_) = self.next() { } self.completed() } @@ -167,15 +178,16 @@ impl<'f> FormItems<'f> { /// assert_eq!(items.inner_str(), form_string); /// ``` #[inline] - pub fn inner_str(&self) -> &'f str { + pub fn inner_str(&self) -> &'f RawStr { self.string } } -impl<'f> From<&'f str> for FormItems<'f> { +impl<'f> From<&'f RawStr> for FormItems<'f> { /// Returns an iterator over the key/value pairs in the /// `x-www-form-urlencoded` form `string`. - fn from(string: &'f str) -> FormItems<'f> { + #[inline(always)] + fn from(string: &'f RawStr) -> FormItems<'f> { FormItems { string: string, next_index: 0 @@ -183,8 +195,20 @@ impl<'f> From<&'f str> for FormItems<'f> { } } +impl<'f> From<&'f str> for FormItems<'f> { + /// Returns an iterator over the key/value pairs in the + /// `x-www-form-urlencoded` form `string`. + #[inline(always)] + fn from(string: &'f str) -> FormItems<'f> { + FormItems { + string: string.into(), + next_index: 0 + } + } +} + impl<'f> Iterator for FormItems<'f> { - type Item = (&'f str, &'f str); + type Item = (&'f RawStr, &'f RawStr); fn next(&mut self) -> Option { let s = &self.string[self.next_index..]; @@ -204,7 +228,7 @@ impl<'f> Iterator for FormItems<'f> { }; self.next_index += key.len() + 1 + consumed; - Some((key, value)) + Some((key.into(), value.into())) } } @@ -224,18 +248,19 @@ mod test { let (expected_key, actual_key) = (expected[i].0, results[i].0); let (expected_val, actual_val) = (expected[i].1, results[i].1); - assert!(expected_key == actual_key, + assert!(actual_key == expected_key, "key [{}] mismatch: expected {}, got {}", i, expected_key, actual_key); - assert!(expected_val == actual_val, + assert!(actual_val == expected_val, "val [{}] mismatch: expected {}, got {}", i, expected_val, actual_val); } } else { - assert!(!items.exhausted()); + assert!(!items.exhaust()); } }); + (@bad $string:expr) => (check_form!(@opt $string, None : Option<&[(&str, &str)]>)); ($string:expr, $expected:expr) => (check_form!(@opt $string, Some($expected))); } diff --git a/lib/src/request/form/from_form_value.rs b/lib/src/request/form/from_form_value.rs index 87c7310b..5e765b37 100644 --- a/lib/src/request/form/from_form_value.rs +++ b/lib/src/request/form/from_form_value.rs @@ -1,8 +1,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; use std::str::FromStr; -use error::Error; -use http::uri::URI; +use http::RawStr; /// Trait to create instance of some type from a form value; expected from field /// types in structs deriving `FromForm`. @@ -38,15 +37,16 @@ use http::uri::URI; /// following structure: /// /// ```rust +/// # use rocket::http::RawStr; /// # #[allow(dead_code)] /// struct Person<'r> { /// name: String, -/// age: Result +/// age: Result /// } /// ``` /// -/// The `Err` value in this case is `&str` since `u16::from_form_value` returns -/// a `Result`. +/// The `Err` value in this case is `&RawStr` since `u16::from_form_value` +/// returns a `Result`. /// /// # Provided Implementations /// @@ -67,7 +67,7 @@ use http::uri::URI; /// `"false"`, `"off"`, or not present. In any other case, the raw form /// value is returned in the `Err` value. /// -/// * **str** +/// * **&RawStr** /// /// _This implementation always returns successfully._ /// @@ -106,14 +106,15 @@ use http::uri::URI; /// /// ```rust /// use rocket::request::FromFormValue; +/// use rocket::http::RawStr; /// /// struct AdultAge(usize); /// /// impl<'v> FromFormValue<'v> for AdultAge { -/// type Error = &'v str; +/// type Error = &'v RawStr; /// -/// fn from_form_value(form_value: &'v str) -> Result { -/// match usize::from_form_value(form_value) { +/// fn from_form_value(form_value: &'v RawStr) -> Result { +/// match form_value.parse::() { /// Ok(age) if age >= 21 => Ok(AdultAge(age)), /// _ => Err(form_value), /// } @@ -141,50 +142,50 @@ pub trait FromFormValue<'v>: Sized { /// Parses an instance of `Self` from an HTTP form field value or returns an /// `Error` if one cannot be parsed. - fn from_form_value(form_value: &'v str) -> Result; + fn from_form_value(form_value: &'v RawStr) -> Result; /// Returns a default value to be used when the form field does not exist. /// If this returns `None`, then the field is required. Otherwise, this /// should return `Some(default_value)`. The default implementation simply /// returns `None`. + #[inline(always)] fn default() -> Option { None } } -impl<'v> FromFormValue<'v> for &'v str { - type Error = Error; +impl<'v> FromFormValue<'v> for &'v RawStr { + type Error = !; // This just gives the raw string. - fn from_form_value(v: &'v str) -> Result { + #[inline(always)] + fn from_form_value(v: &'v RawStr) -> Result { Ok(v) } } impl<'v> FromFormValue<'v> for String { - type Error = &'v str; + type Error = &'v RawStr; // This actually parses the value according to the standard. - fn from_form_value(v: &'v str) -> Result { - let replaced = v.replace("+", " "); - match URI::percent_decode(replaced.as_bytes()) { - Err(_) => Err(v), - Ok(string) => Ok(string.into_owned()) - } + #[inline(always)] + fn from_form_value(v: &'v RawStr) -> Result { + v.url_decode().map_err(|_| v) } } impl<'v> FromFormValue<'v> for bool { - type Error = &'v str; + type Error = &'v RawStr; - fn from_form_value(v: &'v str) -> Result { - match v { + fn from_form_value(v: &'v RawStr) -> Result { + match v.as_str() { "on" | "true" => Ok(true), "off" | "false" => Ok(false), _ => Err(v), } } + #[inline(always)] fn default() -> Option { Some(false) } @@ -193,9 +194,11 @@ impl<'v> FromFormValue<'v> for bool { macro_rules! impl_with_fromstr { ($($T:ident),+) => ($( impl<'v> FromFormValue<'v> for $T { - type Error = &'v str; - fn from_form_value(v: &'v str) -> Result { - $T::from_str(v).map_err(|_| v) + type Error = &'v RawStr; + + #[inline(always)] + fn from_form_value(v: &'v RawStr) -> Result { + $T::from_str(v.as_str()).map_err(|_| v) } } )+) @@ -205,29 +208,31 @@ impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr); impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option { - type Error = Error; + type Error = !; - fn from_form_value(v: &'v str) -> Result { + #[inline(always)] + fn from_form_value(v: &'v RawStr) -> Result { match T::from_form_value(v) { Ok(v) => Ok(Some(v)), Err(_) => Ok(None), } } + #[inline(always)] fn default() -> Option> { Some(None) } } -// TODO: Add more useful implementations (range, regex, etc.). +// // TODO: Add more useful implementations (range, regex, etc.). impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { - type Error = Error; + type Error = !; - fn from_form_value(v: &'v str) -> Result { + #[inline(always)] + fn from_form_value(v: &'v RawStr) -> Result { match T::from_form_value(v) { ok@Ok(_) => Ok(ok), e@Err(_) => Ok(e), } } } - diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs index 57ccdf1a..edcef839 100644 --- a/lib/src/request/form/mod.rs +++ b/lib/src/request/form/mod.rs @@ -72,9 +72,10 @@ use outcome::Outcome::*; /// # #![allow(deprecated, dead_code, unused_attributes)] /// # #![plugin(rocket_codegen)] /// # extern crate rocket; +/// # use rocket::http::RawStr; /// #[derive(FromForm)] /// struct UserInput<'f> { -/// value: &'f str +/// value: &'f RawStr /// } /// # fn main() { } /// ``` @@ -88,9 +89,10 @@ use outcome::Outcome::*; /// # #![plugin(rocket_codegen)] /// # extern crate rocket; /// # use rocket::request::Form; +/// # use rocket::http::RawStr; /// # #[derive(FromForm)] /// # struct UserInput<'f> { -/// # value: &'f str +/// # value: &'f RawStr /// # } /// #[post("/submit", data = "")] /// fn submit_task<'r>(user_input: Form<'r, UserInput<'r>>) -> String { @@ -221,7 +223,7 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { let mut items = FormItems::from(long_lived_string); let result = T::from_form_items(items.by_ref()); - if !items.exhausted() { + if !items.exhaust() { return FormResult::Invalid(form_string); } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index c2ee2993..fe4fa4d5 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -168,10 +168,11 @@ impl Rocket { from_utf8_unchecked(&data.peek()[..min(data_len, max_len)]) }; - let mut form_items = FormItems::from(form); - if let Some(("_method", value)) = form_items.next() { - if let Ok(method) = value.parse() { - req.set_method(method); + if let Some((key, value)) = FormItems::from(form).next() { + if key == "_method" { + if let Ok(method) = value.parse() { + req.set_method(method); + } } } } From cff9901940071d8933eacb667ada63fbb430f8a3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2017 23:17:28 -0700 Subject: [PATCH 074/297] Implement FromData for Vec. --- lib/src/data/from_data.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index ee756269..74afe33a 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -1,4 +1,4 @@ -use std::io::Read; +use std::io::{self, Read}; use outcome::{self, IntoOutcome}; use outcome::Outcome::*; @@ -184,14 +184,28 @@ impl FromData for Option { } impl FromData for String { - type Error = (); + type Error = io::Error; // FIXME: Doc. fn from_data(_: &Request, data: Data) -> Outcome { let mut string = String::new(); match data.open().read_to_string(&mut string) { Ok(_) => Success(string), - Err(_) => Failure((Status::UnprocessableEntity, ())) + Err(e) => Failure((Status::BadRequest, e)) + } + } +} + +// FIXME Implement this. +impl FromData for Vec { + type Error = io::Error; + + // FIXME: Doc. + fn from_data(_: &Request, data: Data) -> Outcome { + let mut bytes = Vec::new(); + match data.open().read_to_end(&mut bytes) { + Ok(_) => Success(bytes), + Err(e) => Failure((Status::BadRequest, e)) } } } From f5ec470a7d8b24a031fc9201679329ca2a524fde Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 31 Mar 2017 00:18:58 -0700 Subject: [PATCH 075/297] Use the `RawStr` type for raw parameter strings. This is a breaking change. The `&str` type no longer implements `FromParam`. The `&RawStr` type should be used in its place. --- codegen/tests/run-pass/complete-decorator.rs | 2 +- codegen/tests/run-pass/dynamic-paths.rs | 2 +- .../tests/run-pass/issue-1-colliding-names.rs | 2 +- contrib/src/uuid.rs | 10 +- examples/errors/src/main.rs | 2 +- examples/extended_validation/src/main.rs | 2 +- examples/hello_person/src/main.rs | 4 +- examples/hello_ranks/src/main.rs | 6 +- examples/manual_routes/src/main.rs | 9 +- examples/optional_redirect/src/main.rs | 5 +- examples/optional_result/src/main.rs | 7 +- examples/pastebin/src/paste_id.rs | 5 +- lib/src/request/param.rs | 113 ++++++++++-------- lib/src/request/request.rs | 10 +- lib/src/response/flash.rs | 3 +- 15 files changed, 100 insertions(+), 82 deletions(-) diff --git a/codegen/tests/run-pass/complete-decorator.rs b/codegen/tests/run-pass/complete-decorator.rs index 5fcc75c3..26c7a7d9 100644 --- a/codegen/tests/run-pass/complete-decorator.rs +++ b/codegen/tests/run-pass/complete-decorator.rs @@ -13,7 +13,7 @@ struct User<'a> { } #[post("/?", format = "application/json", data = "", rank = 2)] -fn get<'r>(name: &str, +fn get<'r>(name: &RawStr, query: User<'r>, user: Form<'r, User<'r>>, cookies: Cookies) diff --git a/codegen/tests/run-pass/dynamic-paths.rs b/codegen/tests/run-pass/dynamic-paths.rs index 92c331c6..b5f3a9c8 100644 --- a/codegen/tests/run-pass/dynamic-paths.rs +++ b/codegen/tests/run-pass/dynamic-paths.rs @@ -4,7 +4,7 @@ extern crate rocket; #[get("/test///")] -fn get(one: &str, two: usize, three: isize) -> &'static str { "hi" } +fn get(one: String, two: usize, three: isize) -> &'static str { "hi" } fn main() { let _ = routes![get]; diff --git a/codegen/tests/run-pass/issue-1-colliding-names.rs b/codegen/tests/run-pass/issue-1-colliding-names.rs index 284cf4d6..6c97c895 100644 --- a/codegen/tests/run-pass/issue-1-colliding-names.rs +++ b/codegen/tests/run-pass/issue-1-colliding-names.rs @@ -4,7 +4,7 @@ extern crate rocket; #[get("/")] -fn todo(todo: &str) -> &str { +fn todo(todo: String) -> String { todo } diff --git a/contrib/src/uuid.rs b/contrib/src/uuid.rs index a12232b4..6b9de682 100644 --- a/contrib/src/uuid.rs +++ b/contrib/src/uuid.rs @@ -84,7 +84,7 @@ impl<'a> FromParam<'a> for UUID { /// A value is successfully parsed if `param` is a properly formatted UUID. /// Otherwise, a `UuidParseError` is returned. #[inline(always)] - fn from_param(param: &'a str) -> Result { + fn from_param(param: &'a RawStr) -> Result { param.parse() } } @@ -141,14 +141,14 @@ mod test { #[test] fn test_from_param() { let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2"; - let uuid_wrapper = UUID::from_param(uuid_str).unwrap(); + let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap(); assert_eq!(uuid_str, uuid_wrapper.to_string()) } #[test] fn test_into_inner() { let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2"; - let uuid_wrapper = UUID::from_param(uuid_str).unwrap(); + let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap(); let real_uuid: uuid_ext::Uuid = uuid_str.parse().unwrap(); let inner_uuid: uuid_ext::Uuid = uuid_wrapper.into_inner(); assert_eq!(real_uuid, inner_uuid) @@ -157,7 +157,7 @@ mod test { #[test] fn test_partial_eq() { let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2"; - let uuid_wrapper = UUID::from_param(uuid_str).unwrap(); + let uuid_wrapper = UUID::from_param(uuid_str.into()).unwrap(); let real_uuid: uuid_ext::Uuid = uuid_str.parse().unwrap(); assert_eq!(uuid_wrapper, real_uuid) } @@ -165,7 +165,7 @@ mod test { #[test] fn test_from_param_invalid() { let uuid_str = "c1aa1e3b-9614-4895-9ebd-705255fa5bc2p"; - let uuid_result = UUID::from_param(uuid_str); + let uuid_result = UUID::from_param(uuid_str.into()); assert_eq!(uuid_result, Err(UuidParseError::InvalidLength(37))); } } diff --git a/examples/errors/src/main.rs b/examples/errors/src/main.rs index cebdc34b..edcb3981 100644 --- a/examples/errors/src/main.rs +++ b/examples/errors/src/main.rs @@ -8,7 +8,7 @@ extern crate rocket; use rocket::response::content; #[get("/hello//")] -fn hello(name: &str, age: i8) -> String { +fn hello(name: String, age: i8) -> String { format!("Hello, {} year old named {}!", age, name) } diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index 75963d59..45dfd151 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -75,7 +75,7 @@ fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result { } #[get("/user/")] -fn user_page(username: &str) -> String { +fn user_page(username: &RawStr) -> String { format!("This is {}'s page.", username) } diff --git a/examples/hello_person/src/main.rs b/examples/hello_person/src/main.rs index cb0c993d..a4769efe 100644 --- a/examples/hello_person/src/main.rs +++ b/examples/hello_person/src/main.rs @@ -6,12 +6,12 @@ extern crate rocket; #[cfg(test)] mod tests; #[get("/hello//")] -fn hello(name: &str, age: u8) -> String { +fn hello(name: String, age: u8) -> String { format!("Hello, {} year old named {}!", age, name) } #[get("/hello/")] -fn hi<'r>(name: &'r str) -> &'r str { +fn hi(name: String) -> String { name } diff --git a/examples/hello_ranks/src/main.rs b/examples/hello_ranks/src/main.rs index f8d12abd..ab1d6f6a 100644 --- a/examples/hello_ranks/src/main.rs +++ b/examples/hello_ranks/src/main.rs @@ -3,15 +3,17 @@ extern crate rocket; +use rocket::http::RawStr; + #[cfg(test)] mod tests; #[get("/hello//")] -fn hello(name: &str, age: i8) -> String { +fn hello(name: String, age: i8) -> String { format!("Hello, {} year old named {}!", age, name) } #[get("/hello//", rank = 2)] -fn hi(name: &str, age: &str) -> String { +fn hi(name: String, age: &RawStr) -> String { format!("Hi {}! Your age ({}) is kind of funky.", name, age) } diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index 1e23f3f7..8720c2be 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -7,8 +7,7 @@ use std::io; use std::fs::File; use rocket::{Request, Route, Data, Catcher, Error}; -use rocket::http::Status; -use rocket::request::FromParam; +use rocket::http::{Status, RawStr}; use rocket::response::{self, Responder}; use rocket::response::status::Custom; use rocket::handler::Outcome; @@ -23,7 +22,8 @@ fn hi(_req: &Request, _: Data) -> Outcome<'static> { } fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> { - Outcome::of(req.get_param(0).unwrap_or("unnamed")) + let param = req.get_param::<&'a RawStr>(0); + Outcome::of(param.map(|r| r.as_str()).unwrap_or("unnamed")) } fn echo_url(req: &Request, _: Data) -> Outcome<'static> { @@ -31,7 +31,8 @@ fn echo_url(req: &Request, _: Data) -> Outcome<'static> { .as_str() .split_at(6) .1; - Outcome::of(String::from_param(param).unwrap()) + + Outcome::of(RawStr::from_str(param).url_decode()) } fn upload<'r>(req: &'r Request, data: Data) -> Outcome<'r> { diff --git a/examples/optional_redirect/src/main.rs b/examples/optional_redirect/src/main.rs index a25fcf08..693ae1ec 100644 --- a/examples/optional_redirect/src/main.rs +++ b/examples/optional_redirect/src/main.rs @@ -6,6 +6,7 @@ extern crate rocket; mod tests; use rocket::response::Redirect; +use rocket::http::RawStr; #[get("/")] fn root() -> Redirect { @@ -13,8 +14,8 @@ fn root() -> Redirect { } #[get("/users/")] -fn user(name: &str) -> Result<&'static str, Redirect> { - match name { +fn user(name: &RawStr) -> Result<&'static str, Redirect> { + match name.as_str() { "Sergio" => Ok("Hello, Sergio!"), _ => Err(Redirect::to("/users/login")), } diff --git a/examples/optional_result/src/main.rs b/examples/optional_result/src/main.rs index 7b47f32c..8edba794 100644 --- a/examples/optional_result/src/main.rs +++ b/examples/optional_result/src/main.rs @@ -3,11 +3,12 @@ extern crate rocket; -#[cfg(test)] -mod tests; +#[cfg(test)] mod tests; + +use rocket::http::RawStr; #[get("/users/")] -fn user(name: &str) -> Option<&'static str> { +fn user(name: &RawStr) -> Option<&'static str> { if name == "Sergio" { Some("Hello, Sergio!") } else { diff --git a/examples/pastebin/src/paste_id.rs b/examples/pastebin/src/paste_id.rs index fee1aec3..2586b302 100644 --- a/examples/pastebin/src/paste_id.rs +++ b/examples/pastebin/src/paste_id.rs @@ -2,6 +2,7 @@ use std::fmt; use std::borrow::Cow; use rocket::request::FromParam; +use rocket::http::RawStr; use rand::{self, Rng}; /// Table to retrieve base62 values from. @@ -44,9 +45,9 @@ fn valid_id(id: &str) -> bool { /// Returns an instance of `PasteID` if the path segment is a valid ID. /// Otherwise returns the invalid ID as the `Err` value. impl<'a> FromParam<'a> for PasteID<'a> { - type Error = &'a str; + type Error = &'a RawStr; - fn from_param(param: &'a str) -> Result, &'a str> { + fn from_param(param: &'a RawStr) -> Result, &'a RawStr> { match valid_id(param) { true => Ok(PasteID(Cow::Borrowed(param))), false => Err(param) diff --git a/lib/src/request/param.rs b/lib/src/request/param.rs index bf3f9807..7602e277 100644 --- a/lib/src/request/param.rs +++ b/lib/src/request/param.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::fmt::Debug; use http::uri::{URI, Segments, SegmentError}; +use http::RawStr; /// Trait to convert a dynamic path segment string to a concrete value. /// @@ -51,15 +52,16 @@ use http::uri::{URI, Segments, SegmentError}; /// /// For instance, imagine you've asked for an `` as a `usize`. To determine /// when the `` was not a valid `usize` and retrieve the string that failed -/// to parse, you can use a `Result` type for the `` parameter -/// as follows: +/// to parse, you can use a `Result` type for the `` +/// parameter as follows: /// /// ```rust /// # #![feature(plugin)] /// # #![plugin(rocket_codegen)] /// # extern crate rocket; +/// # use rocket::http::RawStr; /// #[get("/")] -/// fn hello(id: Result) -> String { +/// fn hello(id: Result) -> String { /// match id { /// Ok(id_num) => format!("usize: {}", id_num), /// Err(string) => format!("Not a usize: {}", string) @@ -80,7 +82,7 @@ use http::uri::{URI, Segments, SegmentError}; /// type returns successfully. Otherwise, the raw path segment is returned /// in the `Err` value. /// -/// * **str** +/// * **&RawStr** /// /// _This implementation always returns successfully._ /// @@ -107,14 +109,6 @@ use http::uri::{URI, Segments, SegmentError}; /// The path segment is parsed by `T`'s `FromParam` implementation. The /// returned `Result` value is returned. /// -/// # `str` vs. `String` -/// -/// Paths are URL encoded. As a result, the `str` `FromParam` implementation -/// returns the raw, URL encoded version of the path segment string. On the -/// other hand, `String` decodes the path parameter, but requires an allocation -/// to do so. This tradeoff is similiar to that of form values, and you should -/// use whichever makes sense for your application. -/// /// # Example /// /// Say you want to parse a segment of the form: @@ -138,13 +132,14 @@ use http::uri::{URI, Segments, SegmentError}; /// /// ```rust /// use rocket::request::FromParam; +/// use rocket::http::RawStr; /// # #[allow(dead_code)] /// # struct MyParam<'r> { key: &'r str, value: usize } /// /// impl<'r> FromParam<'r> for MyParam<'r> { -/// type Error = &'r str; +/// type Error = &'r RawStr; /// -/// fn from_param(param: &'r str) -> Result, &'r str> { +/// fn from_param(param: &'r RawStr) -> Result { /// let (key, val_str) = match param.find(':') { /// Some(i) if i > 0 => (¶m[..i], ¶m[(i + 1)..]), /// _ => return Err(param) @@ -172,11 +167,12 @@ use http::uri::{URI, Segments, SegmentError}; /// # #![plugin(rocket_codegen)] /// # extern crate rocket; /// # use rocket::request::FromParam; +/// # use rocket::http::RawStr; /// # #[allow(dead_code)] /// # struct MyParam<'r> { key: &'r str, value: usize } /// # impl<'r> FromParam<'r> for MyParam<'r> { -/// # type Error = &'r str; -/// # fn from_param(param: &'r str) -> Result, &'r str> { +/// # type Error = &'r RawStr; +/// # fn from_param(param: &'r RawStr) -> Result { /// # Err(param) /// # } /// # } @@ -197,29 +193,35 @@ pub trait FromParam<'a>: Sized { /// Parses an instance of `Self` from a dynamic path parameter string or /// returns an `Error` if one cannot be parsed. - fn from_param(param: &'a str) -> Result; + fn from_param(param: &'a RawStr) -> Result; } -impl<'a> FromParam<'a> for &'a str { +impl<'a> FromParam<'a> for &'a RawStr { type Error = (); - fn from_param(param: &'a str) -> Result<&'a str, Self::Error> { + + #[inline(always)] + fn from_param(param: &'a RawStr) -> Result<&'a RawStr, Self::Error> { Ok(param) } } impl<'a> FromParam<'a> for String { - type Error = &'a str; - fn from_param(p: &'a str) -> Result { - URI::percent_decode(p.as_bytes()).map_err(|_| p).map(|s| s.into_owned()) + type Error = &'a RawStr; + + #[inline(always)] + fn from_param(param: &'a RawStr) -> Result { + param.percent_decode().map(|cow| cow.into_owned()).map_err(|_| param) } } macro_rules! impl_with_fromstr { ($($T:ident),+) => ($( impl<'a> FromParam<'a> for $T { - type Error = &'a str; - fn from_param(param: &'a str) -> Result { - $T::from_str(param).map_err(|_| param) + type Error = &'a RawStr; + + #[inline(always)] + fn from_param(param: &'a RawStr) -> Result { + $T::from_str(param.as_str()).map_err(|_| param) } } )+) @@ -230,22 +232,26 @@ impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, SocketAddr); impl<'a, T: FromParam<'a>> FromParam<'a> for Result { - type Error = (); - fn from_param(p: &'a str) -> Result { - Ok(match T::from_param(p) { - Ok(val) => Ok(val), - Err(e) => Err(e), - }) + type Error = !; + + #[inline] + fn from_param(param: &'a RawStr) -> Result { + match T::from_param(param) { + Ok(val) => Ok(Ok(val)), + Err(e) => Ok(Err(e)), + } } } impl<'a, T: FromParam<'a>> FromParam<'a> for Option { - type Error = (); - fn from_param(p: &'a str) -> Result { - Ok(match T::from_param(p) { - Ok(val) => Some(val), - Err(_) => None - }) + type Error = !; + + #[inline] + fn from_param(param: &'a RawStr) -> Result { + match T::from_param(param) { + Ok(val) => Ok(Some(val)), + Err(_) => Ok(None) + } } } @@ -277,9 +283,10 @@ pub trait FromSegments<'a>: Sized { } impl<'a> FromSegments<'a> for Segments<'a> { - type Error = (); + type Error = !; - fn from_segments(segments: Segments<'a>) -> Result, ()> { + #[inline(always)] + fn from_segments(segments: Segments<'a>) -> Result, Self::Error> { Ok(segments) } } @@ -335,21 +342,25 @@ impl<'a> FromSegments<'a> for PathBuf { } impl<'a, T: FromSegments<'a>> FromSegments<'a> for Result { - type Error = (); - fn from_segments(segments: Segments<'a>) -> Result, ()> { - Ok(match T::from_segments(segments) { - Ok(val) => Ok(val), - Err(e) => Err(e), - }) + type Error = !; + + #[inline] + fn from_segments(segments: Segments<'a>) -> Result, !> { + match T::from_segments(segments) { + Ok(val) => Ok(Ok(val)), + Err(e) => Ok(Err(e)), + } } } impl<'a, T: FromSegments<'a>> FromSegments<'a> for Option { - type Error = (); - fn from_segments(segments: Segments<'a>) -> Result, ()> { - Ok(match T::from_segments(segments) { - Ok(val) => Some(val), - Err(_) => None - }) + type Error = !; + + #[inline] + fn from_segments(segments: Segments<'a>) -> Result, !> { + match T::from_segments(segments) { + Ok(val) => Ok(Some(val)), + Err(_) => Ok(None) + } } } diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 6d607463..07ce0101 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -14,7 +14,7 @@ use super::{FromParam, FromSegments}; use router::Route; use http::uri::{URI, Segments}; use http::{Method, Header, HeaderMap, Cookies, Session, CookieJar, Key}; -use http::{ContentType, Accept, MediaType}; +use http::{RawStr, ContentType, Accept, MediaType}; use http::parse::media_type; use http::hyper; @@ -362,7 +362,7 @@ impl<'r> Request<'r> { /// /// # Example /// - /// Retrieve parameter `0`, which is expected to be an `&str`, in a manual + /// Retrieve parameter `0`, which is expected to be a `String`, in a manual /// route: /// /// ```rust @@ -371,7 +371,7 @@ impl<'r> Request<'r> { /// /// # #[allow(dead_code)] /// fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> { - /// Outcome::of(req.get_param(0).unwrap_or("unnamed")) + /// Outcome::of(req.get_param::(0).unwrap_or("unnamed".into())) /// } /// ``` pub fn get_param<'a, T: FromParam<'a>>(&'a self, n: usize) -> Result { @@ -391,7 +391,7 @@ impl<'r> Request<'r> { /// Get the `n`th path parameter as a string, if it exists. This is used by /// codegen. #[doc(hidden)] - pub fn get_param_str(&self, n: usize) -> Option<&str> { + pub fn get_param_str(&self, n: usize) -> Option<&RawStr> { let params = self.extra.params.borrow(); if n >= params.len() { debug!("{} is >= param count {}", n, params.len()); @@ -405,7 +405,7 @@ impl<'r> Request<'r> { return None; } - Some(&path[i..j]) + Some(path[i..j].into()) } /// Retrieves and parses into `T` all of the path segments in the request diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index ceb629e2..1fe1cf7f 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -53,9 +53,10 @@ const FLASH_COOKIE_NAME: &'static str = "_flash"; /// # /// use rocket::response::{Flash, Redirect}; /// use rocket::request::FlashMessage; +/// use rocket::http::RawStr; /// /// #[post("/login/")] -/// fn login(name: &str) -> Result<&'static str, Flash> { +/// fn login(name: &RawStr) -> Result<&'static str, Flash> { /// if name == "special_user" { /// Ok("Hello, special user!") /// } else { From 351658801e95afe4dc3f49a067eb6997127e2d56 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Apr 2017 16:46:13 -0700 Subject: [PATCH 076/297] Allow unreachable_code in generated route functions for new ! error types. --- codegen/src/decorators/route.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index fc562abb..6b7ac7d6 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -234,9 +234,8 @@ fn generic_route_decorator(known_method: Option>, ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, - annotated: Annotatable) - -> Vec -{ + annotated: Annotatable + ) -> Vec { let mut output = Vec::new(); // Parse the route and generate the code to create the form and param vars. @@ -252,14 +251,17 @@ fn generic_route_decorator(known_method: Option>, let user_fn_name = route.annotated_fn.ident(); let route_fn_name = user_fn_name.prepend(ROUTE_FN_PREFIX); emit_item(&mut output, quote_item!(ecx, - fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) + // Allow the `unreachable_code` lint for those FromParam impls that have + // an `Error` associated type of !. + #[allow(unreachable_code)] + fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) -> ::rocket::handler::Outcome<'_b> { $param_statements $query_statement $data_statement let responder = $user_fn_name($fn_arguments); - ::rocket::handler::Outcome::of(responder) - } + ::rocket::handler::Outcome::of(responder) + } ).unwrap()); // Generate and emit the static route info that uses the just generated From 7c19bf784d64936f90b1a61f86430d5d360072f0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Apr 2017 19:06:30 -0700 Subject: [PATCH 077/297] Allow form field renaming via #[form(field = "name")] attribute. --- codegen/src/decorators/derive_form.rs | 91 +++++++++++++++---- codegen/src/utils/span_ext.rs | 13 ++- codegen/tests/compile-fail/form-field-attr.rs | 83 +++++++++++++++++ codegen/tests/run-pass/form-field-rename.rs | 54 +++++++++++ examples/cookies/src/main.rs | 2 +- examples/form_kitchen_sink/src/main.rs | 4 +- examples/form_kitchen_sink/static/index.html | 8 +- examples/session/src/main.rs | 2 +- examples/todo/src/main.rs | 2 +- 9 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 codegen/tests/compile-fail/form-field-attr.rs create mode 100644 codegen/tests/run-pass/form-field-rename.rs diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index 0c0408e7..3b551265 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -1,10 +1,12 @@ #![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens? use std::mem::transmute; +use std::collections::HashMap; use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::print::pprust::{stmt_to_string}; use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData, Ident}; +use syntax::ast::StructField; use syntax::codemap::Span; use syntax::ext::build::AstBuilder; use syntax::ptr::P; @@ -13,7 +15,7 @@ use syntax_ext::deriving::generic::MethodDef; use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty}; use syntax_ext::deriving::generic::combine_substructure as c_s; -use utils::strip_ty_lifetimes; +use utils::{strip_ty_lifetimes, SpanExt}; static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \ structures with named fields."; @@ -49,6 +51,55 @@ fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span) } } +pub fn extract_field_ident_name(ecx: &ExtCtxt, struct_field: &StructField) + -> (Ident, String, Span) { + let ident = match struct_field.ident { + Some(ident) => ident, + None => ecx.span_fatal(struct_field.span, ONLY_STRUCTS_ERR) + }; + + let field_attrs: Vec<_> = struct_field.attrs.iter() + .filter(|attr| attr.check_name("form")) + .collect(); + + let default = |ident: Ident| (ident, ident.to_string(), struct_field.span); + if field_attrs.len() == 0 { + return default(ident); + } else if field_attrs.len() > 1 { + ecx.span_err(struct_field.span, "only a single #[form(..)] \ + attribute can be applied to a given struct field at a time"); + return default(ident); + } + + let field_attr = field_attrs[0]; + ::syntax::attr::mark_known(&field_attr); + if !field_attr.meta_item_list().map_or(false, |l| l.len() == 1) { + ecx.struct_span_err(field_attr.span, "incorrect use of attribute") + .help(r#"the `form` attribute must have the form: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + let inner_item = &field_attr.meta_item_list().unwrap()[0]; + if !inner_item.check_name("field") { + ecx.struct_span_err(inner_item.span, "invalid `form` attribute contents") + .help(r#"only the 'field' key is supported: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + if !inner_item.is_value_str() { + ecx.struct_span_err(inner_item.span, "invalid `field` in attribute") + .help(r#"the `form` attribute must have the form: #[form(field = "..")]"#) + .emit(); + return default(ident); + } + + let name = inner_item.value_str().unwrap().as_str().to_string(); + let sp = inner_item.span.shorten_upto(name.len() + 2); + (ident, name, sp) +} + // TODO: Use proper logging to emit the error messages. pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, annotated: &Annotatable, push: &mut FnMut(Annotatable)) { @@ -144,19 +195,25 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct _ => cx.span_bug(trait_span, "impossible substructure in `from_form`") }; - // Create a vector of (ident, type) pairs, one for each field in struct. - let mut fields_and_types = vec![]; + // Vec of (ident: Ident, type: Ty, name: String), one for each field. + let mut names = HashMap::new(); + let mut fields_info = vec![]; for field in fields { - let ident = match field.ident { - Some(ident) => ident, - None => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR) - }; - + let (ident, name, span) = extract_field_ident_name(cx, field); let stripped_ty = strip_ty_lifetimes(field.ty.clone()); - fields_and_types.push((ident, stripped_ty)); + + if let Some(sp) = names.get(&name).map(|sp| *sp) { + cx.struct_span_err(span, "field with duplicate name") + .span_note(sp, "original was declared here") + .emit(); + } else { + names.insert(name.clone(), span); + } + + fields_info.push((ident, stripped_ty, name)); } - debug!("Fields and types: {:?}", fields_and_types); + debug!("Fields, types, attrs: {:?}", fields_info); let mut stmts = Vec::new(); // The thing to do when we wish to exit with an error. @@ -168,7 +225,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // placed into the final struct. They start out as `None` and are changed // to Some when a parse completes, or some default value if the parse was // unsuccessful and default() returns Some. - for &(ref ident, ref ty) in &fields_and_types { + for &(ref ident, ref ty, _) in &fields_info { stmts.push(quote_stmt!(cx, let mut $ident: ::std::option::Option<$ty> = None; ).unwrap()); @@ -177,17 +234,15 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // Generating an arm for each struct field. This matches against the key and // tries to parse the value according to the type. let mut arms = vec![]; - for &(ref ident, _) in &fields_and_types { - let ident_string = ident.to_string(); - let id_str = ident_string.as_str(); + for &(ref ident, _, ref name) in &fields_info { arms.push(quote_tokens!(cx, - $id_str => { + $name => { let r = ::rocket::http::RawStr::from_str(v); $ident = match ::rocket::request::FromFormValue::from_form_value(r) { Ok(v) => Some(v), Err(e) => { println!(" => Error parsing form val '{}': {:?}", - $id_str, e); + $name, e); $return_err_stmt } }; @@ -219,7 +274,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // that each parameter actually is Some() or has a default value. let mut failure_conditions = vec![]; - for &(ref ident, ref ty) in (&fields_and_types).iter() { + for &(ref ident, ref ty, _) in (&fields_info).iter() { failure_conditions.push(quote_tokens!(cx, if $ident.is_none() && <$ty as ::rocket::request::FromFormValue>::default().is_none() { @@ -232,7 +287,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // The fields of the struct, which are just the let bindings declared above // or the default value. let mut result_fields = vec![]; - for &(ref ident, ref ty) in &fields_and_types { + for &(ref ident, ref ty, _) in &fields_info { result_fields.push(quote_tokens!(cx, $ident: $ident.unwrap_or_else(|| <$ty as ::rocket::request::FromFormValue>::default().unwrap() diff --git a/codegen/src/utils/span_ext.rs b/codegen/src/utils/span_ext.rs index f6ea4631..932e69ba 100644 --- a/codegen/src/utils/span_ext.rs +++ b/codegen/src/utils/span_ext.rs @@ -1,8 +1,6 @@ use syntax::codemap::{Span, BytePos}; pub trait SpanExt { - fn shorten_to(self, to_length: usize) -> Span; - /// Trim the span on the left and right by `length`. fn trim(self, length: u32) -> Span; @@ -11,6 +9,12 @@ pub trait SpanExt { /// Trim the span on the right by `length`. fn trim_right(self, length: usize) -> Span; + + // Trim from the right so that the span is `length` in size. + fn shorten_to(self, to_length: usize) -> Span; + + // Trim from the left so that the span is `length` in size. + fn shorten_upto(self, length: usize) -> Span; } impl SpanExt for Span { @@ -29,6 +33,11 @@ impl SpanExt for Span { self } + fn shorten_upto(mut self, length: usize) -> Span { + self.lo = self.hi - BytePos(length as u32); + self + } + fn trim(mut self, length: u32) -> Span { self.lo = self.lo + BytePos(length); self.hi = self.hi - BytePos(length); diff --git a/codegen/tests/compile-fail/form-field-attr.rs b/codegen/tests/compile-fail/form-field-attr.rs new file mode 100644 index 00000000..5c2352ac --- /dev/null +++ b/codegen/tests/compile-fail/form-field-attr.rs @@ -0,0 +1,83 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +#[derive(FromForm)] +struct MyForm { + #[form(field = "blah", field = "bloo")] + //~^ ERROR: incorrect use of attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm1 { + #[form] + //~^ ERROR: incorrect use of attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm2 { + #[form("blah")] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm3 { + #[form(123)] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm4 { + #[form(beep = "bop")] + //~^ ERROR: invalid `form` attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm5 { + #[form(field = "blah")] + #[form(field = "blah")] + my_field: String, + //~^ ERROR: only a single +} + +#[derive(FromForm)] +struct MyForm6 { + #[form(field = true)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm7 { + #[form(field)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm8 { + #[form(field = 123)] + //~^ ERROR: invalid `field` in attribute + my_field: String, +} + +#[derive(FromForm)] +struct MyForm9 { + #[form(field = "hello")] + first: String, + #[form(field = "hello")] + //~^ ERROR: field with duplicate name + other: String, +} + +#[derive(FromForm)] +struct MyForm10 { + first: String, + #[form(field = "first")] + //~^ ERROR: field with duplicate name + other: String, +} diff --git a/codegen/tests/run-pass/form-field-rename.rs b/codegen/tests/run-pass/form-field-rename.rs new file mode 100644 index 00000000..674b6e24 --- /dev/null +++ b/codegen/tests/run-pass/form-field-rename.rs @@ -0,0 +1,54 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use rocket::request::{FromForm, FromFormValue, FormItems}; +use rocket::http::RawStr; + +#[derive(Debug, PartialEq, FromForm)] +struct Form { + single: usize, + #[form(field = "camelCase")] + camel_case: String, + #[form(field = "TitleCase")] + title_case: String, + #[form(field = "type")] + field_type: isize, + #[form(field = "DOUBLE")] + double: String, +} + +fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option { + let mut items = FormItems::from(string); + let result = T::from_form_items(items.by_ref()); + if !items.exhaust() { + panic!("Invalid form input."); + } + + result.ok() +} + +fn main() { + let form_string = &[ + "single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong" + ].join("&"); + + let form: Option
= parse(&form_string); + assert_eq!(form, Some(Form { + single: 100, + camel_case: "helloThere".into(), + title_case: "HiHi".into(), + field_type: -2, + double: "bing_bong".into() + })); + + let form_string = &[ + "single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2", + "DOUBLE=bing_bong" + ].join("&"); + + let form: Option = parse(&form_string); + assert!(form.is_none()); +} diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index f4f114dc..73776061 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -1,4 +1,4 @@ -#![feature(plugin, custom_derive, custom_attribute)] +#![feature(plugin, custom_derive)] #![plugin(rocket_codegen)] extern crate rocket_contrib; diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index 9563b696..8583578c 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -33,9 +33,11 @@ impl<'v> FromFormValue<'v> for FormOption { struct FormInput { checkbox: bool, number: usize, + #[form(field = "type")] radio: FormOption, password: String, - textarea: String, + #[form(field = "textarea")] + text_area: String, select: FormOption, } diff --git a/examples/form_kitchen_sink/static/index.html b/examples/form_kitchen_sink/static/index.html index 8749f995..7fc87cf9 100644 --- a/examples/form_kitchen_sink/static/index.html +++ b/examples/form_kitchen_sink/static/index.html @@ -14,15 +14,15 @@

-
")] +fn index_dyn_a(a: &RawStr) -> &'static str { "index" } + +fn rocket() -> rocket::Rocket { + let config = Config::new(Environment::Production).unwrap(); + rocket::custom(config, false) + .mount("/", routes![get_index, put_index, post_index, index_a, + index_b, index_c, index_dyn_a]) +} + +#[cfg(feature = "testing")] +mod benches { + extern crate test; + + use super::rocket; + use self::test::Bencher; + use rocket::testing::MockRequest; + use rocket::http::Method::*; + + #[bench] + fn bench_single_get_index(b: &mut Bencher) { + let rocket = rocket(); + let mut request = MockRequest::new(Get, "/"); + + b.iter(|| { + request.dispatch_with(&rocket); + }); + } + + #[bench] + fn bench_get_put_post_index(b: &mut Bencher) { + let rocket = rocket(); + + // Hold all of the requests we're going to make during the benchmark. + let mut requests = vec![]; + requests.push(MockRequest::new(Get, "/")); + requests.push(MockRequest::new(Put, "/")); + requests.push(MockRequest::new(Post, "/")); + + b.iter(|| { + for request in requests.iter_mut() { + request.dispatch_with(&rocket); + } + }); + } + + #[bench] + fn bench_dynamic(b: &mut Bencher) { + let rocket = rocket(); + + // Hold all of the requests we're going to make during the benchmark. + let mut requests = vec![]; + requests.push(MockRequest::new(Get, "/abc")); + requests.push(MockRequest::new(Get, "/abcdefg")); + requests.push(MockRequest::new(Get, "/123")); + + b.iter(|| { + for request in requests.iter_mut() { + request.dispatch_with(&rocket); + } + }); + } + + #[bench] + fn bench_simple_routing(b: &mut Bencher) { + let rocket = rocket(); + + // Hold all of the requests we're going to make during the benchmark. + let mut requests = vec![]; + for route in rocket.routes() { + let request = MockRequest::new(route.method, route.path.path()); + requests.push(request); + } + + // A few more for the dynamic route. + requests.push(MockRequest::new(Get, "/abc")); + requests.push(MockRequest::new(Get, "/abcdefg")); + requests.push(MockRequest::new(Get, "/123")); + + b.iter(|| { + for request in requests.iter_mut() { + request.dispatch_with(&rocket); + } + }); + } +} diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index fe4fa4d5..22734ea6 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -570,4 +570,10 @@ impl Rocket { unreachable!("the call to `handle_threads` should block on success") } + + /// Retrieves all of the mounted routes. + #[inline(always)] + pub fn routes<'a>(&'a self) -> impl Iterator + 'a { + self.router.routes() + } } diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 48495630..39dcd3eb 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -59,6 +59,11 @@ impl Router { result } + + #[inline] + pub fn routes<'a>(&'a self) -> impl Iterator + 'a { + self.routes.values().flat_map(|v| v.iter()) + } } #[cfg(test)] From 82ec8ee73962730f19dcbc10a65a11fdf4170351 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 6 Apr 2017 15:59:41 -0700 Subject: [PATCH 080/297] Use bash from env, not /usr/bin. --- scripts/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test.sh b/scripts/test.sh index 6a9b891b..c446a115 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # Brings in: ROOT_DIR, EXAMPLES_DIR, LIB_DIR, CODEGEN_DIR, CONTRIB_DIR, DOC_DIR From a2a0aab541d2b64716381cf55c19bd302cf73b6e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 11 Apr 2017 16:55:59 -0700 Subject: [PATCH 081/297] Depend on cookie >= 0.7.4 for ring bugfix. --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 366c945c..1534b046 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -33,7 +33,7 @@ pear = "0.0.8" pear_codegen = "0.0.8" [dependencies.cookie] -version = "0.7.2" +version = "0.7.4" features = ["percent-encode", "secure"] [dev-dependencies] From cf47daa8e140cf61e4d965df2840813a9d3fde8c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 12 Apr 2017 02:58:45 -0700 Subject: [PATCH 082/297] Return 404 on missing static file in todo example. --- examples/todo/src/static_files.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/todo/src/static_files.rs b/examples/todo/src/static_files.rs index 61ed862a..0336de7a 100644 --- a/examples/todo/src/static_files.rs +++ b/examples/todo/src/static_files.rs @@ -1,8 +1,7 @@ -use rocket::response::NamedFile; -use std::io; use std::path::{Path, PathBuf}; +use rocket::response::NamedFile; #[get("/", rank = 5)] -fn all(path: PathBuf) -> io::Result { - NamedFile::open(Path::new("static/").join(path)) +fn all(path: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(path)).ok() } From 1516ca4fb6f475b7e296ddc365d1bd2accad5873 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 13 Apr 2017 00:18:31 -0700 Subject: [PATCH 083/297] Initial TLS support. This commit introduces TLS support, provided by `rustls` and a fork of `hyper-rustls`. TLS support is enabled via the `tls` feature and activated when the `tls` configuration parameter is set. A new `hello_tls` example illustrates its usage. This commit also introduces more robust and complete configuration settings via environment variables. In particular, quoted string, array, and table (dictionaries) based configuration parameters can now be set via environment variables. Resolves #28. --- Cargo.toml | 1 + examples/hello_tls/Cargo.toml | 11 ++ examples/hello_tls/Rocket.toml | 11 ++ examples/hello_tls/private/cert.pem | 37 +++++ examples/hello_tls/private/key.pem | 51 +++++++ examples/hello_tls/src/main.rs | 15 ++ examples/hello_tls/src/tests.rs | 13 ++ lib/Cargo.toml | 8 ++ lib/src/config/builder.rs | 27 ++++ lib/src/config/config.rs | 216 +++++++++++++++++++--------- lib/src/config/error.rs | 19 ++- lib/src/config/mod.rs | 118 +++++++++++++-- lib/src/config/toml_ext.rs | 113 +++++++++++++-- lib/src/data/data.rs | 65 +++++---- lib/src/data/data_stream.rs | 86 ++++++++++- lib/src/error.rs | 5 + lib/src/lib.rs | 3 + lib/src/logger.rs | 10 +- lib/src/rocket.rs | 75 ++++++++-- 19 files changed, 739 insertions(+), 145 deletions(-) create mode 100644 examples/hello_tls/Cargo.toml create mode 100644 examples/hello_tls/Rocket.toml create mode 100644 examples/hello_tls/private/cert.pem create mode 100644 examples/hello_tls/private/key.pem create mode 100644 examples/hello_tls/src/main.rs create mode 100644 examples/hello_tls/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 47d91dbe..1f0af5ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,4 +33,5 @@ members = [ "examples/uuid", "examples/session", "examples/raw_sqlite", + "examples/hello_tls", ] diff --git a/examples/hello_tls/Cargo.toml b/examples/hello_tls/Cargo.toml new file mode 100644 index 00000000..d691440e --- /dev/null +++ b/examples/hello_tls/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hello_tls" +version = "0.0.0" +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib", features = ["tls"] } +rocket_codegen = { path = "../../codegen" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_tls/Rocket.toml b/examples/hello_tls/Rocket.toml new file mode 100644 index 00000000..14c7fa63 --- /dev/null +++ b/examples/hello_tls/Rocket.toml @@ -0,0 +1,11 @@ +# The certificate/private key pair used here was generated via openssl: +# +# openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ +# -keyout key.pem -out cert.pem +# +# The certificate is self-signed. As such, you will need to trust it directly +# for your browser to refer to the connection as secure. You should NEVER use +# this certificate/key pair. It is here for DEMONSTRATION PURPOSES ONLY. +[global.tls] +certs = "private/cert.pem" +key = "private/key.pem" diff --git a/examples/hello_tls/private/cert.pem b/examples/hello_tls/private/cert.pem new file mode 100644 index 00000000..3019e697 --- /dev/null +++ b/examples/hello_tls/private/cert.pem @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGXjCCBEagAwIBAgIJAJBTO2YLMz4tMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQHEwhTdGFuZm9yZDEP +MA0GA1UEChMGUm9ja2V0MRcwFQYDVQQDEw5TZXJnaW8gQmVuaXRlejEbMBkGCSqG +SIb3DQEJARYMc2JAcm9ja2V0LnJzMB4XDTE3MDQwOTA0MTIxM1oXDTI3MDQwNzA0 +MTIxM1owfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNV +BAcTCFN0YW5mb3JkMQ8wDQYDVQQKEwZSb2NrZXQxFzAVBgNVBAMTDlNlcmdpbyBC +ZW5pdGV6MRswGQYJKoZIhvcNAQkBFgxzYkByb2NrZXQucnMwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCzdm0ZxLNP4TlJBI2IpeVT4S6hZeBkem/aj4NZ +mhHA06HXVqcUw3W03YQklhO7E305uU/BTRz5q0BIa2DCPyZDUCkwTjOZAuFiiZzc +AZz/zhu2RwLWeYttlvjKewrIe0k9zrPaPXpdcFe0xq2mcUon0fyRztL1H8EYEScb +/TJqM1LkWKGSJEOMDeEYMVnJn/x9yFgfC82u/4GBc3q3Si2uRLCMkTLsg6TC27EF +kCVuOISf1+CvAKgk2x29SGm/nYoTe+j6YLm12h41S6JlGO9zJnORlwb4Mz5h+72p +NBaVER72kNxwskTNg2IWur7NM2Xi/nAfZ7+YOopgwosRuZl8Nw6CcpWDkGdLnO7X +H18Wy/BXOamXVa65tWefwlCiJ8bkqZgik8AHX36KZzTzkDO5g/4JAQDh4G56paGu +hcd1LXkGvTDuaSN4BkHDuYucr89aliWV/AKzum4BJkyKk3lVWDb9nfwyTRegsZg5 +ipTW7xLhvxzjeoLuDHybRzsw+2NFQoHA4PUouzC1n2/+eJIVysa6p5UZXTcTNGVd +rdU3GmifpFDBv4NwQrQ1y2izw0b+dbZ7DBAQIqW3toHeBUmmTiHSmQR5QT3Dz9HA +l2npMu4S2ZKQYJj+zqxyETzrOgz76LW1yZ3uAbX7z0OxlOoC67XYGAAWlDyU4pZc +qcnR1QIDAQABo4HiMIHfMB0GA1UdDgQWBBSUdwqW1sNXbeS29wXaJL0P9glPmTCB +rwYDVR0jBIGnMIGkgBSUdwqW1sNXbeS29wXaJL0P9glPmaGBgKR+MHwxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQHEwhTdGFuZm9yZDEP +MA0GA1UEChMGUm9ja2V0MRcwFQYDVQQDEw5TZXJnaW8gQmVuaXRlejEbMBkGCSqG +SIb3DQEJARYMc2JAcm9ja2V0LnJzggkAkFM7ZgszPi0wDAYDVR0TBAUwAwEB/zAN +BgkqhkiG9w0BAQsFAAOCAgEAXfsz3a1iDWoyyobVlFV3NvVD5CeZ9oWh/IvmbgfB +XMEB3ZLy3Cqn4op1u6Kbo/L8E+YYMlIGdqUtAlpJrtF1k3KeVGBScx6YebCPWcuL +aRO/l1qUR70RhA/Yrz0iNqcTSkC2n9YYFtr7tOiTzS3kqN03XB6fJBsYnVG4LzR5 +1EdNjgGoISSVmKJwQh0Sjy+GuHDnsjtL5xPxf5OFA6bgJiYkpMgxv0VDzC3Bl6NL +oTYwnQ+/19yzoSZANlvwKi8UftHEpBXMAW2Yr3jEfKuSQIe3FPr2js2JWfpl12yQ +JZnDPEJOamxD4hvvWljENwcVMss9X9FGiQCoFIYmGja4JXZ7KqvOlgdSaS8TUqCp +qHcSJpEiJQAJQton607EjWxBBWVEMEQYZx5nLFifxwexxv1jpbGeh+ehAwRlvrZU +nXR9miv/ohw6HmopNXmXcTCJsT8/OHb4g7cUs3scUmuySMZe1dKht+o+XmkWfF9b +fgqNz9so1ls9oyg/qjuMwh5wNNUsPQJGITmzTOfGGu7engyil744flO5aQFfSwcm +zQ7ZzRh+jDPI/rG8xbrYpXXK3+xln03O/96AC6iEELA0+A2PeworEkz47nEPrqlC +Fr17Aya+rJsrN9JXL1Uz87k3XfySNc6xT8zitNGzgxtgKthUg6fU9oOt/79HuOeL +g+Y= +-----END CERTIFICATE----- diff --git a/examples/hello_tls/private/key.pem b/examples/hello_tls/private/key.pem new file mode 100644 index 00000000..13ee4c54 --- /dev/null +++ b/examples/hello_tls/private/key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAs3ZtGcSzT+E5SQSNiKXlU+EuoWXgZHpv2o+DWZoRwNOh11an +FMN1tN2EJJYTuxN9OblPwU0c+atASGtgwj8mQ1ApME4zmQLhYomc3AGc/84btkcC +1nmLbZb4ynsKyHtJPc6z2j16XXBXtMatpnFKJ9H8kc7S9R/BGBEnG/0yajNS5Fih +kiRDjA3hGDFZyZ/8fchYHwvNrv+BgXN6t0otrkSwjJEy7IOkwtuxBZAlbjiEn9fg +rwCoJNsdvUhpv52KE3vo+mC5tdoeNUuiZRjvcyZzkZcG+DM+Yfu9qTQWlREe9pDc +cLJEzYNiFrq+zTNl4v5wH2e/mDqKYMKLEbmZfDcOgnKVg5BnS5zu1x9fFsvwVzmp +l1WuubVnn8JQoifG5KmYIpPAB19+imc085AzuYP+CQEA4eBueqWhroXHdS15Br0w +7mkjeAZBw7mLnK/PWpYllfwCs7puASZMipN5VVg2/Z38Mk0XoLGYOYqU1u8S4b8c +43qC7gx8m0c7MPtjRUKBwOD1KLswtZ9v/niSFcrGuqeVGV03EzRlXa3VNxpon6RQ +wb+DcEK0Nctos8NG/nW2ewwQECKlt7aB3gVJpk4h0pkEeUE9w8/RwJdp6TLuEtmS +kGCY/s6schE86zoM++i1tcmd7gG1+89DsZTqAuu12BgAFpQ8lOKWXKnJ0dUCAwEA +AQKCAgBIdkLrKq80S75zqzDywfls+vl3FcmbCIztdREWNs2ATHOGnWhtS9bVJrRa +iXaCDQZ9LkPzyw0uCmW0WBcDl7f9afqXlJvk5nLW9LWvZ79a0oACA34z13Pi1hiy +uSfLd2xFVpbsQfKMk/X1+lrXX9sPZQxUW2x2qVGwRAzEkmGu2/ZWWSsz9QyJGnmO +6S5V6RFsQF7EemGcjXJfMJ+WLo9vVDDtMRucwDLgsxAxLNjQPmXenK4OO3epGghS +C1EXm6bK4zdZEYEq2l1kK5vwsjbNCfOUD6Uyxo4jxh/4mB2eJwGXkTpRDsoVKT2L +6+9qr5wuIYpoQ93qu4hwNV0t1QERp4HYTrX6WzDPLCtoHlIfCfzXH05jYa9n/nJD +Ow4eeeK5RE7/9/fKJWX2/R45iz87QKeS2H+ps3IOirK1P4Y/4FdIoHVyHjaCeoGp +YeSXjTWz6OEcHX9qcShdVu8ILkJlBJ4xftxNKWw8d/jKG3xOaxVNAnTGGAx113Pa +RIZcGNfroQcceV0mRAQTTpZyV7jj+lvC9lXqyP4tJJc4YjSWG6ITPrYfKVYBAhNC +K8MutrCaEH87SzIssY3DvpSzIeZMafvR4VYHZbBsDpI8+9iIijXLzDR0IQUANcah +bLWzjhDXrE5f1Et1hyHCAkCzsPIyx67QugYpJ8zTDUp6oYVaaQKCAQEA3XjPYrAJ +kokPk8ErDYTe0lMRGKvIrV3TH5hkfTIlkMfn4M6wVS3WQYYEMwgcJHVILpHkxtuD +ZcNIrey5f3IlwvSGo30w9L4IQCt5nzU0GpYqeN4Uty+0jDO0pygPYRrNAedSQSYa +jgSwoQCiu/qVzb+fPD2n6Hfxn/+cBP/mw6JBZZRb4aIhAluIt958J7NB/AwY+o03 +rG6lq41UUYDtydt/yP3q7QlRBFkY+anm7O7Iy80e4oPciXbettsMuHPa8/zF1TWJ +IxG2v9C4Q++tvj9zKP66l/5JVKVdtexGtPJS9i8lIkPkqX7K9peOUDNwyVAyG4kB +2SxKysu4sTq5ZwKCAQEAz3D6UTbTnzkrQFDnKaYUpMW4jfKUFr/n+s5iIN3FPk9t +ZuZ3YbrIroW50KxmVYFG+x1vyEZvB+Bdb7apCnqK4z2x4vbpl4bsE/uW8Kk+rQxx +gbUfuih1r0FMVE0kGL8MxJEL/4owOZM9G8hinxkVst4LswNe0MPwfJHgeLrx/xAM +lHq4hS5Pb4SYb+Z6iUJlzJpsQPX+JLW3cDqlfUBB9ckijtNkTbwsq7ETWRglnAP1 +flOCBe6l1aSDPiSa3fRSwA0PHTztVZt2TwnhDD2v40tTxCfeAwafeEwpUUgWivt0 +Doq0Tni5lZiPjqNfmbZSa8BKyeEtPuKZcKT4QMSJYwKCAQA8+TTHa8XG5Rs3x5fN +ygX6i8oKK8k9CbbFXRRVb4fuG0tYli7v1IXHVlkzn4j39J4hzCLbKLY9Pw10bNcJ +ImkJCn9C5YWj6+mjmRSL437rzun0itfTMzwW2WlkF+BcEJ/eZUw9CXuIG/xw5xbm +f+/cTGRPln3yv4rzTNEsgzOKKtKsX7MIJLXHy2GRlZxC5dRFyyLZYCWywGe2Glvb +cI6G43qD4HxcNBNtCgaZPdCI7Ji1m0xken8uDV71ossWwTbHs5DXyTxvPkI8/v6s +HYGM/jT7VV4T2Hth5YEuQ9WXnZt/ka08iMqca37/cuxIYlEr63tQH2E15D7XJE09 +5fgDAoIBAQDKC2hDofsMokoWIraEQlbpBgtzdkn2voPcLRg2msp6njIYf3DXp22/ +TlBlhwVFUt0nyMwPbUrHiSh4npiWtDSCkJyqS4PJKojWDb4+ORnqwqvrgdadIrs9 +L4SAt4Ho+GwfKIdfJeFCsr5aSRqFi5Eu3kbW3PmErNOXAR55eNwrah5WoBEI5spH +/AXdN8cx2ZH9borx2qbmand4wCZfkC6ujnEyW4Lek+GOeLI3nOVEyDZcDEogLQko +xUtvQ4fzlvziQdXuzGD9eKYK5bxkh9DAuaWk8I+0ssawDL5RhL0wMSog38guhjd8 +FVP9wfJjbMlqWah+aOwAzARXStbhfouxAoIBAQCw0HQEHJL0nTLT0SdkEAZF12Fo +NMdTh68xtQ1y2papT87L6J8/WmK1/O32KAJ3XXikl0QMTkhjHqf+eA+27C8L+jIR +HPhB4OGdlOufk1QXoaX1Z4vVHfyyXASspfZ1ecxFsQdC/lPnQ+ir/skHI1NNC9oO +Q39d37tyoKCOyZUhD0IsT5+6vVgyj6EcwiCmgwZ7PI3MKKoXx7HmkZ9e4hVYbXP6 +WsNsF2VKPHCre56T2FRT3xLxzN5uZOz0Hrau50Y/3RNuYiJJ2aufAYmv1r6HT/0W +BP2kmzmWlg1JJaU9vS9jZXabQZPp8bJ+fDtSZSa69AJESKRE9e3e895uW3gY +-----END RSA PRIVATE KEY----- diff --git a/examples/hello_tls/src/main.rs b/examples/hello_tls/src/main.rs new file mode 100644 index 00000000..524574ad --- /dev/null +++ b/examples/hello_tls/src/main.rs @@ -0,0 +1,15 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +#[cfg(test)] mod tests; + +#[get("/")] +fn hello() -> &'static str { + "Hello, world!" +} + +fn main() { + rocket::ignite().mount("/", routes![hello]).launch(); +} diff --git a/examples/hello_tls/src/tests.rs b/examples/hello_tls/src/tests.rs new file mode 100644 index 00000000..94b54e03 --- /dev/null +++ b/examples/hello_tls/src/tests.rs @@ -0,0 +1,13 @@ +use super::rocket; +use rocket::testing::MockRequest; +use rocket::http::Method::*; + +#[test] +fn hello_world() { + let rocket = rocket::ignite().mount("/", routes![super::hello]); + let mut req = MockRequest::new(Get, "/"); + let mut response = req.dispatch_with(&rocket); + + let body_str = response.body().and_then(|body| body.into_string()); + assert_eq!(body_str, Some("Hello, world!".to_string())); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 1534b046..2f998332 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,6 +16,7 @@ categories = ["web-programming::http-server"] [features] testing = [] +tls = ["rustls", "hyper-rustls"] [dependencies] term-painter = "0.2" @@ -31,6 +32,13 @@ base64 = "0.4" smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" } pear = "0.0.8" pear_codegen = "0.0.8" +rustls = { version = "0.5.8", optional = true } + +[dependencies.hyper-rustls] +git = "https://github.com/SergioBenitez/hyper-rustls" +default-features = false +features = ["server"] +optional = true [dependencies.cookie] version = "0.7.4" diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index 18647b37..251ef98b 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -20,6 +20,8 @@ pub struct ConfigBuilder { pub log_level: LoggingLevel, /// The session key. pub session_key: Option, + /// TLS configuration (path to certificates file, path to private key file). + pub tls_config: Option<(String, String)>, /// Any extra parameters that aren't part of Rocket's config. pub extras: HashMap, /// The root directory of this config. @@ -63,6 +65,7 @@ impl ConfigBuilder { workers: config.workers, log_level: config.log_level, session_key: None, + tls_config: None, extras: config.extras, root: root_dir, } @@ -162,6 +165,26 @@ impl ConfigBuilder { self } + /// Sets the `tls_config` in the configuration being built. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::{Config, Environment}; + /// + /// let mut config = Config::build(Environment::Staging) + /// .tls("/path/to/certs.pem", "/path/to/key.pem") + /// # ; /* + /// .unwrap(); + /// # */ + /// ``` + pub fn tls(mut self, certs_path: C, key_path: K) -> Self + where C: Into, K: Into + { + self.tls_config = Some((certs_path.into(), key_path.into())); + self + } + /// Sets the `environment` in the configuration being built. /// /// # Example @@ -260,6 +283,10 @@ impl ConfigBuilder { config.set_extras(self.extras); config.set_root(self.root); + if let Some((certs_path, key_path)) = self.tls_config { + config.set_tls(&certs_path, &key_path)?; + } + if let Some(key) = self.session_key { config.set_session_key(key)?; } diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index f6087e1c..8f6b3d51 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -5,10 +5,11 @@ use std::convert::AsRef; use std::fmt; use std::env; -use config::Environment::*; -use config::{self, Value, ConfigBuilder, Environment, ConfigError}; +#[cfg(feature = "tls")] use rustls::{Certificate, PrivateKey}; use {num_cpus, base64}; +use config::Environment::*; +use config::{Result, Table, Value, ConfigBuilder, Environment, ConfigError}; use logger::LoggingLevel; use http::Key; @@ -18,7 +19,7 @@ pub enum SessionKey { } impl SessionKey { - #[inline] + #[inline(always)] pub fn kind(&self) -> &'static str { match *self { SessionKey::Generated(_) => "generated", @@ -26,7 +27,7 @@ impl SessionKey { } } - #[inline] + #[inline(always)] fn inner(&self) -> &Key { match *self { SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key @@ -34,6 +35,15 @@ impl SessionKey { } } +#[cfg(feature = "tls")] +pub struct TlsConfig { + pub certs: Vec, + pub key: PrivateKey +} + +#[cfg(not(feature = "tls"))] +pub struct TlsConfig; + /// Structure for Rocket application configuration. /// /// A `Config` structure is typically built using the [build](#method.build) @@ -61,20 +71,73 @@ pub struct Config { pub workers: u16, /// How much information to log. pub log_level: LoggingLevel, + /// The session key. + pub(crate) session_key: SessionKey, + /// TLS configuration. + pub(crate) tls: Option, /// Extra parameters that aren't part of Rocket's core config. pub extras: HashMap, /// The path to the configuration file this config belongs to. pub config_path: PathBuf, - /// The session key. - pub(crate) session_key: SessionKey, } -macro_rules! parse { - ($conf:expr, $name:expr, $val:expr, $method:ident, $expect: expr) => ( - $val.$method().ok_or_else(|| { - $conf.bad_type($name, $val.type_str(), $expect) - }) - ); +macro_rules! config_from_raw { + ($config:expr, $name:expr, $value:expr, + $($key:ident => ($type:ident, $set:ident, $map:expr)),+ | _ => $rest:expr) => ( + match $name { + $(stringify!($key) => { + concat_idents!(value_as_, $type)($config, $name, $value) + .and_then(|parsed| $map($config.$set(parsed))) + })+ + _ => $rest + } + ) +} + +#[inline(always)] +fn value_as_str<'a>(config: &Config, name: &str, value: &'a Value) -> Result<&'a str> { + value.as_str().ok_or(config.bad_type(name, value.type_str(), "a string")) +} + +#[inline(always)] +fn value_as_u16(config: &Config, name: &str, value: &Value) -> Result { + match value.as_integer() { + Some(x) if x >= 0 && x <= (u16::max_value() as i64) => Ok(x as u16), + _ => Err(config.bad_type(name, value.type_str(), "a 16-bit unsigned integer")) + } +} + +#[inline(always)] +fn value_as_log_level(config: &Config, name: &str, value: &Value) -> Result { + value_as_str(config, name, value) + .and_then(|s| s.parse().map_err(|e| config.bad_type(name, value.type_str(), e))) +} + +#[inline(always)] +fn value_as_tls_config<'v>(config: &Config, + name: &str, + value: &'v Value, + ) -> Result<(&'v str, &'v str)> +{ + let (mut certs_path, mut key_path) = (None, None); + let table = value.as_table() + .ok_or_else(|| config.bad_type(name, value.type_str(), "a table"))?; + + let env = config.environment; + for (key, value) in table { + match key.as_str() { + "certs" => certs_path = Some(value_as_str(config, "tls.certs", value)?), + "key" => key_path = Some(value_as_str(config, "tls.key", value)?), + _ => return Err(ConfigError::UnknownKey(format!("{}.tls.{}", env, key))) + } + } + + if let (Some(certs), Some(key)) = (certs_path, key_path) { + Ok((certs, key)) + } else { + Err(config.bad_type(name, "a table with missing entries", + "a table with `certs` and `key` entries")) + } } impl Config { @@ -119,7 +182,7 @@ impl Config { /// let mut my_config = Config::new(Environment::Production).expect("cwd"); /// my_config.set_port(1001); /// ``` - pub fn new(env: Environment) -> config::Result { + pub fn new(env: Environment) -> Result { let cwd = env::current_dir().map_err(|_| ConfigError::BadCWD)?; Config::default(env, cwd.as_path().join("Rocket.custom.toml")) } @@ -131,7 +194,7 @@ impl Config { /// # Panics /// /// Panics if randomness cannot be retrieved from the OS. - pub(crate) fn default

(env: Environment, path: P) -> config::Result + pub(crate) fn default

(env: Environment, path: P) -> Result where P: AsRef { let config_path = path.as_ref().to_path_buf(); @@ -155,6 +218,7 @@ impl Config { workers: default_workers, log_level: LoggingLevel::Normal, session_key: key, + tls: None, extras: HashMap::new(), config_path: config_path, } @@ -167,6 +231,7 @@ impl Config { workers: default_workers, log_level: LoggingLevel::Normal, session_key: key, + tls: None, extras: HashMap::new(), config_path: config_path, } @@ -179,6 +244,7 @@ impl Config { workers: default_workers, log_level: LoggingLevel::Critical, session_key: key, + tls: None, extras: HashMap::new(), config_path: config_path, } @@ -209,39 +275,21 @@ impl Config { /// * **workers**: Integer (16-bit unsigned) /// * **log**: String /// * **session_key**: String (192-bit base64) - pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> config::Result<()> { - if name == "address" { - let address_str = parse!(self, name, val, as_str, "a string")?; - self.set_address(address_str)?; - } else if name == "port" { - let port = parse!(self, name, val, as_integer, "an integer")?; - if port < 0 || port > (u16::max_value() as i64) { - return Err(self.bad_type(name, val.type_str(), "a 16-bit unsigned integer")) + /// * **tls**: Table (`certs` (path as String), `key` (path as String)) + pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> { + let (id, ok) = (|val| val, |_| Ok(())); + config_from_raw!(self, name, val, + address => (str, set_address, id), + port => (u16, set_port, ok), + workers => (u16, set_workers, ok), + session_key => (str, set_session_key, id), + log => (log_level, set_log_level, ok), + tls => (tls_config, set_raw_tls, id) + | _ => { + self.extras.insert(name.into(), val.clone()); + Ok(()) } - - self.set_port(port as u16); - } else if name == "workers" { - let workers = parse!(self, name, val, as_integer, "an integer")?; - if workers < 0 || workers > (u16::max_value() as i64) { - return Err(self.bad_type(name, val.type_str(), "a 16-bit unsigned integer")); - } - - self.set_workers(workers as u16); - } else if name == "session_key" { - let key = parse!(self, name, val, as_str, "a string")?; - self.set_session_key(key)?; - } else if name == "log" { - let level_str = parse!(self, name, val, as_str, "a string")?; - let expect = "log level ('normal', 'critical', 'debug')"; - match level_str.parse() { - Ok(level) => self.set_log_level(level), - Err(_) => return Err(self.bad_type(name, val.type_str(), expect)) - } - } else { - self.extras.insert(name.into(), val.clone()); - } - - Ok(()) + ) } /// Sets the root directory of this configuration to `root`. @@ -286,7 +334,7 @@ impl Config { /// # Ok(()) /// # } /// ``` - pub fn set_address>(&mut self, address: A) -> config::Result<()> { + pub fn set_address>(&mut self, address: A) -> Result<()> { let address = address.into(); if address.parse::().is_err() && lookup_host(&address).is_err() { return Err(self.bad_type("address", "string", "a valid hostname or IP")); @@ -310,6 +358,7 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[inline] pub fn set_port(&mut self, port: u16) { self.port = port; } @@ -328,6 +377,7 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[inline] pub fn set_workers(&mut self, workers: u16) { self.workers = workers; } @@ -354,7 +404,7 @@ impl Config { /// # Ok(()) /// # } /// ``` - pub fn set_session_key>(&mut self, key: K) -> config::Result<()> { + pub fn set_session_key>(&mut self, key: K) -> Result<()> { let key = key.into(); let error = self.bad_type("session_key", "string", "a 256-bit base64 encoded string"); @@ -387,10 +437,42 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[inline] pub fn set_log_level(&mut self, log_level: LoggingLevel) { self.log_level = log_level; } + #[cfg(feature = "tls")] + pub fn set_tls(&mut self, certs_path: &str, key_path: &str) -> Result<()> { + use hyper_rustls::util as tls; + + let err = "nonexistent or invalid file"; + let certs = tls::load_certs(certs_path) + .map_err(|_| self.bad_type("tls", err, "a readable certificates file"))?; + let key = tls::load_private_key(key_path) + .map_err(|_| self.bad_type("tls", err, "a readable private key file"))?; + + self.tls = Some(TlsConfig { certs, key }); + Ok(()) + } + + #[cfg(not(feature = "tls"))] + pub fn set_tls(&mut self, _: &str, _: &str) -> Result<()> { + self.tls = Some(TlsConfig); + Ok(()) + } + + #[cfg(not(test))] + #[inline(always)] + fn set_raw_tls(&mut self, paths: (&str, &str)) -> Result<()> { + self.set_tls(paths.0, paths.1) + } + + #[cfg(test)] + fn set_raw_tls(&mut self, _: (&str, &str)) -> Result<()> { + Ok(()) + } + /// Sets the extras for `self` to be the key/value pairs in `extras`. /// encoded string. /// @@ -413,6 +495,7 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[inline] pub fn set_extras(&mut self, extras: HashMap) { self.extras = extras; } @@ -441,6 +524,7 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[inline] pub fn extras<'a>(&'a self) -> impl Iterator { self.extras.iter().map(|(k, v)| (k.as_str(), v)) } @@ -470,9 +554,9 @@ impl Config { /// /// assert_eq!(config.get_str("my_extra"), Ok("extra_value")); /// ``` - pub fn get_str<'a>(&'a self, name: &str) -> config::Result<&'a str> { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_str, "a string") + pub fn get_str<'a>(&'a self, name: &str) -> Result<&'a str> { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_str().ok_or_else(|| self.bad_type(name, val.type_str(), "a string")) } /// Attempts to retrieve the extra named `name` as an integer. @@ -494,9 +578,9 @@ impl Config { /// /// assert_eq!(config.get_int("my_extra"), Ok(1025)); /// ``` - pub fn get_int(&self, name: &str) -> config::Result { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_integer, "an integer") + pub fn get_int(&self, name: &str) -> Result { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_integer().ok_or_else(|| self.bad_type(name, val.type_str(), "an integer")) } /// Attempts to retrieve the extra named `name` as a boolean. @@ -518,9 +602,9 @@ impl Config { /// /// assert_eq!(config.get_bool("my_extra"), Ok(true)); /// ``` - pub fn get_bool(&self, name: &str) -> config::Result { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_bool, "a boolean") + pub fn get_bool(&self, name: &str) -> Result { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_bool().ok_or_else(|| self.bad_type(name, val.type_str(), "a boolean")) } /// Attempts to retrieve the extra named `name` as a float. @@ -542,9 +626,9 @@ impl Config { /// /// assert_eq!(config.get_float("pi"), Ok(3.14159)); /// ``` - pub fn get_float(&self, name: &str) -> config::Result { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_float, "a float") + pub fn get_float(&self, name: &str) -> Result { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_float().ok_or_else(|| self.bad_type(name, val.type_str(), "a float")) } /// Attempts to retrieve the extra named `name` as a slice of an array. @@ -566,9 +650,9 @@ impl Config { /// /// assert!(config.get_slice("numbers").is_ok()); /// ``` - pub fn get_slice(&self, name: &str) -> config::Result<&[Value]> { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_slice, "a slice") + pub fn get_slice(&self, name: &str) -> Result<&[Value]> { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_slice().ok_or_else(|| self.bad_type(name, val.type_str(), "a slice")) } /// Attempts to retrieve the extra named `name` as a table. @@ -594,9 +678,9 @@ impl Config { /// /// assert!(config.get_table("my_table").is_ok()); /// ``` - pub fn get_table(&self, name: &str) -> config::Result<&config::Table> { - let value = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; - parse!(self, name, value, as_table, "a table") + pub fn get_table(&self, name: &str) -> Result<&Table> { + let val = self.extras.get(name).ok_or_else(|| ConfigError::NotFound)?; + val.as_table().ok_or_else(|| self.bad_type(name, val.type_str(), "a table")) } /// Returns the path at which the configuration file for `self` is stored. diff --git a/lib/src/config/error.rs b/lib/src/config/error.rs index 8a814de2..01179d2f 100644 --- a/lib/src/config/error.rs +++ b/lib/src/config/error.rs @@ -52,8 +52,12 @@ pub enum ConfigError { ParseError(String, PathBuf, Vec), /// There was a TOML parsing error in a config environment variable. /// - /// Parameters: (env_key, env_value, expected type) - BadEnvVal(String, String, &'static str), + /// Parameters: (env_key, env_value, error) + BadEnvVal(String, String, String), + /// The entry (key) is unknown. + /// + /// Parameters: (key) + UnknownKey(String), } impl ConfigError { @@ -95,11 +99,14 @@ impl ConfigError { trace_!("'{}' - {}", error_source, White.paint(&error.desc)); } } - BadEnvVal(ref key, ref value, ref expected) => { + BadEnvVal(ref key, ref value, ref error) => { error!("environment variable '{}={}' could not be parsed", White.paint(key), White.paint(value)); - info_!("value for {:?} must be {}", - White.paint(key), White.paint(expected)) + info_!("{}", White.paint(error)); + } + UnknownKey(ref key) => { + error!("the configuration key '{}' is unknown and disallowed in \ + this position", White.paint(key)); } } } @@ -123,6 +130,7 @@ impl fmt::Display for ConfigError { BadFilePath(ref p, _) => write!(f, "{:?} is not a valid config path", p), BadEnv(ref e) => write!(f, "{:?} is not a valid `ROCKET_ENV` value", e), ParseError(..) => write!(f, "the config file contains invalid TOML"), + UnknownKey(ref k) => write!(f, "'{}' is an unknown key", k), BadEntry(ref e, _) => { write!(f, "{:?} is not a valid `[environment]` entry", e) } @@ -148,6 +156,7 @@ impl Error for ConfigError { ParseError(..) => "the config file contains invalid TOML", BadType(..) => "a key was specified with a value of the wrong type", BadEnvVal(..) => "an environment variable could not be parsed", + UnknownKey(..) => "an unknown key was used in a disallowed position", } } } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 401eeec7..488d03ba 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -43,6 +43,10 @@ //! * **session_key**: _[string]_ a 256-bit base64 encoded string (44 //! characters) to use as the session key //! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="` +//! * **tls**: _[dict]_ a dictionary with two keys: 1) `certs`: _[string]_ a +//! path to a certificate chain in PEM format, and 2) `key`: _[string]_ a +//! path to a private key file in PEM format for the certificate in `certs` +//! * example: `{ certs = "/path/to/certs.pem", key = "/path/to/key.pem" }` //! //! ### Rocket.toml //! @@ -118,6 +122,31 @@ //! //! Environment variables take precedence over all other configuration methods: //! if the variable is set, it will be used as the value for the parameter. +//! Variable values are parsed as if they were TOML syntax. As illustration, +//! consider the following examples: +//! +//! ```sh +//! ROCKET_INTEGER=1 +//! ROCKET_FLOAT=3.14 +//! ROCKET_STRING=Hello +//! ROCKET_STRING="Hello" +//! ROCKET_BOOL=true +//! ROCKET_ARRAY=[1,"b",3.14] +//! ROCKET_DICT={key="abc",val=123} +//! ``` +//! +//! ### TLS Configuration +//! +//! TLS can be enabled by specifying the `tls.key` and `tls.certs` parameters. +//! Rocket must be compiled with the `tls` feature enabled for the parameters to +//! take effect. The recommended way to specify the parameters is via the +//! `global` environment: +//! +//! ```toml +//! [global.tls] +//! certs = "/path/to/certs.pem" +//! key = "/path/to/key.pem" +//! ``` //! //! ## Retrieving Configuration Parameters //! @@ -278,6 +307,7 @@ impl RocketConfig { Err(ConfigError::NotFound) } + #[inline] fn get_mut(&mut self, env: Environment) -> &mut Config { match self.config.get_mut(&env) { Some(config) => config, @@ -306,33 +336,37 @@ impl RocketConfig { } /// Retrieves the `Config` for the active environment. + #[inline] pub fn active(&self) -> &Config { self.get(self.active_env) } // Override all environments with values from env variables if present. fn override_from_env(&mut self) -> Result<()> { - 'outer: for (env_key, env_val) in env::vars() { - if env_key.len() < ENV_VAR_PREFIX.len() { + for (key, val) in env::vars() { + if key.len() < ENV_VAR_PREFIX.len() { continue - } else if !uncased_eq(&env_key[..ENV_VAR_PREFIX.len()], ENV_VAR_PREFIX) { + } else if !uncased_eq(&key[..ENV_VAR_PREFIX.len()], ENV_VAR_PREFIX) { continue } // Skip environment variables that are handled elsewhere. - for prehandled_var in PREHANDLED_VARS.iter() { - if uncased_eq(&env_key, &prehandled_var) { - continue 'outer - } + if PREHANDLED_VARS.iter().any(|var| uncased_eq(&key, var)) { + continue } // Parse the key and value and try to set the variable for all envs. - let key = env_key[ENV_VAR_PREFIX.len()..].to_lowercase(); - let val = parse_simple_toml_value(&env_val); + let key = key[ENV_VAR_PREFIX.len()..].to_lowercase(); + let toml_val = match parse_simple_toml_value(&val) { + Ok(val) => val, + Err(e) => return Err(ConfigError::BadEnvVal(key, val, e.into())) + }; + for env in &Environment::all() { - match self.get_mut(*env).set_raw(&key, &val) { - Err(ConfigError::BadType(_, exp, _, _)) => { - return Err(ConfigError::BadEnvVal(env_key, env_val, exp)) + match self.get_mut(*env).set_raw(&key, &toml_val) { + Err(ConfigError::BadType(_, exp, actual, _)) => { + let e = format!("expected {}, but found {}", exp, actual); + return Err(ConfigError::BadEnvVal(key, val, e)) } Err(e) => return Err(e), Ok(_) => { /* move along */ } @@ -458,7 +492,7 @@ unsafe fn private_init() { let config = RocketConfig::read().unwrap_or_else(|e| { match e { ParseError(..) | BadEntry(..) | BadEnv(..) | BadType(..) - | BadFilePath(..) | BadEnvVal(..) => bail(e), + | BadFilePath(..) | BadEnvVal(..) | UnknownKey(..) => bail(e), IOError | BadCWD => warn!("Failed reading Rocket.toml. Using defaults."), NotFound => { /* try using the default below */ } } @@ -697,6 +731,64 @@ mod test { "#.to_string(), TEST_CONFIG_FILENAME).is_err()); } + // Only do this test when the tls feature is disabled since the file paths + // we're supplying don't actually exist. + #[test] + fn test_good_tls_values() { + // Take the lock so changing the environment doesn't cause races. + let _env_lock = ENV_LOCK.lock().unwrap(); + env::set_var(CONFIG_ENV, "dev"); + + assert!(RocketConfig::parse(r#" + [staging] + tls = { certs = "some/path.pem", key = "some/key.pem" } + "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); + + assert!(RocketConfig::parse(r#" + [staging.tls] + certs = "some/path.pem" + key = "some/key.pem" + "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); + + assert!(RocketConfig::parse(r#" + [global.tls] + certs = "some/path.pem" + key = "some/key.pem" + "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); + + assert!(RocketConfig::parse(r#" + [global] + tls = { certs = "some/path.pem", key = "some/key.pem" } + "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); + } + + #[test] + fn test_bad_tls_config() { + // Take the lock so changing the environment doesn't cause races. + let _env_lock = ENV_LOCK.lock().unwrap(); + env::remove_var(CONFIG_ENV); + + assert!(RocketConfig::parse(r#" + [development] + tls = "hello" + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [development] + tls = { certs = "some/path.pem" } + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [development] + tls = { certs = "some/path.pem", key = "some/key.pem", extra = "bah" } + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + + assert!(RocketConfig::parse(r#" + [staging] + tls = { cert = "some/path.pem", key = "some/key.pem" } + "#.to_string(), TEST_CONFIG_FILENAME).is_err()); + } + #[test] fn test_good_port_values() { // Take the lock so changing the environment doesn't cause races. diff --git a/lib/src/config/toml_ext.rs b/lib/src/config/toml_ext.rs index cc2a107a..ac2b7e21 100644 --- a/lib/src/config/toml_ext.rs +++ b/lib/src/config/toml_ext.rs @@ -4,20 +4,63 @@ use std::str::FromStr; use config::Value; -pub fn parse_simple_toml_value(string: &str) -> Value { - if let Ok(int) = i64::from_str(string) { - return Value::Integer(int) +pub fn parse_simple_toml_value(string: &str) -> Result { + if string.is_empty() { + return Err("value is empty") } - if let Ok(boolean) = bool::from_str(string) { - return Value::Boolean(boolean) - } + let value = if let Ok(int) = i64::from_str(string) { + Value::Integer(int) + } else if let Ok(float) = f64::from_str(string) { + Value::Float(float) + } else if let Ok(boolean) = bool::from_str(string) { + Value::Boolean(boolean) + } else if string.starts_with('{') { + if !string.ends_with('}') { + return Err("value is missing closing '}'") + } - if let Ok(float) = f64::from_str(string) { - return Value::Float(float) - } + let mut table = BTreeMap::new(); + let inner = &string[1..string.len() - 1].trim(); + if !inner.is_empty() { + for key_val in inner.split(',') { + let (key, val) = match key_val.find('=') { + Some(i) => (&key_val[..i], &key_val[(i + 1)..]), + None => return Err("missing '=' in dicitonary key/value pair") + }; - Value::String(string.to_string()) + let key = key.trim().to_string(); + let val = parse_simple_toml_value(val.trim())?; + table.insert(key, val); + } + } + + Value::Table(table) + } else if string.starts_with('[') { + if !string.ends_with(']') { + return Err("value is missing closing ']'") + } + + let mut vals = vec![]; + let inner = &string[1..string.len() - 1].trim(); + if !inner.is_empty() { + for val_str in inner.split(',') { + vals.push(parse_simple_toml_value(val_str.trim())?); + } + } + + Value::Array(vals) + } else if string.starts_with('"') { + if !string[1..].ends_with('"') { + return Err("value is missing closing '\"'"); + } + + Value::String(string[1..string.len() - 1].to_string()) + } else { + Value::String(string.to_string()) + }; + + Ok(value) } /// Conversion trait from standard types into TOML `Value`s. @@ -27,18 +70,21 @@ pub trait IntoValue { } impl<'a> IntoValue for &'a str { + #[inline(always)] fn into_value(self) -> Value { Value::String(self.to_string()) } } impl IntoValue for Value { + #[inline(always)] fn into_value(self) -> Value { self } } impl IntoValue for Vec { + #[inline(always)] fn into_value(self) -> Value { Value::Array(self.into_iter().map(|v| v.into_value()).collect()) } @@ -87,3 +133,50 @@ impl_into_value!(Boolean: bool); impl_into_value!(Float: f64); impl_into_value!(Float: f32, as f64); +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + use super::parse_simple_toml_value; + use super::IntoValue; + use super::Value::*; + + macro_rules! assert_parse { + ($string:expr, $value:expr) => ( + match parse_simple_toml_value($string) { + Ok(value) => assert_eq!(value, $value), + Err(e) => panic!("{:?} failed to parse: {:?}", $string, e) + }; + ) + } + + #[test] + fn parse_toml_values() { + assert_parse!("1", Integer(1)); + assert_parse!("1.32", Float(1.32)); + assert_parse!("true", Boolean(true)); + assert_parse!("false", Boolean(false)); + assert_parse!("hello, WORLD!", String("hello, WORLD!".into())); + assert_parse!("hi", String("hi".into())); + assert_parse!("\"hi\"", String("hi".into())); + + assert_parse!("[]", Array(Vec::new())); + assert_parse!("[1]", vec![1].into_value()); + assert_parse!("[1, 2, 3]", vec![1, 2, 3].into_value()); + assert_parse!("[1.32, 2]", + vec![1.32.into_value(), 2.into_value()].into_value()); + + assert_parse!("{}", Table(BTreeMap::new())); + assert_parse!("{a=b}", Table({ + let mut map = BTreeMap::new(); + map.insert("a".into(), "b".into_value()); + map + })); + assert_parse!("{v=1, on=true,pi=3.14}", Table({ + let mut map = BTreeMap::new(); + map.insert("v".into(), 1.into_value()); + map.insert("on".into(), true.into_value()); + map.insert("pi".into(), 3.14.into_value()); + map + })); + } +} diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 0000336e..d6e92e1d 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -4,9 +4,10 @@ use std::fs::File; use std::time::Duration; use std::mem::transmute; -use super::data_stream::{DataStream, StreamReader, kill_stream}; +#[cfg(feature = "tls")] use hyper_rustls::WrappedStream; use ext::ReadExt; +use super::data_stream::{DataStream, HyperNetStream, StreamReader, kill_stream}; use http::hyper::h1::HttpReader; use http::hyper::buffer; @@ -82,32 +83,47 @@ impl Data { DataStream::new(stream, network) } + // FIXME: This is absolutely terrible (downcasting!), thanks to Hyper. pub(crate) fn from_hyp(mut h_body: BodyReader) -> Result { - // FIXME: This is asolutely terrible, thanks to Hyper. + // Create the Data object from hyper's buffer. + let (vec, pos, cap) = h_body.get_mut().take_buf(); + let net_stream = h_body.get_ref().get_ref(); + + #[cfg(feature = "tls")] + fn concrete_stream(stream: &&mut NetworkStream) -> Option { + stream.downcast_ref::() + .map(|s| HyperNetStream::Http(s.clone())) + .or_else(|| { + stream.downcast_ref::() + .map(|s| HyperNetStream::Https(s.clone())) + }) + } + + #[cfg(not(feature = "tls"))] + fn concrete_stream(stream: &&mut NetworkStream) -> Option { + stream.downcast_ref::() + .map(|s| HyperNetStream::Http(s.clone())) + } // Retrieve the underlying HTTPStream from Hyper. - let mut stream = match h_body.get_ref().get_ref() - .downcast_ref::() { - Some(s) => { - let owned_stream = s.clone(); - let buf_len = h_body.get_ref().get_buf().len() as u64; - match h_body { - SizedReader(_, n) => SizedReader(owned_stream, n - buf_len), - EofReader(_) => EofReader(owned_stream), - EmptyReader(_) => EmptyReader(owned_stream), - ChunkedReader(_, n) => - ChunkedReader(owned_stream, n.map(|k| k - buf_len)), - } - }, - None => return Err("Stream is not an HTTP stream!"), + let stream = match concrete_stream(net_stream) { + Some(stream) => stream, + None => return Err("Stream is not an HTTP(s) stream!") }; // Set the read timeout to 5 seconds. - stream.get_mut().set_read_timeout(Some(Duration::from_secs(5))).unwrap(); + stream.set_read_timeout(Some(Duration::from_secs(5))).expect("timeout set"); - // Create the Data object from hyper's buffer. - let (vec, pos, cap) = h_body.get_mut().take_buf(); - Ok(Data::new(vec, pos, cap, stream)) + // Create a reader from the stream. Don't read what's already buffered. + let buffered = (cap - pos) as u64; + let reader = match h_body { + SizedReader(_, n) => SizedReader(stream, n - buffered), + EofReader(_) => EofReader(stream), + EmptyReader(_) => EmptyReader(stream), + ChunkedReader(_, n) => ChunkedReader(stream, n.map(|k| k - buffered)), + }; + + Ok(Data::new(vec, pos, cap, reader)) } /// Retrieve the `peek` buffer. @@ -151,10 +167,10 @@ impl Data { // in the buffer is at `pos` and the buffer has `cap` valid bytes. The // remainder of the data bytes can be read from `stream`. pub(crate) fn new(mut buf: Vec, - pos: usize, - mut cap: usize, - mut stream: StreamReader) - -> Data { + pos: usize, + mut cap: usize, + mut stream: StreamReader + ) -> Data { // Make sure the buffer is large enough for the bytes we want to peek. const PEEK_BYTES: usize = 4096; if buf.len() < PEEK_BYTES { @@ -198,4 +214,3 @@ impl Drop for Data { } } } - diff --git a/lib/src/data/data_stream.rs b/lib/src/data/data_stream.rs index 1eeb8838..a63371d4 100644 --- a/lib/src/data/data_stream.rs +++ b/lib/src/data/data_stream.rs @@ -1,12 +1,80 @@ use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take}; -use std::net::Shutdown; +use std::net::{SocketAddr, Shutdown}; +use std::time::Duration; + +#[cfg(feature = "tls")] use hyper_rustls::WrappedStream as RustlsStream; use http::hyper::net::{HttpStream, NetworkStream}; use http::hyper::h1::HttpReader; -pub type StreamReader = HttpReader; +pub type StreamReader = HttpReader; pub type InnerStream = Chain>>, BufReader>; +#[derive(Clone)] +pub enum HyperNetStream { + Http(HttpStream), + #[cfg(feature = "tls")] + Https(RustlsStream) +} + +macro_rules! with_inner { + ($net:expr, |$stream:ident| $body:expr) => ({ + trace!("{}:{}", file!(), line!()); + match *$net { + HyperNetStream::Http(ref $stream) => $body, + #[cfg(feature = "tls")] HyperNetStream::Https(ref $stream) => $body + } + }); + ($net:expr, |mut $stream:ident| $body:expr) => ({ + trace!("{}:{}", file!(), line!()); + match *$net { + HyperNetStream::Http(ref mut $stream) => $body, + #[cfg(feature = "tls")] HyperNetStream::Https(ref mut $stream) => $body + } + }) +} + +impl io::Read for HyperNetStream { + #[inline(always)] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + with_inner!(self, |mut stream| io::Read::read(stream, buf)) + } +} + +impl io::Write for HyperNetStream { + #[inline(always)] + fn write(&mut self, buf: &[u8]) -> io::Result { + with_inner!(self, |mut stream| io::Write::write(stream, buf)) + } + + #[inline(always)] + fn flush(&mut self) -> io::Result<()> { + with_inner!(self, |mut stream| io::Write::flush(stream)) + } +} + +impl NetworkStream for HyperNetStream { + #[inline(always)] + fn peer_addr(&mut self) -> io::Result { + with_inner!(self, |mut stream| NetworkStream::peer_addr(stream)) + } + + #[inline(always)] + fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + with_inner!(self, |stream| NetworkStream::set_read_timeout(stream, dur)) + } + + #[inline(always)] + fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + with_inner!(self, |stream| NetworkStream::set_write_timeout(stream, dur)) + } + + #[inline(always)] + fn close(&mut self, how: Shutdown) -> io::Result<()> { + with_inner!(self, |mut stream| NetworkStream::close(stream, how)) + } +} + /// Raw data stream of a request body. /// /// This stream can only be obtained by calling @@ -15,33 +83,38 @@ pub type InnerStream = Chain>>, BufReader>; /// Instead, it must be used as an opaque `Read` or `BufRead` structure. pub struct DataStream { stream: InnerStream, - network: HttpStream, + network: HyperNetStream, } impl DataStream { - pub(crate) fn new(stream: InnerStream, network: HttpStream) -> DataStream { - DataStream { stream: stream, network: network, } + #[inline(always)] + pub(crate) fn new(stream: InnerStream, network: HyperNetStream) -> DataStream { + DataStream { stream, network } } } impl Read for DataStream { + #[inline(always)] fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.read(buf) } } impl BufRead for DataStream { + #[inline(always)] fn fill_buf(&mut self) -> io::Result<&[u8]> { self.stream.fill_buf() } + #[inline(always)] fn consume(&mut self, amt: usize) { self.stream.consume(amt) } } +// pub fn kill_stream(stream: &mut S, network: &mut HyperNetStream) { pub fn kill_stream(stream: &mut S, network: &mut N) { - io::copy(&mut stream.take(1024), &mut io::sink()).expect("sink"); + io::copy(&mut stream.take(1024), &mut io::sink()).expect("kill_stream: sink"); // If there are any more bytes, kill it. let mut buf = [0]; @@ -61,4 +134,3 @@ impl Drop for DataStream { kill_stream(&mut self.stream, &mut self.network); } } - diff --git a/lib/src/error.rs b/lib/src/error.rs index a9ce4c02..ebbf14c5 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -111,6 +111,7 @@ impl LaunchError { } impl From for LaunchError { + #[inline] fn from(error: hyper::Error) -> LaunchError { match error { hyper::Error::Io(e) => LaunchError::new(LaunchErrorKind::Io(e)), @@ -120,6 +121,7 @@ impl From for LaunchError { } impl fmt::Display for LaunchErrorKind { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), @@ -129,6 +131,7 @@ impl fmt::Display for LaunchErrorKind { } impl fmt::Debug for LaunchError { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.mark_handled(); write!(f, "{:?}", self.kind()) @@ -136,6 +139,7 @@ impl fmt::Debug for LaunchError { } impl fmt::Display for LaunchError { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.mark_handled(); write!(f, "{}", self.kind()) @@ -143,6 +147,7 @@ impl fmt::Display for LaunchError { } impl ::std::error::Error for LaunchError { + #[inline] fn description(&self) -> &str { self.mark_handled(); match *self.kind() { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f69710f8..b2842639 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,6 +7,7 @@ #![feature(lookup_host)] #![feature(plugin)] #![feature(never_type)] +#![feature(concat_idents)] #![plugin(pear_codegen)] @@ -99,6 +100,8 @@ #[macro_use] extern crate log; #[macro_use] extern crate pear; +#[cfg(feature = "tls")] extern crate rustls; +#[cfg(feature = "tls")] extern crate hyper_rustls; extern crate term_painter; extern crate hyper; extern crate url; diff --git a/lib/src/logger.rs b/lib/src/logger.rs index 39ec56f7..2550dd7c 100644 --- a/lib/src/logger.rs +++ b/lib/src/logger.rs @@ -32,13 +32,14 @@ impl LoggingLevel { } impl FromStr for LoggingLevel { - type Err = (); + type Err = &'static str; + fn from_str(s: &str) -> Result { let level = match s { "critical" => LoggingLevel::Critical, "normal" => LoggingLevel::Normal, "debug" => LoggingLevel::Debug, - _ => return Err(()) + _ => return Err("a log level (debug, normal, critical)") }; Ok(level) @@ -87,9 +88,10 @@ impl Log for RocketLogger { return; } - // Don't print Hyper's messages unless Debug is enabled. + // Don't print Hyper or Rustls messages unless debug is enabled. let from_hyper = record.location().module_path().starts_with("hyper::"); - if from_hyper && self.0 != LoggingLevel::Debug { + let from_rustls = record.location().module_path().starts_with("rustls::"); + if self.0 != LoggingLevel::Debug && (from_hyper || from_rustls) { return; } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 22734ea6..0dae48b9 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -6,9 +6,9 @@ use std::io::{self, Write}; use term_painter::Color::*; use term_painter::ToStyle; - use state::Container; +#[cfg(feature = "tls")] use hyper_rustls::TlsServer; use {logger, handler}; use ext::ReadExt; use config::{self, Config}; @@ -74,6 +74,35 @@ impl hyper::Handler for Rocket { } } +// This macro is a terrible hack to get around Hyper's Server type. What we +// want is to use almost exactly the same launch code when we're serving over +// HTTPS as over HTTP. But Hyper forces two different types, so we can't use the +// same code, at least not trivially. These macros get around that by passing in +// the same code as a continuation in `$continue`. This wouldn't work as a +// regular function taking in a closure because the types of the inputs to the +// closure would be different depending on whether TLS was enabled or not. +#[cfg(not(feature = "tls"))] +macro_rules! serve { + ($rocket:expr, $addr:expr, |$server:ident, $proto:ident| $continue:expr) => ({ + let ($proto, $server) = ("http://", hyper::Server::http($addr)); + $continue + }) +} + +#[cfg(feature = "tls")] +macro_rules! serve { + ($rocket:expr, $addr:expr, |$server:ident, $proto:ident| $continue:expr) => ({ + if let Some(ref tls) = $rocket.config.tls { + let tls = TlsServer::new(tls.certs.clone(), tls.key.clone()); + let ($proto, $server) = ("https://", hyper::Server::https($addr, tls)); + $continue + } else { + let ($proto, $server) = ("http://", hyper::Server::http($addr)); + $continue + } + }) +} + impl Rocket { #[inline] fn issue_response(&self, mut response: Response, hyp_res: hyper::FreshResponse) { @@ -262,7 +291,11 @@ impl Rocket { Outcome::Forward(data) } - // TODO: DOC. + // Finds the error catcher for the status `status` and executes it fo the + // given request `req`. If a user has registere a catcher for `status`, the + // catcher is called. If the catcher fails to return a good response, the + // 500 catcher is executed. if there is no registered catcher for `status`, + // the default catcher is used. fn handle_error<'r>(&self, status: Status, req: &'r Request) -> Response<'r> { warn_!("Responding with {} catcher.", Red.paint(&status)); @@ -350,6 +383,16 @@ impl Rocket { info_!("workers: {}", White.paint(config.workers)); info_!("session key: {}", White.paint(config.session_key.kind())); + let tls_configured = config.tls.is_some(); + if tls_configured && cfg!(feature = "tls") { + info_!("tls: {}", White.paint("enabled")); + } else { + info_!("tls: {}", White.paint("disabled")); + if tls_configured { + warn_!("tls is configured, but the tls feature is disabled"); + } + } + for (name, value) in config.extras() { info_!("{} {}: {}", Yellow.paint("[extra]"), name, White.paint(value)); } @@ -553,22 +596,24 @@ impl Rocket { } let full_addr = format!("{}:{}", self.config.address, self.config.port); - let server = match hyper::Server::http(full_addr.as_str()) { - Ok(hyper_server) => hyper_server, - Err(e) => return LaunchError::from(e) - }; + serve!(self, &full_addr, |server, proto| { + let server = match server { + Ok(server) => server, + Err(e) => return LaunchError::from(e) + }; - info!("🚀 {} {}{}", - White.paint("Rocket has launched from"), - White.bold().paint("http://"), - White.bold().paint(&full_addr)); + info!("🚀 {} {}{}", + White.paint("Rocket has launched from"), + White.bold().paint(proto), + White.bold().paint(&full_addr)); - let threads = self.config.workers as usize; - if let Err(e) = server.handle_threads(self, threads) { - return LaunchError::from(e); - } + let threads = self.config.workers as usize; + if let Err(e) = server.handle_threads(self, threads) { + return LaunchError::from(e); + } - unreachable!("the call to `handle_threads` should block on success") + unreachable!("the call to `handle_threads` should block on success") + }) } /// Retrieves all of the mounted routes. From 6f29696b4fcd460c9dff7c15a480dd42469839c7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 13 Apr 2017 01:13:25 -0700 Subject: [PATCH 084/297] Make TLS misconfig an error. Always print launch message. --- examples/hello_tls/Cargo.toml | 2 +- lib/src/logger.rs | 18 +++++++++++++++++- lib/src/rocket.rs | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/hello_tls/Cargo.toml b/examples/hello_tls/Cargo.toml index d691440e..24d37d21 100644 --- a/examples/hello_tls/Cargo.toml +++ b/examples/hello_tls/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" workspace = "../../" [dependencies] -rocket = { path = "../../lib", features = ["tls"] } +rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } [dev-dependencies] diff --git a/lib/src/logger.rs b/lib/src/logger.rs index 2550dd7c..e0569c3b 100644 --- a/lib/src/logger.rs +++ b/lib/src/logger.rs @@ -66,6 +66,13 @@ macro_rules! log_ { }; } +#[doc(hidden)] #[macro_export] +macro_rules! launch_info { + ($format:expr, $($args:expr),*) => { + error!(target: "launch", $format, $($args),*) + } +} + #[doc(hidden)] #[macro_export] macro_rules! error_ { ($($args:expr),+) => { log_!(error: $($args),+); }; } #[doc(hidden)] #[macro_export] @@ -78,6 +85,7 @@ macro_rules! debug_ { ($($args:expr),+) => { log_!(debug: $($args),+); }; } macro_rules! warn_ { ($($args:expr),+) => { log_!(warn: $($args),+); }; } impl Log for RocketLogger { + #[inline(always)] fn enabled(&self, md: &LogMetadata) -> bool { md.level() <= self.0.max_log_level() } @@ -88,6 +96,14 @@ impl Log for RocketLogger { return; } + // We use the `launch_info` macro to "fake" a high priority info + // message. We want to print the message unless the user uses a custom + // drain, so we set it's status to critical, but reset it here to info. + let level = match record.target() { + "launch" => Info, + _ => record.level() + }; + // Don't print Hyper or Rustls messages unless debug is enabled. let from_hyper = record.location().module_path().starts_with("hyper::"); let from_rustls = record.location().module_path().starts_with("rustls::"); @@ -101,7 +117,7 @@ impl Log for RocketLogger { } use log::LogLevel::*; - match record.level() { + match level { Info => println!("{}", Blue.paint(record.args())), Trace => println!("{}", Magenta.paint(record.args())), Error => { diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 0dae48b9..ac41fbdb 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -387,9 +387,9 @@ impl Rocket { if tls_configured && cfg!(feature = "tls") { info_!("tls: {}", White.paint("enabled")); } else { - info_!("tls: {}", White.paint("disabled")); + error_!("tls: {}", White.paint("disabled")); if tls_configured { - warn_!("tls is configured, but the tls feature is disabled"); + error_!("tls is configured, but the tls feature is disabled"); } } @@ -602,7 +602,7 @@ impl Rocket { Err(e) => return LaunchError::from(e) }; - info!("🚀 {} {}{}", + launch_info!("🚀 {} {}{}", White.paint("Rocket has launched from"), White.bold().paint(proto), White.bold().paint(&full_addr)); From e50164115b714821881da144109be11214431f73 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 13 Apr 2017 01:30:48 -0700 Subject: [PATCH 085/297] Only emit TLS disabled error on misconfig. --- lib/src/rocket.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index ac41fbdb..262d03a1 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -387,9 +387,11 @@ impl Rocket { if tls_configured && cfg!(feature = "tls") { info_!("tls: {}", White.paint("enabled")); } else { - error_!("tls: {}", White.paint("disabled")); if tls_configured { + error_!("tls: {}", White.paint("disabled")); error_!("tls is configured, but the tls feature is disabled"); + } else { + info_!("tls: {}", White.paint("disabled")); } } From a25a3c69c6ce5cf147fcc7848082eef04904e59c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 13 Apr 2017 02:36:51 -0700 Subject: [PATCH 086/297] Cache parsed ContentType and Accept headers. This is a breaking change. `Request::content_type` now returns a borrow to `ContentType`. `FromRequest` for `ContentType` is no longer implemented. Instead, `FromRequest` for `&ContentType` is implemented. --- lib/Cargo.toml | 2 +- lib/src/data/from_data.rs | 2 +- lib/src/request/from_request.rs | 4 +-- lib/src/request/request.rs | 56 ++++++++++++++++++++------------- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 2f998332..a322a97d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -25,7 +25,7 @@ url = "1" hyper = { version = "0.10.4", default-features = false } toml = { version = "0.2", default-features = false } num_cpus = "1" -state = "0.2" +state = "0.2.1" time = "0.1" memchr = "1" base64 = "0.4" diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index 74afe33a..57ad8203 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -104,7 +104,7 @@ impl<'a, S, E> IntoOutcome for Result { /// fn from_data(req: &Request, data: Data) -> data::Outcome { /// // Ensure the content type is correct before opening the data. /// let person_ct = ContentType::new("application", "x-person"); -/// if req.content_type() != Some(person_ct) { +/// if req.content_type() != Some(&person_ct) { /// return Outcome::Forward(data); /// } /// diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 9d4b416b..0f1dc395 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -220,7 +220,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Session<'a> { } } -impl<'a, 'r> FromRequest<'a, 'r> for Accept { +impl<'a, 'r> FromRequest<'a, 'r> for &'a Accept { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome { @@ -231,7 +231,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Accept { } } -impl<'a, 'r> FromRequest<'a, 'r> for ContentType { +impl<'a, 'r> FromRequest<'a, 'r> for &'a ContentType { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome { diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 07ce0101..d5df43ad 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -6,7 +6,7 @@ use std::str; use term_painter::Color::*; use term_painter::ToStyle; -use state::Container; +use state::{Container, Storage}; use error::Error; use super::{FromParam, FromSegments}; @@ -15,7 +15,6 @@ use router::Route; use http::uri::{URI, Segments}; use http::{Method, Header, HeaderMap, Cookies, Session, CookieJar, Key}; use http::{RawStr, ContentType, Accept, MediaType}; -use http::parse::media_type; use http::hyper; struct PresetState<'r> { @@ -28,6 +27,8 @@ struct RequestState<'r> { params: RefCell>, cookies: RefCell, session: RefCell, + accept: Storage>, + content_type: Storage>, } /// The type of an incoming web request. @@ -71,6 +72,8 @@ impl<'r> Request<'r> { params: RefCell::new(Vec::new()), cookies: RefCell::new(CookieJar::new()), session: RefCell::new(CookieJar::new()), + accept: Storage::new(), + content_type: Storage::new(), } } } @@ -238,11 +241,11 @@ impl<'r> Request<'r> { /// let mut request = Request::new(Method::Get, "/uri"); /// assert!(request.headers().is_empty()); /// - /// request.add_header(ContentType::HTML); - /// assert_eq!(request.content_type(), Some(ContentType::HTML)); + /// request.add_header(ContentType::Any); + /// assert_eq!(request.headers().get_one("Content-Type"), Some("*/*")); /// - /// request.replace_header(ContentType::JSON); - /// assert_eq!(request.content_type(), Some(ContentType::JSON)); + /// request.replace_header(ContentType::PNG); + /// assert_eq!(request.headers().get_one("Content-Type"), Some("image/png")); /// ``` #[inline(always)] pub fn replace_header>>(&mut self, header: H) { @@ -307,7 +310,9 @@ impl<'r> Request<'r> { } /// Returns `Some` of the Content-Type header of `self`. If the header is - /// not present, returns `None`. + /// not present, returns `None`. The Content-Type header is cached after the + /// first call to this function. As a result, subsequent calls will always + /// return the same value. /// /// # Example /// @@ -317,38 +322,47 @@ impl<'r> Request<'r> { /// /// let mut request = Request::new(Method::Get, "/uri"); /// assert_eq!(request.content_type(), None); + /// ``` /// - /// request.replace_header(ContentType::JSON); - /// assert_eq!(request.content_type(), Some(ContentType::JSON)); + /// ```rust + /// use rocket::Request; + /// use rocket::http::{Method, ContentType}; + /// + /// let mut request = Request::new(Method::Get, "/uri"); + /// request.add_header(ContentType::JSON); + /// assert_eq!(request.content_type(), Some(&ContentType::JSON)); /// ``` #[inline(always)] - pub fn content_type(&self) -> Option { - // FIXME: Don't reparse each time! Use RC? Smarter than that? - // Use state::Storage! - self.headers().get_one("Content-Type").and_then(|value| value.parse().ok()) + pub fn content_type(&self) -> Option<&ContentType> { + self.extra.content_type.get_or_set(|| { + self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()) + }).as_ref() } #[inline(always)] - pub fn accept(&self) -> Option { - self.headers().get_one("Accept").and_then(|v| v.parse().ok()) + pub fn accept(&self) -> Option<&Accept> { + self.extra.accept.get_or_set(|| { + self.headers().get_one("Accept").and_then(|v| v.parse().ok()) + }).as_ref() } #[inline(always)] - pub fn accept_first(&self) -> Option { - self.headers().get_one("Accept").and_then(|mut v| media_type(&mut v).ok()) + pub fn accept_first(&self) -> Option<&MediaType> { + self.accept().and_then(|accept| accept.first()).map(|wmt| wmt.media_type()) } #[inline(always)] - pub fn format(&self) -> Option { + pub fn format(&self) -> Option<&MediaType> { + static ANY: MediaType = MediaType::Any; if self.method.supports_payload() { - self.content_type().map(|ct| ct.into_media_type()) + self.content_type().map(|ct| ct.media_type()) } else { // FIXME: Should we be using `accept_first` or `preferred`? Or // should we be checking neither and instead pass things through // where the client accepts the thing at all? self.accept() - .map(|accept| accept.preferred().media_type().clone()) - .or(Some(MediaType::Any)) + .map(|accept| accept.preferred().media_type()) + .or(Some(&ANY)) } } From da1f6264e49bdffeff0dd910082cb68cad1eb24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20Desr=C3=A9?= Date: Thu, 13 Apr 2017 13:20:57 -0700 Subject: [PATCH 087/297] Enable the "tls" feature in the hello_tls example. --- examples/hello_tls/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hello_tls/Cargo.toml b/examples/hello_tls/Cargo.toml index 24d37d21..d691440e 100644 --- a/examples/hello_tls/Cargo.toml +++ b/examples/hello_tls/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" workspace = "../../" [dependencies] -rocket = { path = "../../lib" } +rocket = { path = "../../lib", features = ["tls"] } rocket_codegen = { path = "../../codegen" } [dev-dependencies] From 2e54a1f74d79cbb2559b308bf7b4edc0d09ec2cf Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 00:43:24 -0700 Subject: [PATCH 088/297] Don't use &str where RawStr is now preferred. --- README.md | 2 +- codegen/tests/compile-fail/absolute-mount-paths.rs | 4 ++-- codegen/tests/compile-fail/bad-ident-argument.rs | 2 +- codegen/tests/compile-fail/ignored_params.rs | 2 +- codegen/tests/compile-fail/malformed-param-list.rs | 8 ++++---- lib/Cargo.toml | 5 +---- lib/src/request/form/mod.rs | 4 ++++ lib/src/response/responder.rs | 2 +- lib/src/router/mod.rs | 4 ++-- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d4998981..cb9ddde3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ expressibility, and speed. Here's an example of a complete Rocket application: extern crate rocket; #[get("//")] -fn hello(name: &str, age: u8) -> String { +fn hello(name: String, age: u8) -> String { format!("Hello, {} year old named {}!", age, name) } diff --git a/codegen/tests/compile-fail/absolute-mount-paths.rs b/codegen/tests/compile-fail/absolute-mount-paths.rs index a484b27b..96637bde 100644 --- a/codegen/tests/compile-fail/absolute-mount-paths.rs +++ b/codegen/tests/compile-fail/absolute-mount-paths.rs @@ -5,9 +5,9 @@ fn get() -> &'static str { "hi" } #[get("")] //~ ERROR absolute -fn get1(name: &str) -> &'static str { "hi" } +fn get1(id: usize) -> &'static str { "hi" } #[get("a/b/c")] //~ ERROR absolute -fn get2(name: &str) -> &'static str { "hi" } +fn get2(id: usize) -> &'static str { "hi" } fn main() { } diff --git a/codegen/tests/compile-fail/bad-ident-argument.rs b/codegen/tests/compile-fail/bad-ident-argument.rs index 4c7de96e..4b544f02 100644 --- a/codegen/tests/compile-fail/bad-ident-argument.rs +++ b/codegen/tests/compile-fail/bad-ident-argument.rs @@ -4,6 +4,6 @@ extern crate rocket; #[get("/")] -fn get(_: &str) -> &'static str { "hi" } //~ ERROR argument +fn get(_: usize) -> &'static str { "hi" } //~ ERROR argument fn main() { } diff --git a/codegen/tests/compile-fail/ignored_params.rs b/codegen/tests/compile-fail/ignored_params.rs index 0aa5b9e3..225eedc1 100644 --- a/codegen/tests/compile-fail/ignored_params.rs +++ b/codegen/tests/compile-fail/ignored_params.rs @@ -2,7 +2,7 @@ #![plugin(rocket_codegen)] #[get("/")] //~ ERROR 'name' is declared -fn get(other: &str) -> &'static str { "hi" } //~ ERROR isn't in the function +fn get(other: usize) -> &'static str { "hi" } //~ ERROR isn't in the function #[get("/a?")] //~ ERROR 'r' is declared fn get1() -> &'static str { "hi" } //~ ERROR isn't in the function diff --git a/codegen/tests/compile-fail/malformed-param-list.rs b/codegen/tests/compile-fail/malformed-param-list.rs index cc51808b..46018b17 100644 --- a/codegen/tests/compile-fail/malformed-param-list.rs +++ b/codegen/tests/compile-fail/malformed-param-list.rs @@ -4,11 +4,11 @@ #[get("/><")] //~ ERROR malformed fn get() -> &'static str { "hi" } -#[get("/<")] //~ ERROR malformed -fn get1(name: &str) -> &'static str { "hi" } +#[get("/<")] //~ ERROR malformed +fn get1(id: usize) -> &'static str { "hi" } -#[get("/<<<<")] //~ ERROR malformed -fn get2(name: &str) -> &'static str { "hi" } +#[get("/<<<<")] //~ ERROR malformed +fn get2(id: usize) -> &'static str { "hi" } #[get("/")] //~ ERROR identifiers fn get3() -> &'static str { "hi" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a322a97d..6b33f4df 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -33,6 +33,7 @@ smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" } pear = "0.0.8" pear_codegen = "0.0.8" rustls = { version = "0.5.8", optional = true } +cookie = { version = "0.7.4", features = ["percent-encode", "secure"] } [dependencies.hyper-rustls] git = "https://github.com/SergioBenitez/hyper-rustls" @@ -40,10 +41,6 @@ default-features = false features = ["server"] optional = true -[dependencies.cookie] -version = "0.7.4" -features = ["percent-encode", "secure"] - [dev-dependencies] lazy_static = "0.2" rocket_codegen = { version = "0.2.4", path = "../codegen" } diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs index edcef839..16c43a64 100644 --- a/lib/src/request/form/mod.rs +++ b/lib/src/request/form/mod.rs @@ -181,17 +181,20 @@ impl FormResult { impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { /// Immutably borrow the parsed type. + #[inline(always)] pub fn get(&'f self) -> &'f T { &self.object } /// Mutably borrow the parsed type. + #[inline(always)] pub fn get_mut(&'f mut self) -> &'f mut T { &mut self.object } /// Returns the raw form string that was used to parse the encapsulated /// object. + #[inline(always)] pub fn raw_form_string(&self) -> &str { &self.form_string } @@ -240,6 +243,7 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { impl<'f, T: FromForm<'f> + 'static> Form<'f, T> { /// Consume this object and move out the parsed object. + #[inline(always)] pub fn into_inner(self) -> T { self.object } diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs index fb43fb95..23ad7744 100644 --- a/lib/src/response/responder.rs +++ b/lib/src/response/responder.rs @@ -190,7 +190,7 @@ impl<'r> Responder<'r> for &'r str { } } -/// Returns a response with Content-Type `text/html` and a fixed-size body +/// Returns a response with Content-Type `text/plain` and a fixed-size body /// containing the string `self`. Always returns `Ok`. impl Responder<'static> for String { fn respond(self) -> Result, Status> { diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 39dcd3eb..799e9298 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -292,7 +292,7 @@ mod test { assert!(routed_to.len() == expected.len()); for (got, expected) in routed_to.iter().zip(expected.iter()) { assert_eq!(got.rank, expected.0); - assert_eq!(got.path.as_str() as &str, expected.1); + assert_eq!(got.path.as_str(), expected.1); } }) } @@ -361,7 +361,7 @@ mod test { let expected = &[$($want),+]; assert!(routed_to.len() == expected.len()); for (got, expected) in routed_to.iter().zip(expected.iter()) { - assert_eq!(got.path.as_str() as &str, expected as &str); + assert_eq!(got.path.as_str(), expected as &str); } }) } From 0d674c57fd0aab04c9cef16077c28531b4c302de Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 01:21:06 -0700 Subject: [PATCH 089/297] Return `HeaderMap` from Response::headers(). Remove Response::header_values(). This is a breaking change. A call to `Response::headers()` can be replaced with `Response::headers().iter()`. A call to `Response::header_values()` can be replaced with `Response::headers().get()`. --- examples/cookies/src/tests.rs | 4 +- examples/handlebars_templates/src/tests.rs | 2 +- examples/optional_redirect/src/tests.rs | 2 +- examples/redirect/src/tests.rs | 2 +- lib/src/request/request.rs | 3 +- lib/src/response/responder.rs | 2 +- lib/src/response/response.rs | 91 ++++++++------------ lib/src/rocket.rs | 2 +- lib/tests/head_handling.rs | 4 +- lib/tests/redirect_from_catcher-issue-113.rs | 2 +- 10 files changed, 46 insertions(+), 68 deletions(-) diff --git a/examples/cookies/src/tests.rs b/examples/cookies/src/tests.rs index af9724d3..0528d963 100644 --- a/examples/cookies/src/tests.rs +++ b/examples/cookies/src/tests.rs @@ -12,8 +12,8 @@ fn test_submit() { .header(ContentType::Form) .body("message=Hello from Rocket!"); let response = request.dispatch_with(&rocket); - let cookie_headers: Vec<_> = response.header_values("Set-Cookie").collect(); - let location_headers: Vec<_> = response.header_values("Location").collect(); + let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect(); + let location_headers: Vec<_> = response.headers().get("Location").collect(); assert_eq!(response.status(), Status::SeeOther); assert_eq!(cookie_headers, vec!["message=Hello%20from%20Rocket!".to_string()]); diff --git a/examples/handlebars_templates/src/tests.rs b/examples/handlebars_templates/src/tests.rs index 18654b3a..c72a116b 100644 --- a/examples/handlebars_templates/src/tests.rs +++ b/examples/handlebars_templates/src/tests.rs @@ -25,7 +25,7 @@ fn test_root() { assert_eq!(response.status(), Status::SeeOther); assert!(response.body().is_none()); - let location_headers: Vec<_> = response.header_values("Location").collect(); + let location_headers: Vec<_> = response.headers().get("Location").collect(); assert_eq!(location_headers, vec!["/hello/Unknown"]); }); } diff --git a/examples/optional_redirect/src/tests.rs b/examples/optional_redirect/src/tests.rs index d7e50c07..c8680f1d 100644 --- a/examples/optional_redirect/src/tests.rs +++ b/examples/optional_redirect/src/tests.rs @@ -18,7 +18,7 @@ fn test_303(uri: &str, expected_location: &str) { .mount("/", routes![super::root, super::user, super::login]); let mut request = MockRequest::new(Method::Get, uri); let response = request.dispatch_with(&rocket); - let location_headers: Vec<_> = response.header_values("Location").collect(); + let location_headers: Vec<_> = response.headers().get("Location").collect(); assert_eq!(response.status(), Status::SeeOther); assert_eq!(location_headers, vec![expected_location]); diff --git a/examples/redirect/src/tests.rs b/examples/redirect/src/tests.rs index 69848b0d..f14e85eb 100644 --- a/examples/redirect/src/tests.rs +++ b/examples/redirect/src/tests.rs @@ -18,7 +18,7 @@ fn test_root() { run_test!("/", |mut response: Response| { assert!(response.body().is_none()); assert_eq!(response.status(), Status::SeeOther); - for h in response.headers() { + for h in response.headers().iter() { match h.name.as_str() { "Location" => assert_eq!(h.value, "/login"), "Content-Length" => assert_eq!(h.value.parse::().unwrap(), 0), diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index d5df43ad..323261eb 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -192,7 +192,8 @@ impl<'r> Request<'r> { self.remote = Some(address); } - /// Returns a `HeaderMap` of all of the headers in `self`. + /// Returns a [HeaderMap](/rocket/http/struct.HeaderMap.html) of all of the + /// headers in `self`. /// /// # Example /// diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs index 23ad7744..d8b0ebd7 100644 --- a/lib/src/response/responder.rs +++ b/lib/src/response/responder.rs @@ -177,7 +177,7 @@ pub trait Responder<'r> { /// let body_string = response.body().and_then(|b| b.into_string()); /// assert_eq!(body_string, Some("Hello".to_string())); /// -/// let content_type: Vec<_> = response.header_values("Content-Type").collect(); +/// let content_type: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(content_type.len(), 1); /// assert_eq!(content_type[0], ContentType::Plain.to_string()); /// ``` diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 68031529..156b9237 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -243,7 +243,7 @@ impl<'r> ResponseBuilder<'r> { /// .header(ContentType::HTML) /// .finalize(); /// - /// assert_eq!(response.header_values("Content-Type").count(), 1); + /// assert_eq!(response.headers().get("Content-Type").count(), 1); /// ``` #[inline(always)] pub fn header<'h: 'r, H>(&mut self, header: H) -> &mut ResponseBuilder<'r> @@ -274,7 +274,7 @@ impl<'r> ResponseBuilder<'r> { /// .header_adjoin(Accept::text()) /// .finalize(); /// - /// assert_eq!(response.header_values("Accept").count(), 2); + /// assert_eq!(response.headers().get("Accept").count(), 2); /// ``` #[inline(always)] pub fn header_adjoin<'h: 'r, H>(&mut self, header: H) -> &mut ResponseBuilder<'r> @@ -299,7 +299,7 @@ impl<'r> ResponseBuilder<'r> { /// .raw_header("X-Custom", "second") /// .finalize(); /// - /// assert_eq!(response.header_values("X-Custom").count(), 1); + /// assert_eq!(response.headers().get("X-Custom").count(), 1); /// ``` #[inline(always)] pub fn raw_header<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) @@ -326,7 +326,7 @@ impl<'r> ResponseBuilder<'r> { /// .raw_header_adjoin("X-Custom", "second") /// .finalize(); /// - /// assert_eq!(response.header_values("X-Custom").count(), 2); + /// assert_eq!(response.headers().get("X-Custom").count(), 2); /// ``` #[inline(always)] pub fn raw_header_adjoin<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) @@ -444,12 +444,12 @@ impl<'r> ResponseBuilder<'r> { /// assert_eq!(response.status(), Status::NotFound); /// /// # { - /// let ctype: Vec<_> = response.header_values("Content-Type").collect(); + /// let ctype: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(ctype, vec![ContentType::HTML.to_string()]); /// # } /// /// # { - /// let custom_values: Vec<_> = response.header_values("X-Custom").collect(); + /// let custom_values: Vec<_> = response.headers().get("X-Custom").collect(); /// assert_eq!(custom_values, vec!["value 1"]); /// # } /// ``` @@ -488,12 +488,12 @@ impl<'r> ResponseBuilder<'r> { /// assert_eq!(response.status(), Status::ImATeapot); /// /// # { - /// let ctype: Vec<_> = response.header_values("Content-Type").collect(); + /// let ctype: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(ctype, vec![ContentType::HTML.to_string()]); /// # } /// /// # { - /// let custom_values: Vec<_> = response.header_values("X-Custom").collect(); + /// let custom_values: Vec<_> = response.headers().get("X-Custom").collect(); /// assert_eq!(custom_values, vec!["value 2", "value 3", "value 1"]); /// # } /// ``` @@ -562,7 +562,7 @@ impl<'r> Response<'r> { /// let mut response = Response::new(); /// /// assert_eq!(response.status(), Status::Ok); - /// assert_eq!(response.headers().count(), 0); + /// assert_eq!(response.headers().len(), 0); /// assert!(response.body().is_none()); /// ``` #[inline(always)] @@ -660,11 +660,8 @@ impl<'r> Response<'r> { self.status = Some(Status::new(code, reason)); } - /// Returns an iterator over all of the headers stored in `self`. Multiple - /// headers with the same name may be returned, but all of the headers with - /// the same name will be appear in a group in the iterator. The values in - /// this group will be emitted in the order they were added to `self`. Aside - /// from this grouping, there are no other ordering guarantees. + /// Returns a [HeaderMap](/rocket/http/struct.HeaderMap.html) of all of the + /// headers in `self`. /// /// # Example /// @@ -676,34 +673,14 @@ impl<'r> Response<'r> { /// response.adjoin_raw_header("X-Custom", "1"); /// response.adjoin_raw_header("X-Custom", "2"); /// - /// let mut headers = response.headers(); - /// assert_eq!(headers.next(), Some(Header::new("X-Custom", "1"))); - /// assert_eq!(headers.next(), Some(Header::new("X-Custom", "2"))); - /// assert_eq!(headers.next(), None); + /// let mut custom_headers = response.headers().iter(); + /// assert_eq!(custom_headers.next(), Some(Header::new("X-Custom", "1"))); + /// assert_eq!(custom_headers.next(), Some(Header::new("X-Custom", "2"))); + /// assert_eq!(custom_headers.next(), None); /// ``` #[inline(always)] - pub fn headers<'a>(&'a self) -> impl Iterator> { - self.headers.iter() - } - - /// Returns an iterator over all of the values stored in `self` for the - /// header with name `name`. The values are returned in FIFO order. - /// - /// # Example - /// - /// ```rust - /// use rocket::Response; - /// - /// let mut response = Response::new(); - /// response.adjoin_raw_header("X-Custom", "1"); - /// response.adjoin_raw_header("X-Custom", "2"); - /// - /// let values: Vec<_> = response.header_values("X-Custom").collect(); - /// assert_eq!(values, vec!["1", "2"]); - /// ``` - #[inline(always)] - pub fn header_values<'h>(&'h self, name: &str) -> impl Iterator { - self.headers.get(name) + pub fn headers(&self) -> &HeaderMap<'r> { + &self.headers } /// Sets the header `header` in `self`. Any existing headers with the name @@ -721,12 +698,12 @@ impl<'r> Response<'r> { /// let mut response = Response::new(); /// /// response.set_header(ContentType::HTML); - /// assert_eq!(response.headers().next(), Some(ContentType::HTML.into())); - /// assert_eq!(response.headers().count(), 1); + /// assert_eq!(response.headers().iter().next(), Some(ContentType::HTML.into())); + /// assert_eq!(response.headers().len(), 1); /// /// response.set_header(ContentType::JSON); - /// assert_eq!(response.headers().next(), Some(ContentType::JSON.into())); - /// assert_eq!(response.headers().count(), 1); + /// assert_eq!(response.headers().iter().next(), Some(ContentType::JSON.into())); + /// assert_eq!(response.headers().len(), 1); /// ``` #[inline(always)] pub fn set_header<'h: 'r, H: Into>>(&mut self, header: H) -> bool { @@ -747,12 +724,12 @@ impl<'r> Response<'r> { /// let mut response = Response::new(); /// /// response.set_raw_header("X-Custom", "1"); - /// assert_eq!(response.headers().next(), Some(Header::new("X-Custom", "1"))); - /// assert_eq!(response.headers().count(), 1); + /// assert_eq!(response.headers().get_one("X-Custom"), Some("1")); + /// assert_eq!(response.headers().len(), 1); /// /// response.set_raw_header("X-Custom", "2"); - /// assert_eq!(response.headers().next(), Some(Header::new("X-Custom", "2"))); - /// assert_eq!(response.headers().count(), 1); + /// assert_eq!(response.headers().get_one("X-Custom"), Some("2")); + /// assert_eq!(response.headers().len(), 1); /// ``` #[inline(always)] pub fn set_raw_header<'a: 'r, 'b: 'r, N, V>(&mut self, name: N, value: V) -> bool @@ -778,7 +755,7 @@ impl<'r> Response<'r> { /// response.adjoin_header(Accept::json()); /// response.adjoin_header(Accept::text()); /// - /// let mut accept_headers = response.headers(); + /// let mut accept_headers = response.headers().iter(); /// assert_eq!(accept_headers.next(), Some(Accept::json().into())); /// assert_eq!(accept_headers.next(), Some(Accept::text().into())); /// assert_eq!(accept_headers.next(), None); @@ -805,7 +782,7 @@ impl<'r> Response<'r> { /// response.adjoin_raw_header("X-Custom", "one"); /// response.adjoin_raw_header("X-Custom", "two"); /// - /// let mut custom_headers = response.headers(); + /// let mut custom_headers = response.headers().iter(); /// assert_eq!(custom_headers.next(), Some(Header::new("X-Custom", "one"))); /// assert_eq!(custom_headers.next(), Some(Header::new("X-Custom", "two"))); /// assert_eq!(custom_headers.next(), None); @@ -829,10 +806,10 @@ impl<'r> Response<'r> { /// response.adjoin_raw_header("X-Custom", "one"); /// response.adjoin_raw_header("X-Custom", "two"); /// response.adjoin_raw_header("X-Other", "hi"); - /// assert_eq!(response.headers().count(), 3); + /// assert_eq!(response.headers().len(), 3); /// /// response.remove_header("X-Custom"); - /// assert_eq!(response.headers().count(), 1); + /// assert_eq!(response.headers().len(), 1); /// ``` #[inline(always)] pub fn remove_header(&mut self, name: &str) { @@ -1034,12 +1011,12 @@ impl<'r> Response<'r> { /// assert_eq!(response.status(), Status::NotFound); /// /// # { - /// let ctype: Vec<_> = response.header_values("Content-Type").collect(); + /// let ctype: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(ctype, vec![ContentType::HTML.to_string()]); /// # } /// /// # { - /// let custom_values: Vec<_> = response.header_values("X-Custom").collect(); + /// let custom_values: Vec<_> = response.headers().get("X-Custom").collect(); /// assert_eq!(custom_values, vec!["value 1"]); /// # } /// ``` @@ -1083,12 +1060,12 @@ impl<'r> Response<'r> { /// assert_eq!(response.status(), Status::ImATeapot); /// /// # { - /// let ctype: Vec<_> = response.header_values("Content-Type").collect(); + /// let ctype: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(ctype, vec![ContentType::HTML.to_string()]); /// # } /// /// # { - /// let custom_values: Vec<_> = response.header_values("X-Custom").collect(); + /// let custom_values: Vec<_> = response.headers().get("X-Custom").collect(); /// assert_eq!(custom_values, vec!["value 2", "value 3", "value 1"]); /// # } /// ``` @@ -1111,7 +1088,7 @@ impl<'r> fmt::Debug for Response<'r> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", self.status())?; - for header in self.headers() { + for header in self.headers().iter() { writeln!(f, "{}", header)?; } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 262d03a1..9170e9e0 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -121,7 +121,7 @@ impl Rocket { { *hyp_res.status_mut() = hyper::StatusCode::from_u16(response.status().code); - for header in response.headers() { + for header in response.headers().iter() { // FIXME: Using hyper here requires two allocations. let name = header.name.into_string(); let value = Vec::from(header.value.as_bytes()); diff --git a/lib/tests/head_handling.rs b/lib/tests/head_handling.rs index 6b5b2a9a..0c86c067 100644 --- a/lib/tests/head_handling.rs +++ b/lib/tests/head_handling.rs @@ -54,7 +54,7 @@ mod tests { } - let content_type: Vec<_> = response.header_values("Content-Type").collect(); + let content_type: Vec<_> = response.headers().get("Content-Type").collect(); assert_eq!(content_type, vec![ContentType::Plain.to_string()]); let mut req = MockRequest::new(Head, "/empty"); @@ -70,7 +70,7 @@ mod tests { assert_eq!(response.status(), Status::Ok); - let content_type: Vec<_> = response.header_values("Content-Type").collect(); + let content_type: Vec<_> = response.headers().get("Content-Type").collect(); assert_eq!(content_type, vec![ContentType::JSON.to_string()]); } } diff --git a/lib/tests/redirect_from_catcher-issue-113.rs b/lib/tests/redirect_from_catcher-issue-113.rs index 79bae319..c68152a6 100644 --- a/lib/tests/redirect_from_catcher-issue-113.rs +++ b/lib/tests/redirect_from_catcher-issue-113.rs @@ -24,7 +24,7 @@ mod tests { let response = req.dispatch_with(&rocket); println!("Response:\n{:?}", response); - let location: Vec<_> = response.header_values("location").collect(); + let location: Vec<_> = response.headers().get("location").collect(); assert_eq!(response.status(), Status::SeeOther); assert_eq!(location, vec!["/"]); } From 3bebdcc53de12abd33a7014a8120db0abaed6ebf Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 01:59:28 -0700 Subject: [PATCH 090/297] Add Response::body_string(). Use it in all tests. --- examples/config/tests/common/mod.rs | 3 +- examples/content_types/src/tests.rs | 2 +- examples/cookies/src/tests.rs | 2 +- examples/errors/src/tests.rs | 2 +- examples/extended_validation/src/tests.rs | 2 +- examples/forms/src/tests.rs | 2 +- examples/from_request/src/main.rs | 2 +- examples/handlebars_templates/src/tests.rs | 15 +++---- examples/hello_alt_methods/src/tests.rs | 2 +- examples/hello_person/src/tests.rs | 3 +- examples/hello_ranks/src/tests.rs | 4 +- examples/hello_tls/src/tests.rs | 3 +- examples/hello_world/src/tests.rs | 3 +- examples/manual_routes/src/tests.rs | 4 +- examples/optional_redirect/src/tests.rs | 3 +- examples/optional_result/src/tests.rs | 3 +- examples/query_params/src/tests.rs | 45 +++++++++---------- examples/raw_sqlite/src/tests.rs | 3 +- examples/redirect/src/tests.rs | 4 +- examples/state/src/tests.rs | 3 +- examples/testing/src/main.rs | 4 +- examples/uuid/src/tests.rs | 2 +- lib/src/response/responder.rs | 4 +- lib/src/response/response.rs | 43 +++++++++++------- lib/src/testing.rs | 10 ++--- lib/tests/form_method-issue-45.rs | 3 +- lib/tests/form_value_decoding-issue-82.rs | 3 +- lib/tests/precise-content-type-matching.rs | 2 +- lib/tests/query-and-non-query-dont-collide.rs | 6 +-- lib/tests/remote-rewrite.rs | 2 +- lib/tests/segments-issues-41-86.rs | 3 +- 31 files changed, 88 insertions(+), 104 deletions(-) diff --git a/examples/config/tests/common/mod.rs b/examples/config/tests/common/mod.rs index 43832aab..77ee8264 100644 --- a/examples/config/tests/common/mod.rs +++ b/examples/config/tests/common/mod.rs @@ -49,6 +49,5 @@ pub fn test_hello() { let mut request = MockRequest::new(Method::Get, "/hello"); let mut response = request.dispatch_with(&rocket); - assert_eq!(response.body().and_then(|b| b.into_string()), - Some("Hello, world!".to_string())); + assert_eq!(response.body_string(), Some("Hello, world!".into())); } diff --git a/examples/content_types/src/tests.rs b/examples/content_types/src/tests.rs index 5e9f5e85..c15ed0f6 100644 --- a/examples/content_types/src/tests.rs +++ b/examples/content_types/src/tests.rs @@ -14,7 +14,7 @@ fn test(method: Method, uri: &str, header: H, status: Status, body: String) let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), status); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(body)); + assert_eq!(response.body_string(), Some(body)); } #[test] diff --git a/examples/cookies/src/tests.rs b/examples/cookies/src/tests.rs index 0528d963..f979ad54 100644 --- a/examples/cookies/src/tests.rs +++ b/examples/cookies/src/tests.rs @@ -31,7 +31,7 @@ fn test_body(optional_cookie: Option>, expected_body: String) { let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected_body)); + assert_eq!(response.body_string(), Some(expected_body)); } #[test] diff --git a/examples/errors/src/tests.rs b/examples/errors/src/tests.rs index 4cfd4567..9d12e8a2 100644 --- a/examples/errors/src/tests.rs +++ b/examples/errors/src/tests.rs @@ -10,7 +10,7 @@ fn test(uri: &str, status: Status, body: String) { let mut response = req.dispatch_with(&rocket); assert_eq!(response.status(), status); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(body)); + assert_eq!(response.body_string(), Some(body)); } #[test] diff --git a/examples/extended_validation/src/tests.rs b/examples/extended_validation/src/tests.rs index 8d096be4..db4f91ad 100644 --- a/examples/extended_validation/src/tests.rs +++ b/examples/extended_validation/src/tests.rs @@ -17,7 +17,7 @@ fn test_login(user: &str, pass: &str, age: &str, status: Status, body: T) let mut response = req.dispatch_with(&rocket); assert_eq!(response.status(), status); - let body_str = response.body().and_then(|body| body.into_string()); + let body_str = response.body_string(); if let Some(expected_str) = body.into() { assert!(body_str.map_or(false, |s| s.contains(expected_str))); } diff --git a/examples/forms/src/tests.rs b/examples/forms/src/tests.rs index 65b33677..0ef8b559 100644 --- a/examples/forms/src/tests.rs +++ b/examples/forms/src/tests.rs @@ -12,7 +12,7 @@ fn test_login(username: &str, password: &str, age: isize, status: Status, .body(&format!("username={}&password={}&age={}", username, password, age)); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); + let body_str = response.body_string(); println!("Checking: {:?}/{:?}/{:?}/{:?}", username, password, age, body_str); assert_eq!(response.status(), status); diff --git a/examples/from_request/src/main.rs b/examples/from_request/src/main.rs index f9d4d813..ec613a7c 100644 --- a/examples/from_request/src/main.rs +++ b/examples/from_request/src/main.rs @@ -50,7 +50,7 @@ mod test { let mut response = req.dispatch_with(&rocket); let expect = format!("Your request contained {} headers!", headers.len()); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(expect)); + assert_eq!(response.body_string(), Some(expect)); } #[test] diff --git a/examples/handlebars_templates/src/tests.rs b/examples/handlebars_templates/src/tests.rs index c72a116b..ddce9328 100644 --- a/examples/handlebars_templates/src/tests.rs +++ b/examples/handlebars_templates/src/tests.rs @@ -34,14 +34,12 @@ fn test_root() { for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] { let req = MockRequest::new(*method, "/"); run_test!(req, |mut response: Response| { - assert_eq!(response.status(), Status::NotFound); - let mut map = ::std::collections::HashMap::new(); map.insert("path", "/"); - let expected = Template::render("error/404", &map).to_string(); + let expected_body = Template::render("error/404", &map).to_string(); - let body_string = response.body().and_then(|body| body.into_string()); - assert_eq!(body_string, Some(expected)); + assert_eq!(response.status(), Status::NotFound); + assert_eq!(response.body_string(), Some(expected_body)); }); } } @@ -59,8 +57,7 @@ fn test_name() { }; let expected = Template::render("index", &context).to_string(); - let body_string = response.body().and_then(|body| body.into_string()); - assert_eq!(body_string, Some(expected)); + assert_eq!(response.body_string(), Some(expected)); }); } @@ -74,8 +71,6 @@ fn test_404() { let mut map = ::std::collections::HashMap::new(); map.insert("path", "/hello/"); let expected = Template::render("error/404", &map).to_string(); - - let body_string = response.body().and_then(|body| body.into_string()); - assert_eq!(body_string, Some(expected)); + assert_eq!(response.body_string(), Some(expected)); }); } diff --git a/examples/hello_alt_methods/src/tests.rs b/examples/hello_alt_methods/src/tests.rs index 1703900f..47cd1d6e 100644 --- a/examples/hello_alt_methods/src/tests.rs +++ b/examples/hello_alt_methods/src/tests.rs @@ -12,7 +12,7 @@ fn test(method: Method, status: Status, body_prefix: Option<&str>) { assert_eq!(response.status(), status); if let Some(expected_body_string) = body_prefix { - let body_str = response.body().and_then(|body| body.into_string()).unwrap(); + let body_str = response.body_string().unwrap(); assert!(body_str.starts_with(expected_body_string)); } } diff --git a/examples/hello_person/src/tests.rs b/examples/hello_person/src/tests.rs index e7768534..2bd6dbc4 100644 --- a/examples/hello_person/src/tests.rs +++ b/examples/hello_person/src/tests.rs @@ -7,8 +7,7 @@ fn test(uri: &str, expected: String) { let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]); let mut req = MockRequest::new(Get, uri); let mut response = req.dispatch_with(&rocket); - - assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected)); + assert_eq!(response.body_string(), Some(expected)); } fn test_404(uri: &str) { diff --git a/examples/hello_ranks/src/tests.rs b/examples/hello_ranks/src/tests.rs index 22ed979f..66e4afed 100644 --- a/examples/hello_ranks/src/tests.rs +++ b/examples/hello_ranks/src/tests.rs @@ -5,10 +5,8 @@ use rocket::http::Method::*; fn test(uri: &str, expected: String) { let rocket = rocket::ignite().mount("/", routes![super::hello, super::hi]); let mut req = MockRequest::new(Get, uri); - let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some(expected)); + assert_eq!(response.body_string(), Some(expected)); } #[test] diff --git a/examples/hello_tls/src/tests.rs b/examples/hello_tls/src/tests.rs index 94b54e03..abb31f89 100644 --- a/examples/hello_tls/src/tests.rs +++ b/examples/hello_tls/src/tests.rs @@ -8,6 +8,5 @@ fn hello_world() { let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("Hello, world!".to_string())); + assert_eq!(response.body_string(), Some("Hello, world!".into())); } diff --git a/examples/hello_world/src/tests.rs b/examples/hello_world/src/tests.rs index 94b54e03..abb31f89 100644 --- a/examples/hello_world/src/tests.rs +++ b/examples/hello_world/src/tests.rs @@ -8,6 +8,5 @@ fn hello_world() { let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("Hello, world!".to_string())); + assert_eq!(response.body_string(), Some("Hello, world!".into())); } diff --git a/examples/manual_routes/src/tests.rs b/examples/manual_routes/src/tests.rs index 452bdcb9..2c964c91 100644 --- a/examples/manual_routes/src/tests.rs +++ b/examples/manual_routes/src/tests.rs @@ -9,7 +9,7 @@ fn test(uri: &str, content_type: ContentType, status: Status, body: String) { let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), status); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(body)); + assert_eq!(response.body_string(), Some(body)); } #[test] @@ -50,7 +50,7 @@ fn test_upload() { let mut request = MockRequest::new(Get, "/upload"); let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body().and_then(|b| b.into_string()), Some(expected_body)); + assert_eq!(response.body_string(), Some(expected_body)); } #[test] diff --git a/examples/optional_redirect/src/tests.rs b/examples/optional_redirect/src/tests.rs index c8680f1d..6dc38077 100644 --- a/examples/optional_redirect/src/tests.rs +++ b/examples/optional_redirect/src/tests.rs @@ -9,8 +9,7 @@ fn test_200(uri: &str, expected_body: &str) { let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body().and_then(|b| b.into_string()), - Some(expected_body.to_string())); + assert_eq!(response.body_string(), Some(expected_body.to_string())); } fn test_303(uri: &str, expected_location: &str) { diff --git a/examples/optional_result/src/tests.rs b/examples/optional_result/src/tests.rs index f3d79148..5d63fcfd 100644 --- a/examples/optional_result/src/tests.rs +++ b/examples/optional_result/src/tests.rs @@ -9,8 +9,7 @@ fn test_200() { let mut response = request.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - assert_eq!(response.body().and_then(|b| b.into_string()), - Some("Hello, Sergio!".to_string())); + assert_eq!(response.body_string(), Some("Hello, Sergio!".into())); } #[test] diff --git a/examples/query_params/src/tests.rs b/examples/query_params/src/tests.rs index 0ac9a887..2a194ed5 100644 --- a/examples/query_params/src/tests.rs +++ b/examples/query_params/src/tests.rs @@ -16,45 +16,44 @@ macro_rules! run_test { #[test] fn age_and_name_params() { - run_test!("?age=10&name=john", |mut response: Response| { - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("Hello, 10 year old named john!".to_string())); - }); + run_test!("?age=10&name=john", |mut response: Response| { + assert_eq!(response.body_string(), + Some("Hello, 10 year old named john!".into())); + }); } #[test] fn age_param_only() { - run_test!("?age=10", |response: Response| { - assert_eq!(response.status(), Status::NotFound); - }); + run_test!("?age=10", |response: Response| { + assert_eq!(response.status(), Status::NotFound); + }); } #[test] fn name_param_only() { - run_test!("?name=John", |mut response: Response| { - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("Hello John!".to_string())); - }); + run_test!("?name=John", |mut response: Response| { + assert_eq!(response.body_string(), Some("Hello John!".into())); + }); } #[test] fn no_params() { - run_test!("", |response: Response| { - assert_eq!(response.status(), Status::NotFound); - }); + run_test!("", |response: Response| { + assert_eq!(response.status(), Status::NotFound); + }); - run_test!("?", |response: Response| { - assert_eq!(response.status(), Status::NotFound); - }); + run_test!("?", |response: Response| { + assert_eq!(response.status(), Status::NotFound); + }); } #[test] fn non_existent_params() { - run_test!("?x=y", |response: Response| { - assert_eq!(response.status(), Status::NotFound); - }); + run_test!("?x=y", |response: Response| { + assert_eq!(response.status(), Status::NotFound); + }); - run_test!("?age=10&name=john&complete=true", |response: Response| { - assert_eq!(response.status(), Status::NotFound); - }); + run_test!("?age=10&name=john&complete=true", |response: Response| { + assert_eq!(response.status(), Status::NotFound); + }); } diff --git a/examples/raw_sqlite/src/tests.rs b/examples/raw_sqlite/src/tests.rs index 4f290935..9fda4e0f 100644 --- a/examples/raw_sqlite/src/tests.rs +++ b/examples/raw_sqlite/src/tests.rs @@ -8,6 +8,5 @@ fn hello() { let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("Rocketeer".to_string())); + assert_eq!(response.body_string(), Some("Rocketeer".into())); } diff --git a/examples/redirect/src/tests.rs b/examples/redirect/src/tests.rs index f14e85eb..9a80c074 100644 --- a/examples/redirect/src/tests.rs +++ b/examples/redirect/src/tests.rs @@ -31,8 +31,8 @@ fn test_root() { #[test] fn test_login() { run_test!("/login", |mut response: Response| { - let body_string = response.body().and_then(|body| body.into_string()); - assert_eq!(body_string, Some("Hi! Please log in before continuing.".to_string())); + assert_eq!(response.body_string(), + Some("Hi! Please log in before continuing.".to_string())); assert_eq!(response.status(), Status::Ok); }); } diff --git a/examples/state/src/tests.rs b/examples/state/src/tests.rs index 7986d52a..5ab86781 100644 --- a/examples/state/src/tests.rs +++ b/examples/state/src/tests.rs @@ -12,8 +12,7 @@ fn register_hit(rocket: &Rocket) { fn get_count(rocket: &Rocket) -> usize { let mut req = MockRequest::new(Get, "/count"); let mut response = req.dispatch_with(&rocket); - let body_string = response.body().and_then(|b| b.into_string()).unwrap(); - body_string.parse().unwrap() + response.body_string().and_then(|s| s.parse().ok()).unwrap() } #[test] diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 0ca16f63..e2eb6530 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -25,8 +25,6 @@ mod test { let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - - let body_string = response.body().and_then(|b| b.into_string()); - assert_eq!(body_string, Some("Hello, world!".to_string())); + assert_eq!(response.body_string(), Some("Hello, world!".into())); } } diff --git a/examples/uuid/src/tests.rs b/examples/uuid/src/tests.rs index 47d4011e..0d1640cc 100644 --- a/examples/uuid/src/tests.rs +++ b/examples/uuid/src/tests.rs @@ -9,7 +9,7 @@ fn test(uri: &str, expected: &str) { let mut req = MockRequest::new(Get, uri); let mut res = req.dispatch_with(&rocket); - assert_eq!(res.body().and_then(|b| b.into_string()), Some(expected.into())); + assert_eq!(res.body_string(), Some(expected.into())); } fn test_404(uri: &str) { diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs index d8b0ebd7..5af34fd6 100644 --- a/lib/src/response/responder.rs +++ b/lib/src/response/responder.rs @@ -173,9 +173,7 @@ pub trait Responder<'r> { /// use rocket::http::ContentType; /// /// let mut response = "Hello".respond().unwrap(); -/// -/// let body_string = response.body().and_then(|b| b.into_string()); -/// assert_eq!(body_string, Some("Hello".to_string())); +/// assert_eq!(response.body_string(), Some("Hello".into())); /// /// let content_type: Vec<_> = response.headers().get("Content-Type").collect(); /// assert_eq!(content_type.len(), 1); diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 156b9237..2b436975 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -829,10 +829,7 @@ impl<'r> Response<'r> { /// assert!(response.body().is_none()); /// /// response.set_sized_body(Cursor::new("Hello, world!")); - /// - /// let body_string = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_string, Some("Hello, world!".to_string())); - /// assert!(response.body().is_some()); + /// assert_eq!(response.body_string(), Some("Hello, world!".to_string())); /// ``` #[inline(always)] pub fn body(&mut self) -> Option> { @@ -846,6 +843,29 @@ impl<'r> Response<'r> { } } + /// Consumes `self's` body and reads it into a string. If `self` doesn't + /// have a body, reading fails, or string conversion (for non-UTF-8 bodies) + /// fails, returns `None`. Note that `self`'s `body` is consumed after a + /// call to this method. + /// + /// # Example + /// + /// ```rust + /// use std::io::Cursor; + /// use rocket::Response; + /// + /// let mut response = Response::new(); + /// assert!(response.body().is_none()); + /// + /// response.set_sized_body(Cursor::new("Hello, world!")); + /// assert_eq!(response.body_string(), Some("Hello, world!".to_string())); + /// assert!(response.body().is_none()); + /// ``` + #[inline(always)] + pub fn body_string(&mut self) -> Option { + self.take_body().and_then(|b| b.into_string()) + } + /// Moves the body of `self` out and returns it, if there is one, leaving no /// body in its place. /// @@ -901,9 +921,7 @@ impl<'r> Response<'r> { /// /// let mut response = Response::new(); /// response.set_sized_body(Cursor::new("Hello, world!")); - /// - /// let body_string = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_string, Some("Hello, world!".to_string())); + /// assert_eq!(response.body_string(), Some("Hello, world!".to_string())); /// ``` #[inline] pub fn set_sized_body(&mut self, mut body: B) @@ -929,9 +947,7 @@ impl<'r> Response<'r> { /// /// let mut response = Response::new(); /// response.set_streamed_body(repeat(97).take(5)); - /// - /// let body_string = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_string, Some("aaaaa".to_string())); + /// assert_eq!(response.body_string(), Some("aaaaa".to_string())); /// ``` #[inline(always)] pub fn set_streamed_body(&mut self, body: B) where B: io::Read + 'r { @@ -949,9 +965,7 @@ impl<'r> Response<'r> { /// /// let mut response = Response::new(); /// response.set_chunked_body(repeat(97).take(5), 10); - /// - /// let body_string = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_string, Some("aaaaa".to_string())); + /// assert_eq!(response.body_string(), Some("aaaaa".to_string())); /// ``` #[inline(always)] pub fn set_chunked_body(&mut self, body: B, chunk_size: u64) @@ -974,8 +988,7 @@ impl<'r> Response<'r> { /// let mut response = Response::new(); /// response.set_raw_body(body); /// - /// let body_string = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_string, Some("Hello!".to_string())); + /// assert_eq!(response.body_string(), Some("Hello!".to_string())); /// ``` #[inline(always)] pub fn set_raw_body(&mut self, body: Body) { diff --git a/lib/src/testing.rs b/lib/src/testing.rs index a7e71eb8..733d00ff 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -96,11 +96,8 @@ //! let mut req = MockRequest::new(Get, "/"); //! let mut response = req.dispatch_with(&rocket); //! -//! // Write the body out as a string. -//! let body_str = response.body().and_then(|b| b.into_string()); -//! -//! // Check that the body contains what we expect. -//! assert_eq!(body_str, Some("Hello, world!".to_string())); +//! // Check that the body contains the string we expect. +//! assert_eq!(response.body_string(), Some("Hello, world!".into())); //! } //! } //! ``` @@ -260,8 +257,7 @@ impl<'r> MockRequest<'r> { /// let mut req = MockRequest::new(Get, "/"); /// let mut response = req.dispatch_with(&rocket); /// - /// let body_str = response.body().and_then(|b| b.into_string()); - /// assert_eq!(body_str, Some("Hello, world!".to_string())); + /// assert_eq!(response.body_string(), Some("Hello, world!".into())); /// # } /// ``` pub fn dispatch_with<'s>(&'s mut self, rocket: &'r Rocket) -> Response<'s> { diff --git a/lib/tests/form_method-issue-45.rs b/lib/tests/form_method-issue-45.rs index 2189484a..0100dfae 100644 --- a/lib/tests/form_method-issue-45.rs +++ b/lib/tests/form_method-issue-45.rs @@ -32,8 +32,7 @@ mod tests { .body("_method=patch&form_data=Form+data"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|b| b.into_string()); - assert_eq!(body_str, Some("OK".to_string())); + assert_eq!(response.body_string(), Some("OK".into())); } #[test] diff --git a/lib/tests/form_value_decoding-issue-82.rs b/lib/tests/form_value_decoding-issue-82.rs index 69e8817c..32e5260b 100644 --- a/lib/tests/form_value_decoding-issue-82.rs +++ b/lib/tests/form_value_decoding-issue-82.rs @@ -30,9 +30,8 @@ mod tests { .body(format!("form_data={}", raw)); let mut response = req.dispatch_with(&rocket); - let body_string = response.body().and_then(|b| b.into_string()); assert_eq!(response.status(), Status::Ok); - assert_eq!(Some(decoded.to_string()), body_string); + assert_eq!(Some(decoded.to_string()), response.body_string()); } #[test] diff --git a/lib/tests/precise-content-type-matching.rs b/lib/tests/precise-content-type-matching.rs index 2bcc9a20..8a717555 100644 --- a/lib/tests/precise-content-type-matching.rs +++ b/lib/tests/precise-content-type-matching.rs @@ -48,7 +48,7 @@ mod tests { } let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|b| b.into_string()); + let body_str = response.body_string(); let body: Option<&'static str> = $body; match body { Some(string) => assert_eq!(body_str, Some(string.to_string())), diff --git a/lib/tests/query-and-non-query-dont-collide.rs b/lib/tests/query-and-non-query-dont-collide.rs index a7fff33f..45105251 100644 --- a/lib/tests/query-and-non-query-dont-collide.rs +++ b/lib/tests/query-and-non-query-dont-collide.rs @@ -29,13 +29,11 @@ mod tests { fn assert_no_collision(rocket: &Rocket) { let mut req = MockRequest::new(Get, "/?field=query"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|b| b.into_string()); - assert_eq!(body_str, Some("query".to_string())); + assert_eq!(response.body_string(), Some("query".into())); let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|b| b.into_string()); - assert_eq!(body_str, Some("no query".to_string())); + assert_eq!(response.body_string(), Some("no query".into())); } #[test] diff --git a/lib/tests/remote-rewrite.rs b/lib/tests/remote-rewrite.rs index 56c99402..4ef8b494 100644 --- a/lib/tests/remote-rewrite.rs +++ b/lib/tests/remote-rewrite.rs @@ -33,7 +33,7 @@ mod remote_rewrite_tests { let mut response = req.dispatch_with(&rocket); assert_eq!(response.status(), Status::Ok); - let body_str = response.body().and_then(|b| b.into_string()); + let body_str = response.body_string(); match ip { Some(ip) => assert_eq!(body_str, Some(format!("{}:{}", ip, port))), None => assert_eq!(body_str, Some(KNOWN_IP.into())) diff --git a/lib/tests/segments-issues-41-86.rs b/lib/tests/segments-issues-41-86.rs index ff61f75b..834b92e1 100644 --- a/lib/tests/segments-issues-41-86.rs +++ b/lib/tests/segments-issues-41-86.rs @@ -52,8 +52,7 @@ mod tests { let mut req = MockRequest::new(Get, format!("{}/{}", prefix, path)); let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|b| b.into_string()); - assert_eq!(body_str, Some(path.into())); + assert_eq!(response.body_string(), Some(path.into())); } } } From 6641e9b92d072c9a6485ac155b8931bd4daa404b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 14:35:22 -0700 Subject: [PATCH 091/297] Add Response::content_type() method. --- lib/src/request/request.rs | 8 ++++---- lib/src/response/response.rs | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 323261eb..074cf641 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -310,10 +310,10 @@ impl<'r> Request<'r> { self.extra.session = RefCell::new(jar); } - /// Returns `Some` of the Content-Type header of `self`. If the header is - /// not present, returns `None`. The Content-Type header is cached after the - /// first call to this function. As a result, subsequent calls will always - /// return the same value. + /// Returns the Content-Type header of `self`. If the header is not present, + /// returns `None`. The Content-Type header is cached after the first call + /// to this function. As a result, subsequent calls will always return the + /// same value. /// /// # Example /// diff --git a/lib/src/response/response.rs b/lib/src/response/response.rs index 2b436975..1bc81158 100644 --- a/lib/src/response/response.rs +++ b/lib/src/response/response.rs @@ -1,9 +1,8 @@ use std::{io, fmt, str}; use std::borrow::Cow; -use http::{Header, HeaderMap}; use response::Responder; -use http::Status; +use http::{Header, HeaderMap, Status, ContentType}; /// The default size, in bytes, of a chunk for streamed responses. pub const DEFAULT_CHUNK_SIZE: u64 = 4096; @@ -641,6 +640,24 @@ impl<'r> Response<'r> { self.status = Some(status); } + /// Returns the Content-Type header of `self`. If the header is not present + /// or is malformed, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use rocket::Response; + /// use rocket::http::ContentType; + /// + /// let mut response = Response::new(); + /// response.set_header(ContentType::HTML); + /// assert_eq!(response.content_type(), Some(ContentType::HTML)); + /// ``` + #[inline(always)] + pub fn content_type(&self) -> Option { + self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()) + } + /// Sets the status of `self` to a custom `status` with status code `code` /// and reason phrase `reason`. This method should be used sparingly; prefer /// to use [set_status](#method.set_status) instead. From e6203a77e7dc886c5f7dfbd58214e7dbe6469afb Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 14:35:34 -0700 Subject: [PATCH 092/297] Compile with 4 codegen units on dev. --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1f0af5ef..3b5031ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[profile.dev] +codegen-units = 4 + [workspace] members = [ "lib/", From e6615af7e6aa1f71f7a6a69f4777a4530310272c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 14:39:17 -0700 Subject: [PATCH 093/297] Add tests for pastebin example. --- .gitignore | 3 ++ examples/pastebin/Cargo.toml | 3 ++ examples/pastebin/src/main.rs | 9 +++- examples/pastebin/src/tests.rs | 76 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 examples/pastebin/src/tests.rs diff --git a/.gitignore b/.gitignore index 8dcb1cb3..03fcdce2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ scripts/upload-docs.sh # Backup files. *.bak + +# Uploads in pastebin example. +examples/pastebin/upload/* diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml index 907ff59a..de14c65e 100644 --- a/examples/pastebin/Cargo.toml +++ b/examples/pastebin/Cargo.toml @@ -7,3 +7,6 @@ workspace = "../../" rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } rand = "0.3" + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index b15cdf1b..2a95e527 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -5,6 +5,7 @@ extern crate rocket; extern crate rand; mod paste_id; +#[cfg(test)] mod tests; use std::io; use std::fs::File; @@ -52,6 +53,10 @@ fn index() -> &'static str { " } -fn main() { - rocket::ignite().mount("/", routes![index, upload, retrieve]).launch(); +fn rocket() -> rocket::Rocket { + rocket::ignite().mount("/", routes![index, upload, retrieve]) +} + +fn main() { + rocket().launch(); } diff --git a/examples/pastebin/src/tests.rs b/examples/pastebin/src/tests.rs new file mode 100644 index 00000000..01f9a6e8 --- /dev/null +++ b/examples/pastebin/src/tests.rs @@ -0,0 +1,76 @@ +use super::{rocket, index}; +use rocket::testing::MockRequest; +use rocket::http::Method::*; +use rocket::http::{Status, ContentType}; +use rocket::Rocket; + +fn extract_id(from: &str) -> Option { + from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_right().to_string()) +} + +#[test] +fn check_index() { + let rocket = rocket(); + + // Ensure the index returns what we expect. + let mut req = MockRequest::new(Get, "/"); + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + assert_eq!(response.body_string(), Some(index().into())) +} + +fn upload_paste(rocket: &Rocket, body: &str) -> String { + let mut req = MockRequest::new(Post, "/").body(body); + let mut response = req.dispatch_with(rocket); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + extract_id(&response.body_string().unwrap()).unwrap() +} + + +fn download_paste(rocket: &Rocket, id: &str) -> String { + let mut req = MockRequest::new(Get, format!("/{}", id)); + let mut response = req.dispatch_with(rocket); + assert_eq!(response.status(), Status::Ok); + response.body_string().unwrap() +} + +#[test] +fn pasting() { + let rocket = rocket(); + + // Do a trivial upload, just to make sure it works. + let body_1 = "Hello, world!"; + let id_1 = upload_paste(&rocket, body_1); + assert_eq!(download_paste(&rocket, &id_1), body_1); + + // Make sure we can keep getting that paste. + assert_eq!(download_paste(&rocket, &id_1), body_1); + assert_eq!(download_paste(&rocket, &id_1), body_1); + assert_eq!(download_paste(&rocket, &id_1), body_1); + + // Upload some unicode. + let body_2 = "こんにちは"; + let id_2 = upload_paste(&rocket, body_2); + assert_eq!(download_paste(&rocket, &id_2), body_2); + + // Make sure we can get both pastes. + assert_eq!(download_paste(&rocket, &id_1), body_1); + assert_eq!(download_paste(&rocket, &id_2), body_2); + assert_eq!(download_paste(&rocket, &id_1), body_1); + assert_eq!(download_paste(&rocket, &id_2), body_2); + + // Now a longer upload. + let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed + do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit + in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui + officia deserunt mollit anim id est laborum."; + let id_3 = upload_paste(&rocket, body_3); + assert_eq!(download_paste(&rocket, &id_3), body_3); + assert_eq!(download_paste(&rocket, &id_1), body_1); + assert_eq!(download_paste(&rocket, &id_2), body_2); +} From b4586f62ee9dfea13ff575bf002c8f1f097a9423 Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Sun, 19 Feb 2017 08:01:27 +0200 Subject: [PATCH 094/297] Add managed_queue example and tests. --- Cargo.toml | 1 + examples/managed_queue/Cargo.toml | 12 ++++++ examples/managed_queue/src/main.rs | 68 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 examples/managed_queue/Cargo.toml create mode 100644 examples/managed_queue/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 3b5031ac..97ae1a1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "examples/raw_upload", "examples/pastebin", "examples/state", + "examples/managed_queue", "examples/uuid", "examples/session", "examples/raw_sqlite", diff --git a/examples/managed_queue/Cargo.toml b/examples/managed_queue/Cargo.toml new file mode 100644 index 00000000..6b261f44 --- /dev/null +++ b/examples/managed_queue/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "managed_queue" +version = "0.0.1" +workspace = "../.." + +[dependencies] +crossbeam = "*" +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/managed_queue/src/main.rs b/examples/managed_queue/src/main.rs new file mode 100644 index 00000000..9f350b64 --- /dev/null +++ b/examples/managed_queue/src/main.rs @@ -0,0 +1,68 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate crossbeam; +extern crate rocket; + +use crossbeam::sync::MsQueue; +use rocket::State; + +#[derive(FromForm, Debug)] +struct Event { + description: String +} + +struct LogChannel(MsQueue); + +#[get("/push?")] +fn push(event: Event, queue: State) -> &'static str { + queue.0.push(event); + "got it" +} + +#[get("/pop")] +fn pop(queue: State) -> String { + let e = queue.0.pop(); + e.description +} + +// Use with: curl http://:8000/test?foo=bar + +fn main() { + let q:MsQueue = MsQueue::new(); + + rocket::ignite() + .mount("/", routes![push,pop]) + .manage(LogChannel(q)) + .launch(); + +} + +#[cfg(test)] +mod test { + use super::rocket; + use rocket::testing::MockRequest; + use rocket::http::Status; + use rocket::http::Method::*; + use crossbeam::sync::MsQueue; + use std::{thread, time}; + use super::LogChannel; + use super::Event; + + #[test] + fn test_get() { + let q: MsQueue = MsQueue::new(); + let rocket = rocket::ignite().manage(LogChannel(q)).mount("/", routes![super::push, super::pop]); + let mut req = MockRequest::new(Get, "/push?description=test1"); + let response = req.dispatch_with(&rocket); + assert_eq!(response.status(), Status::Ok); + + let ten_millis = time::Duration::from_millis(10); + thread::sleep(ten_millis); + + let mut req = MockRequest::new(Get, "/pop"); + let mut response = req.dispatch_with(&rocket); + let body_str = response.body().and_then(|body| body.into_string()); + assert_eq!(body_str, Some("test1".to_string())); + } +} \ No newline at end of file From 8d14cd571c0eb235e92dde8a1033660552753390 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 14:53:41 -0700 Subject: [PATCH 095/297] Tidy up managed_queue example. --- examples/managed_queue/src/main.rs | 54 +++++++---------------------- examples/managed_queue/src/tests.rs | 18 ++++++++++ 2 files changed, 30 insertions(+), 42 deletions(-) create mode 100644 examples/managed_queue/src/tests.rs diff --git a/examples/managed_queue/src/main.rs b/examples/managed_queue/src/main.rs index 9f350b64..7dd09c4e 100644 --- a/examples/managed_queue/src/main.rs +++ b/examples/managed_queue/src/main.rs @@ -4,6 +4,8 @@ extern crate crossbeam; extern crate rocket; +#[cfg(test)] mod tests; + use crossbeam::sync::MsQueue; use rocket::State; @@ -14,55 +16,23 @@ struct Event { struct LogChannel(MsQueue); -#[get("/push?")] -fn push(event: Event, queue: State) -> &'static str { +#[put("/push?")] +fn push(event: Event, queue: State) { queue.0.push(event); - "got it" } #[get("/pop")] fn pop(queue: State) -> String { - let e = queue.0.pop(); - e.description + let queue = &queue.0; + queue.pop().description } -// Use with: curl http://:8000/test?foo=bar +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount("/", routes![push, pop]) + .manage(LogChannel(MsQueue::new())) +} fn main() { - let q:MsQueue = MsQueue::new(); - - rocket::ignite() - .mount("/", routes![push,pop]) - .manage(LogChannel(q)) - .launch(); - + rocket().launch(); } - -#[cfg(test)] -mod test { - use super::rocket; - use rocket::testing::MockRequest; - use rocket::http::Status; - use rocket::http::Method::*; - use crossbeam::sync::MsQueue; - use std::{thread, time}; - use super::LogChannel; - use super::Event; - - #[test] - fn test_get() { - let q: MsQueue = MsQueue::new(); - let rocket = rocket::ignite().manage(LogChannel(q)).mount("/", routes![super::push, super::pop]); - let mut req = MockRequest::new(Get, "/push?description=test1"); - let response = req.dispatch_with(&rocket); - assert_eq!(response.status(), Status::Ok); - - let ten_millis = time::Duration::from_millis(10); - thread::sleep(ten_millis); - - let mut req = MockRequest::new(Get, "/pop"); - let mut response = req.dispatch_with(&rocket); - let body_str = response.body().and_then(|body| body.into_string()); - assert_eq!(body_str, Some("test1".to_string())); - } -} \ No newline at end of file diff --git a/examples/managed_queue/src/tests.rs b/examples/managed_queue/src/tests.rs new file mode 100644 index 00000000..c6382de7 --- /dev/null +++ b/examples/managed_queue/src/tests.rs @@ -0,0 +1,18 @@ +use super::rocket; + +use rocket::testing::MockRequest; +use rocket::http::Status; +use rocket::http::Method::*; + +#[test] +fn test_push_pop() { + let rocket = rocket(); + + let mut req = MockRequest::new(Put, "/push?description=test1"); + let response = req.dispatch_with(&rocket); + assert_eq!(response.status(), Status::Ok); + + let mut req = MockRequest::new(Get, "/pop"); + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.body_string(), Some("test1".to_string())); +} From 7d2a1142808e8fef0c3012c25dbecf8882d8c916 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 14 Apr 2017 14:58:17 -0700 Subject: [PATCH 096/297] Set version of managed_queue example to 0.0.0. --- examples/managed_queue/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/managed_queue/Cargo.toml b/examples/managed_queue/Cargo.toml index 6b261f44..da95be2f 100644 --- a/examples/managed_queue/Cargo.toml +++ b/examples/managed_queue/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managed_queue" -version = "0.0.1" +version = "0.0.0" workspace = "../.." [dependencies] From 586d46ae9c9ae467dd0ae74970f1f8e3827078f8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 15 Apr 2017 19:03:40 -0700 Subject: [PATCH 097/297] Use upstream smallvec. --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 6b33f4df..220dab49 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,7 +29,7 @@ state = "0.2.1" time = "0.1" memchr = "1" base64 = "0.4" -smallvec = { git = "https://github.com/SergioBenitez/rust-smallvec" } +smallvec = "0.3.3" pear = "0.0.8" pear_codegen = "0.0.8" rustls = { version = "0.5.8", optional = true } From 73e39dcf1791be673cc9bcc8782299e78101b9fe Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 00:59:52 -0700 Subject: [PATCH 098/297] Fix small typo: It -> In. --- lib/src/outcome.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/outcome.rs b/lib/src/outcome.rs index 74ae03c3..2ce8b634 100644 --- a/lib/src/outcome.rs +++ b/lib/src/outcome.rs @@ -38,7 +38,7 @@ //! fails with some error and no processing can or should continue as a result. //! The meaning of a failure depends on the context. //! -//! It Rocket, a `Failure` generally means that a request is taken out of normal +//! In Rocket, a `Failure` generally means that a request is taken out of normal //! processing. The request is then given to the catcher corresponding to some //! status code. users can catch failures by requesting a type of `Result` //! or `Option` in request handlers. For example, if a user's handler looks From 08fbe06b102256c00e71a9c8670ce0da01089bd1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 14:13:18 -0700 Subject: [PATCH 099/297] Fix lints for latest nightly. --- codegen/src/lints/utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codegen/src/lints/utils.rs b/codegen/src/lints/utils.rs index 6af05a8a..baa3cee6 100644 --- a/codegen/src/lints/utils.rs +++ b/codegen/src/lints/utils.rs @@ -160,7 +160,8 @@ impl DefExt for Def { | Def::AssociatedTy(id) | Def::TyParam(id) | Def::Struct(id) | Def::StructCtor(id, ..) | Def::Union(id) | Def::Trait(id) | Def::Method(id) | Def::Const(id) | Def::AssociatedConst(id) - | Def::Local(id) | Def::Upvar(id, ..) | Def::Macro(id, ..) => Some(id), + | Def::Local(id) | Def::Upvar(id, ..) | Def::Macro(id, ..) + | Def::GlobalAsm(id) => Some(id), Def::Label(..) | Def::PrimTy(..) | Def::SelfTy(..) | Def::Err => None, } } From 8a789c5d0454f28e18a49ae9346a19a6f6444245 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 14:23:34 -0700 Subject: [PATCH 100/297] Update minimum nightly version in codegen. --- codegen/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen/build.rs b/codegen/build.rs index 4946eabd..0e444cf9 100644 --- a/codegen/build.rs +++ b/codegen/build.rs @@ -8,7 +8,7 @@ use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version, is_min_date}; // Specifies the minimum nightly version needed to compile Rocket's codegen. -const MIN_DATE: &'static str = "2017-03-30"; +const MIN_DATE: &'static str = "2017-04-15"; const MIN_VERSION: &'static str = "1.18.0-nightly"; // Convenience macro for writing to stderr. From 9b7f58448a71af642e6a8d4161ce4c6052b6f09a Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 14:25:16 -0700 Subject: [PATCH 101/297] New version: 0.2.5. --- CHANGELOG.md | 7 +++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 872c49cf..01a9dd38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Version 0.2.5 (Apr 16, 2017) + +## Codegen + + * Lints were updated for `2017-04-15` nightly. + * Minimum required `rustc` is `1.18.0-nightly (2017-04-15)`. + # Version 0.2.4 (Mar 30, 2017) ## Codegen diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index b6b4dbde..93b2d2eb 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.4" +version = "0.2.5" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.4", path = "../lib/" } +rocket = { version = "0.2.5", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 493df989..4ac2bed3 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.4" +version = "0.2.5" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -22,7 +22,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.4", path = "../lib/" } +rocket = { version = "0.2.5", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 220dab49..f32f38d5 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.4" +version = "0.2.5" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -43,7 +43,7 @@ optional = true [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.4", path = "../codegen" } +rocket_codegen = { version = "0.2.5", path = "../codegen" } [build-dependencies] ansi_term = "0.9" From b5ef6ec85b953b5c5a93c994e30f4bd9f66348cb Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 19:48:59 -0700 Subject: [PATCH 102/297] Add site contents, including the guide. Add license information. --- README.md | 2 + scripts/bump_version.sh | 1 + site/LICENSE | 674 ++++++++++++++++++++++++++++ site/README.md | 29 ++ site/guide.md | 43 ++ site/guide/conclusion.md | 28 ++ site/guide/getting-started.md | 95 ++++ site/guide/introduction.md | 43 ++ site/guide/overview.md | 317 +++++++++++++ site/guide/pastebin.md | 405 +++++++++++++++++ site/guide/quickstart.md | 21 + site/guide/requests.md | 378 ++++++++++++++++ site/guide/responses.md | 169 +++++++ site/guide/state.md | 144 ++++++ site/guide/testing.md | 136 ++++++ site/index.toml | 193 ++++++++ site/news.toml | 15 + site/news/2017-02-06-version-0.2.md | 392 ++++++++++++++++ site/overview.toml | 231 ++++++++++ 19 files changed, 3316 insertions(+) create mode 100644 site/LICENSE create mode 100644 site/README.md create mode 100644 site/guide.md create mode 100644 site/guide/conclusion.md create mode 100644 site/guide/getting-started.md create mode 100644 site/guide/introduction.md create mode 100644 site/guide/overview.md create mode 100644 site/guide/pastebin.md create mode 100644 site/guide/quickstart.md create mode 100644 site/guide/requests.md create mode 100644 site/guide/responses.md create mode 100644 site/guide/state.md create mode 100644 site/guide/testing.md create mode 100644 site/index.toml create mode 100644 site/news.toml create mode 100644 site/news/2017-02-06-version-0.2.md create mode 100644 site/overview.toml diff --git a/README.md b/README.md index cb9ddde3..b395a2c4 100644 --- a/README.md +++ b/README.md @@ -213,3 +213,5 @@ Rocket is licensed under either of the following, at your option: * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +The Rocket website source is licensed under [separate terms](site/README.md#license). diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 36e42409..d91e127d 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -11,4 +11,5 @@ if [ -z ${1} ] || [ -z ${2} ]; then fi find . -name "*.toml" | xargs sed -i.bak "s/${1}/${2}/g" +find site/ -name "*.md" | xargs sed -i.bak "s/${1}/${2}/g" find . -name "*.bak" | xargs rm diff --git a/site/LICENSE b/site/LICENSE new file mode 100644 index 00000000..55b5b7e2 --- /dev/null +++ b/site/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/site/README.md b/site/README.md new file mode 100644 index 00000000..ea5491b5 --- /dev/null +++ b/site/README.md @@ -0,0 +1,29 @@ +# Rocket Website Source + +This directory contains the source files for the content on [Rocket's +website](https://rocket.rs). + +## Contents + +This directory contains the following: + + * `index.toml` - Source data for the index (`/`). + * `news.toml` - Source data for the news page (`/news`). + * `overview.toml` - Source data for the overview page (`/overview`). + * `guide.md` - Index page for the guide (`/guide`). + * `news/*.md` - News articles linked to from `news.toml`. + * `guide/*.md` - Guide pages linked to from `guide.md`. + +## Guide + +The source files for the [Rocket Programming Guide](https://rocket.rs/guide/) +can be found in the `guide/` directory. One exception is the root of the guide, +which is `guide.md`. + +Cross-linking to pages in the guide is accomplished via absolute links rooted at +`/guide/`. To link to the page whose source is at `guide/page.md` in this +directory, for instance, link to `/guide/page`. + +# License + +The Rocket website source is licensed under the [GNU General Public License v3.0](LICENSE). diff --git a/site/guide.md b/site/guide.md new file mode 100644 index 00000000..3cd83139 --- /dev/null +++ b/site/guide.md @@ -0,0 +1,43 @@ +# The Rocket Programming Guide + +Welcome to Rocket! + +This is the official guide. It is designed to serve as a starting point to +writing web application with Rocket and Rust. The guide is also designed to be a +reference for experienced Rocket developers. This guide is conversational in +tone. For concise and purely technical documentation, see the [API +documentation](https://api.rocket.rs). + +The guide is split into several sections, each with a focus on a different +aspect of Rocket. The sections are: + + - **[Introduction](introduction/):** introduces Rocket and its philosophy. + - **[Quickstart](quickstart/):** presents the minimal steps necessary to + run your first Rocket application. + - **[Getting Started](getting-started/):** a gentle introduction to getting + your first Rocket application running. + - **[Overview](overview/):** describes the core concepts of Rocket. + - **[Requests](requests/):** discusses handling requests: control-flow, + parsing, and validating. + - **[Responses](responses/):** discusses generating responses. + - **[State](state/):** how to manage state in a Rocket application. + - **[Testing](testing/):** how to unit and integration test a Rocket + application. + - **[Pastebin](pastebin/):** a tutorial on how to create a pastebin with + Rocket. + - **[Conclusion](conclusion/):** concludes the guide and discusses next steps + for learning. + +## Getting Help + +The official community support channels are the `#rocket` IRC channel on the +[Mozilla IRC Server](https://wiki.mozilla.org/IRC) at `irc.mozilla.org` and the +bridged [Rocket room on +Matrix](https://riot.im/app/#/room/#mozilla_#rocket:matrix.org). If you're not +familiar with IRC, we recommend chatting through [Matrix via +Riot](https://riot.im/app/#/room/#mozilla_#rocket:matrix.org) or via the [Kiwi +web IRC client](https://kiwiirc.com/client/irc.mozilla.org/#rocket). You can +learn more about IRC via Mozilla's [Getting Started with +IRC](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Getting_Started_with_IRC) +guide. + diff --git a/site/guide/conclusion.md b/site/guide/conclusion.md new file mode 100644 index 00000000..d7d337a4 --- /dev/null +++ b/site/guide/conclusion.md @@ -0,0 +1,28 @@ +# Conclusion + +We hope you agree that Rocket is a refreshing take on web frameworks. As with +any software project, Rocket is _alive_. There are always things to improve, and +we're happy to take the best ideas. If you have something in mind, please +[submit an issue](https://github.com/SergioBenitez/Rocket/issues). + +## Getting Help + +If you find yourself having trouble developing Rocket applications, you can get +help via the `#rocket` IRC channel on the [Mozilla IRC +Server](https://wiki.mozilla.org/IRC) at `irc.mozilla.org` and the bridged +[Rocket room on Matrix](https://riot.im/app/#/room/#mozilla_#rocket:matrix.org). +If you're not familiar with IRC, we recommend chatting through [Matrix via +Riot](https://riot.im/app/#/room/#mozilla_#rocket:matrix.org) or via the [Kiwi +web IRC client](https://kiwiirc.com/client/irc.mozilla.org/#rocket). You can +learn more about IRC via Mozilla's [Getting Started with +IRC](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Getting_Started_with_IRC) +guide. + +## What's next? + +The best way to learn Rocket is to _build something_. It should be fun and easy, +and there's always someone to help. Alternatively, you can read through the +[Rocket examples](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples) +or the [Rocket source +code](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/lib/src). Whatever you +decide to do next, we hope you have a blast! diff --git a/site/guide/getting-started.md b/site/guide/getting-started.md new file mode 100644 index 00000000..9b601608 --- /dev/null +++ b/site/guide/getting-started.md @@ -0,0 +1,95 @@ +# Getting Started + +Let's create and run our first Rocket application. We'll ensure we have a +compatible version of Rust, create a new Cargo project that depends on Rocket, +and then run the application. + +## Installing Rust + +Rocket makes abundant use of Rust's syntax extensions. Because syntax extensions +don't yet have a stable compiler API, we'll need to use a nightly version of +Rust. If you already have a working installation of the latest Rust nightly, +feel free to skip to the next section. + +To install a nightly version of Rust, we recommend using `rustup`. Install +`rustup` by following the instructions on [its website](https://rustup.rs/). +Once `rustup` is installed, configure Rust nightly as your default toolchain by +running the command: + +```sh +rustup default nightly +``` + +If you prefer, once we setup a project directory in the following section, you +can use per-directory overrides to use the nightly version _only_ for your +Rocket project by running the following command in the directory: + +```sh +rustup override set nightly +``` + +## Nightly Version + +Rocket requires the _latest_ version of Rust nightly to function. If your Rocket +application suddently stops building, ensure you're using the latest version of +Rust nightly and Rocket by updating your toolchain and dependencies with: + +```sh +rustup update && cargo update +``` + +## Hello, world! + +Let's write our first Rocket application! Start by creating a new binary-based +Cargo project and changing into the new directory: + +```sh +cargo new hello-rocket --bin +cd hello-rocket +``` + +Now, add Rocket and its code generation facilities as dependencies of your +project by ensuring your `Cargo.toml` contains the following: + +``` +[dependencies] +rocket = "0.2.5" +rocket_codegen = "0.2.5" +``` + +Modify `src/main.rs` so that it contains the code for the Rocket `Hello, world!` +program, which we reproduce below: + +```rust +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +#[get("/")] +fn index() -> &'static str { + "Hello, world!" +} + +fn main() { + rocket::ignite().mount("/", routes![index]).launch(); +} +``` + +We won't explain exactly what the program does now; we leave that for the rest +of the guide. In short, it creates an `index` route, _mounts_ the route at the +`/` path, and launches the application. Run the program with `cargo run`. You +should see the following: + +```sh +🔧 Configured for development. + => address: localhost + => port: 8000 + => log: normal + => workers: {logical cores} +🛰 Mounting '/': + => GET / +🚀 Rocket has launched from http://localhost:8000... +``` + +Visit `http://localhost:8000` to see your first Rocket application in action! diff --git a/site/guide/introduction.md b/site/guide/introduction.md new file mode 100644 index 00000000..a49a9e2c --- /dev/null +++ b/site/guide/introduction.md @@ -0,0 +1,43 @@ +# Introduction + +Rocket is a web framework for Rust. If you'd like, you can think of Rocket as +being a more flexible, friendly medley of [Rails](http://rubyonrails.org), +[Flask](http://flask.pocoo.org/), +[Bottle](http://bottlepy.org/docs/dev/index.html), and +[Yesod](http://www.yesodweb.com/). We prefer to think of Rocket as something +new. Rocket aims to be fast, easy, and flexible. It also aims to be _fun_, and +it accomplishes this by ensuring that you write as little code as needed to +accomplish your task. This guide introduces you to the core, intermediate, and +advanced concepts of Rocket. After reading this guide, you should find yourself +being _very_ productive with Rocket. + +## Audience + +Readers are assumed to have a good grasp of the Rust programming language. +Readers new to Rust are encouraged to read the [Rust +Book](https://doc.rust-lang.org/book/). This guide also assumes a basic +understanding of web application fundamentals, such as routing and HTTP. + +## Foreword + +Rocket's design is centered around three core philosophies: + + * **Function declaration and parameter types should contain all the necessary + information to validate and process a request.** This immediately prohibits + APIs where request state is retrieved from a global context. As a result, + request handling is **self-contained** in Rocket: handlers are regular + functions with regular arguments. + + * **All request handling information should be typed.** Because the web and + HTTP are themselves untyped (or _stringly_ typed, as some call it), this + means that something or someone has to convert strings to native types. + Rocket does this for you with zero programming overhead. + + * **Decisions should not be forced.** Templates, serialization, sessions, and + just about everything else are all pluggable, optional components. While + Rocket has official support and libraries for each of these, they are + completely optional and swappable. + +These three ideas dictate Rocket's interface, and you will find the ideas +embedded in Rocket's core features. + diff --git a/site/guide/overview.md b/site/guide/overview.md new file mode 100644 index 00000000..53a7ef0c --- /dev/null +++ b/site/guide/overview.md @@ -0,0 +1,317 @@ +# Overview + +Rocket provides primitives to build web servers and applications with Rust: the +rest is up to you. In short, Rocket provides routing, pre-processing of +requests, and post-processing of responses. Your application code instructs +Rocket on what to pre-process and post-process and fills the gaps between +pre-processing and post-processing. + +## Lifecycle + +Rocket's main task is to listen for incoming web requests, dispatch the request +to the application code, and return a response to the client. We call the +process that goes from request to response: the _lifecycle_. We summarize the +lifecycle as the sequence of steps: + + 1. **Routing** + + Rocket parses an incoming HTTP request into native structures that your code + operates on indirectly. Rocket determines which request handler to invoke by + matching against route attributes declared by your application. + + 2. **Validation** + + Rocket validates the incoming request against types and request guards + present in the matched route. If validation fails, Rocket _forwards_ the + request to the next matching route or calls an _error handler_. + + 3. **Processing** + + The request handler associated with the route is invoked with validated + arguments. This is the main business logic of the application. Processing + completes by returning a `Response`. + + 4. **Response** + + The returned `Response` is processed. Rocket generates the appropriate HTTP + response and sends it to the client. This completes the lifecycle. Rocket + continues listening for requests, restarting the lifecycle for each incoming + request. + +The remainder of this section details the _routing_ phase as well as additional +components needed for Rocket to begin dispatching requests to request handlers. +The sections following describe the request and response phases. + +## Routing + +Rocket applications are centered around routes and handlers. + +A _handler_ is simply a function that takes an arbitrary number of arguments and +returns any arbitrary type. A _route_ is a combination of: + + * A set of parameters to match an incoming request against. + * A handler to process the request and return a response. + +The parameters to match against include static paths, dynamic paths, path +segments, forms, query strings, request format specifiers, and body data. Rocket +uses attributes, which look like function decorators in other languages, to make +declaring routes easy. Routes are declared by annotating a function, the +handler, with the set of parameters to match against. A complete route +declaration looks like this: + +```rust +#[get("/world")] // <- route attribute +fn world() -> &'static str { // <- request handler + "Hello, world!" +} +``` + +This declares the `world` route to match against the static path `"/world"` on +incoming `GET` requests. The `world` route is simple, but additional route +parameters are necessary when building more interesting applications. The +[Requests](/guide/requests) section describes the available options for +constructing routes. + +## Mounting + +Before Rocket can dispatch requests to a route, the route needs to be _mounted_. +Mounting a route is like namespacing it. Routes are mounted via the `mount` +method on a `Rocket` instance. Rocket instances can be created with the +`ignite()` static method. + +The `mount` method takes **1)** a path to namespace a list of routes under, and +**2)** a list of route handlers through the `routes!` macro. The `routes!` macro +ties Rocket's code generation to your application. + +For instance, to mount the `world` route we declared above, we would use the +following code: + +```rust +rocket::ignite().mount("/hello", routes![world]) +``` + +Altogether, this creates a new `Rocket` instance via the `ignite` function and +mounts the `world` route to the `"/hello"` path. As a result, `GET` requests to +the `"/hello/world"` path will be directed to the `world` function. + +### Namespacing + +When a route is declared inside a module other than the root, you may find +yourself with unexpected errors when mounting: + +```rust +mod other { + #[get("/world")] + pub fn world() -> &'static str { + "Hello, world!" + } +} + +use other::world; + +fn main() { + // error[E0425]: cannot find value `static_rocket_route_info_for_world` in this scope + rocket::ignite().mount("/hello", routes![world]) +} +``` + +This occurs because the `routes!` macro implicitly converts the route's name +into the name of a structure generated by Rocket's code generation. The solution +is to name the route by a module path instead: + +```rust +rocket::ignite().mount("/hello", routes![other::world]) +``` + +## Launching + +Now that Rocket knows about the route, you can tell Rocket to start accepting +requests via the `launch` method. The method starts up the server and waits for +incoming requests. When a request arrives, Rocket finds the matching route and +dispatches the request to the route's handler. + +We typically call `launch` from the `main` function. Our complete _Hello, +world!_ application thus looks like: + +```rust +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +#[get("/world")] +fn world() -> &'static str { + "Hello, world!" +} + +fn main() { + rocket::ignite().mount("/hello", routes![world]).launch(); +} +``` + +Note that we've added the `#![feature(plugin)]` and `#![plugin(rocket_codegen)]` +lines to tell Rust that we'll be using Rocket's code generation plugin. We've +also imported the `rocket` crate into our namespace via `extern crate rocket`. +Finally, we call the `launch` method in the `main` function. + +Running the application, the console shows: + +```sh +🔧 Configured for development. + => address: localhost + => port: 8000 + => log: normal + => workers: {logical cores} +🛰 Mounting '/world': + => GET /hello/world +🚀 Rocket has launched from http://localhost:8000... +``` + +If we visit `localhost:8000/hello/world`, we see `Hello, world!`, exactly as +we expected. + +A version of this example's complete crate, ready to `cargo run`, can be found +on +[GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/hello_world). +You can find dozens of other complete examples, spanning all of Rocket's +features, in the [GitHub examples +directory](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/). + +## Configuration + +At any point in time, a Rocket application is operating in a given +_configuration environment_. There are three such environments: + + * `development` (short: `dev`) + * `staging` (short: `stage`) + * `production` (short: `prod`) + +Without any action, Rocket applications run in the `development` environment. +The environment can be changed via the `ROCKET_ENV` environment variable. For +example, to launch the `Hello, world!` application in the `staging` environment, +we can run: + +```sh +ROCKET_ENV=stage cargo run +``` + +You'll likely need `sudo` for the command to succeed since `staging` defaults to +listening on port `80`. Note that you can use the short or long form of the +environment name to specify the environment, `stage` _or_ `staging` here. Rocket +tells us the environment we have chosen and its configuration when it launches: + +```sh +$ sudo ROCKET_ENV=staging cargo run + +🔧 Configured for staging. + => address: 0.0.0.0 + => port: 80 + => log: normal + => workers: {logical cores} +🛰 Mounting '/': + => GET / +🚀 Rocket has launched from http://0.0.0.0:80... +``` + +Configuration settings can be changed in one of two ways: via the `Rocket.toml` +configuration file, or via environment variables. + +### Configuration File + +A `Rocket.toml` file can be used to specify the configuration parameters for +each environment. The file is optional. If it is not present, the default +configuration parameters are used. + +The file must be a series of TOML tables, at most one for each environment and +an optional "global" table, where each table contains key-value pairs +corresponding to configuration parameters for that environment. If a +configuration parameter is missing, the default value is used. The following is +a complete `Rocket.toml` file, where every standard configuration parameter is +specified with the default value: + +```toml +[development] +address = "localhost" +port = 8000 +workers = max(number_of_cpus, 2) +log = "normal" + +[staging] +address = "0.0.0.0" +port = 80 +workers = max(number_of_cpus, 2) +log = "normal" + +[production] +address = "0.0.0.0" +port = 80 +workers = max(number_of_cpus, 2) +log = "critical" +``` + +The `workers` parameter is computed by Rocket automatically; the value above is +not valid TOML syntax. + +The "global" pseudo-environment can be used to set and/or override configuration +parameters globally. A parameter defined in a `[global]` table sets, or +overrides if already present, that parameter in every environment. For example, +given the following `Rocket.toml` file, the value of `address` will be +`"1.2.3.4"` in every environment: + +```toml +[global] +address = "1.2.3.4" + +[development] +address = "localhost" + +[production] +address = "0.0.0.0" +``` + +### Extras + +In addition to overriding default configuration parameters, a configuration file +can also define values for any number of _extra_ configuration parameters. While +these parameters aren't used by Rocket directly, other libraries, or your own +application, can use them as they wish. As an example, the +[Template](https://api.rocket.rs/rocket_contrib/struct.Template.html) type +accepts a value for the `template_dir` configuration parameter. The parameter +can be set in `Rocket.toml` as follows: + +```toml +[development] +template_dir = "dev_templates/" + +[production] +template_dir = "prod_templates/" +``` + +This sets the `template_dir` extra configuration parameter to `"dev_templates/"` +when operating in the `development` environment and `"prod_templates/"` when +operating in the `production` environment. Rocket will prepend the `[extra]` tag +to extra configuration parameters when launching: + +```sh +🔧 Configured for development. + => ... + => [extra] template_dir: "dev_templates/" +``` + +### Environment Variables + +All configuration parameters, including extras, can be overridden through +environment variables. To override the configuration parameter `{param}`, use an +environment variable named `ROCKET_{PARAM}`. For instance, to override the +"port" configuration parameter, you can run your application with: + +```sh +ROCKET_PORT=3721 cargo run + +🔧 Configured for staging. + => ... + => port: 3721 +``` + +Environment variables take precedence over all other configuration methods: if a +variable is set, it will be used as that parameter's value. diff --git a/site/guide/pastebin.md b/site/guide/pastebin.md new file mode 100644 index 00000000..adf440f3 --- /dev/null +++ b/site/guide/pastebin.md @@ -0,0 +1,405 @@ +# Pastebin + +To give you a taste of what a real Rocket application looks like, this section +of the guide is a tutorial on how to create a Pastebin application in Rocket. A +pastebin is a simple web application that allows users to upload a text document +and later retrieve it via a special URL. They're often used to share code +snippets, configuration files, and error logs. In this tutorial, we'll build a +simple pastebin service that allows users to upload a file from their terminal. +The service will respond back with a URL to the uploaded file. + +## Finished Product + +A souped-up, completed version of the application you're about to build is +deployed live at [paste.rs](https://paste.rs). Feel free to play with the +application to get a feel for how it works. For example, to upload a text +document named `test.txt`, you can do: + +```sh +curl --data-binary @test.txt https://paste.rs/ +# => https://paste.rs/IYu +``` + +The finished product is composed of the following routes: + + * index: **GET /** - returns a simple HTML page with instructions about how + to use the service + * upload: **POST /** - accepts raw data in the body of the request and + responds with a URL of a page containing the body's content + * retrieve: **GET /<id>** - retrieves the content for the paste with id + `` + +## Getting Started + +Let's get started! First, create a fresh Cargo binary project named +`rocket-pastebin`: + +```rust +cargo new --bin rocket-pastebin +cd rocket-pastebin +``` + +Then add the usual Rocket dependencies to the `Cargo.toml` file: + +```toml +[dependencies] +rocket = "0.2.5" +rocket_codegen = "0.2.5" +``` + +And finally, create a skeleton Rocket application to work off of in +`src/main.rs`: + +```rust +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +fn main() { + rocket::ignite().launch() +} +``` + +Ensure everything works by running the application: + +```sh +cargo run +``` + +At this point, we haven't declared any routes or handlers, so visiting any page +will result in Rocket returning a **404** error. Throughout the rest of the +tutorial, we'll create the three routes and accompanying handlers. + +## Index + +The first route we'll create is the `index` route. This is the page users will +see when they first visit the service. As such, the route should field requests +of the form `GET /`. We declare the route and its handler by adding the `index` +function below to `src/main.rs`: + +```rust +#[get("/")] +fn index() -> &'static str { + " + USAGE + + POST / + + accepts raw data in the body of the request and responds with a URL of + a page containing the body's content + + GET / + + retrieves the content for the paste with id `` + " +} +``` + +This declares the `index` route for requests to `GET /` as returning a static +string with the specified contents. Rocket will take the string and return it as +the body of a fully formed HTTP response with `Content-Type: text/plain`. You +can read more about how Rocket formulates responses at the [API documentation +for the Responder + trait](https://api.rocket.rs/rocket/response/trait.Responder.html). + +Remember that routes first need to be mounted before Rocket dispatches requests +to them. To mount the `index` route, modify the main function so that it reads: + +```rust +fn main() { + rocket::ignite().mount("/", routes![index]).launch() +} +``` + +You should now be able to `cargo run` the application and visit the root path +(`/`) to see the text being displayed. + +## Uploading + +The most complicated aspect of the pastebin, as you might imagine, is handling +upload requests. When a user attempts to upload a pastebin, our service needs to +generate a unique ID for the upload, read the data, write it out to a file or +database, and then return a URL with the ID. We'll take each of these one step +at a time, beginning with generating IDs. + +### Unique IDs + +Generating a unique and useful ID is an interesting topic, but it is outside the +scope of this tutorial. Instead, we simply provide the code for a `PasteID` +structure that represents a _probably_ unique ID. Read through the code, then +copy/paste it into a new file named `paste_id.rs` in the `src/` directory: + +```rust +use std::fmt; +use std::borrow::Cow; + +use rand::{self, Rng}; + +/// Table to retrieve base62 values from. +const BASE62: &'static [u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/// A _probably_ unique paste ID. +pub struct PasteID<'a>(Cow<'a, str>); + +impl<'a> PasteID<'a> { + /// Generate a _probably_ unique ID with `size` characters. For readability, + /// the characters used are from the sets [0-9], [A-Z], [a-z]. The + /// probability of a collision depends on the value of `size`. In + /// particular, the probability of a collision is 1/62^(size). + pub fn new(size: usize) -> PasteID<'static> { + let mut id = String::with_capacity(size); + let mut rng = rand::thread_rng(); + for _ in 0..size { + id.push(BASE62[rng.gen::() % 62] as char); + } + + PasteID(Cow::Owned(id)) + } +} + +impl<'a> fmt::Display for PasteID<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} +``` + +Then, in `src/main.rs`, add the following after `extern crate rocket`: + +```rust +extern crate rand; + +mod paste_id; + +use paste_id::PasteID; +``` + +Finally, add a dependency for the `rand` crate to the `Cargo.toml` file: + +```toml +[dependencies] +# existing Rocket dependencies... +rand = "0.3" +``` + +Then, ensure that your application builds with the new code: + +```sh +cargo build +``` + +You'll likely see many "unused" warnings for the new code we've added: that's +okay and expected. We'll be using the new code soon. + +### Processing + +Believe it or not, the hard part is done! (_whew!_). + +To process the upload, we'll need a place to store the uploaded files. To +simplify things, we'll store the uploads in a directory named `uploads/`. Create +an `upload` directory next to the `src` directory: + +```sh +mkdir upload +``` + +For the `upload` route, we'll need to `use` a few items: + +```rust +use std::io; +use std::path::Path; + +use rocket::Data; +``` + +The [Data](https://api.rocket.rs/rocket/data/struct.Data.html) structure is key +here: it represents an unopened stream to the incoming request body data. We'll +use it to efficiently stream the incoming request to a file. + +### Upload Route + +We're finally ready to write the `upload` route. Before we show you the code, +you should attempt to write the route yourself. Here's a hint: a possible route +and handler signature look like this: + +```rust +#[post("/", data = "")] +fn upload(paste: Data) -> io::Result +``` + +Your code should: + + 1. Create a new `PasteID` of a length of your choosing. + 2. Construct a filename inside `upload/` given the `PasteID`. + 3. Stream the `Data` to the file with the constructed filename. + 4. Construct a URL given the `PasteID`. + 5. Return the URL to the client. + +Here's our version (in `src/main.rs`): + +```rust +#[post("/", data = "")] +fn upload(paste: Data) -> io::Result { + let id = PasteID::new(3); + let filename = format!("upload/{id}", id = id); + let url = format!("{host}/{id}\n", host = "http://localhost:8000", id = id); + + // Write the paste out to the file and return the URL. + paste.stream_to_file(Path::new(&filename))?; + Ok(url) +} +``` + +Make sure that the route is mounted at the root path: + +```rust +fn main() { + rocket::ignite().mount("/", routes![index, upload]).launch() +} +``` + +Test that your route works via `cargo run`. From a separate terminal, upload a +file using `curl`. Then verify that the file was saved to the `upload` directory +with the correct ID: + +```sh +# in the project root +cargo run + +# in a seperate terminal +echo "Hello, world." | curl --data-binary @- http://localhost:8000 +# => http://localhost:8000/eGs + +# back to the terminal running the pastebin + # kill running process +ls upload # ensure the upload is there +cat upload/* # ensure that contents are correct +``` + +Note that since we haven't created a `GET /` route, visting the returned URL +will result in a **404**. We'll fix that now. + +## Retrieving Pastes + +The final step is to create the `retrieve` route which, given an ``, will +return the corresponding paste if it exists. + +Here's a first take at implementing the `retrieve` route. The route below takes +in an `` as a dynamic path element. The handler uses the `id` to construct a +path to the paste inside `upload/`, and then attempts to open the file at that +path, optionally returning the `File` if it exists. Rocket treats a `None` +[Responder](https://api.rocket.rs/rocket/response/trait.Responder.html#provided-implementations) +as a **404** error, which is exactly what we want to return when the requested +paste doesn't exist. + +```rust +use std::fs::File; + +#[get("/")] +fn retrieve(id: &str) -> Option { + let filename = format!("upload/{id}", id = id); + File::open(&filename).ok() +} +``` + +Unfortunately, there's a problem with this code. Can you spot the issue? + +The issue is that the _user_ controls the value of `id`, and as a result, can +coerce the service into opening files inside `upload/` that aren't meant to be +opened. For instance, imagine that you later decide that a special file +`upload/_credentials.txt` will store some important, private information. If the +user issues a `GET` request to `/_credentials.txt`, the server will read and +return the `upload/_credentials.txt` file, leaking the sensitive information. +This is a big problem; it's known as the [full path disclosure +attack](https://www.owasp.org/index.php/Full_Path_Disclosure), and Rocket +provides the tools to prevent this and other kinds of attacks from happening. + +To prevent the attack, we need to _validate_ `id` before we use it. Since the +`id` is a dynamic parameter, we can use Rocket's +[FromParam](https://api.rocket.rs/rocket/request/trait.FromParam.html) trait to +implement the validation and ensure that the `id` is a valid `PasteID` before +using it. We do this by implementing `FromParam` for `PasteID` in +`src/paste_id.rs`, as below: + +```rust +use rocket::request::FromParam; + +/// Returns `true` if `id` is a valid paste ID and `false` otherwise. +fn valid_id(id: &str) -> bool { + id.chars().all(|c| { + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + }) +} + +/// Returns an instance of `PasteID` if the path segment is a valid ID. +/// Otherwise returns the invalid ID as the `Err` value. +impl<'a> FromParam<'a> for PasteID<'a> { + type Error = &'a str; + + fn from_param(param: &'a str) -> Result, &'a str> { + match valid_id(param) { + true => Ok(PasteID(Cow::Borrowed(param))), + false => Err(param) + } + } +} +``` + +Then, we simply need to change the type of `id` in the handler to `PasteID`. +Rocket will then ensure that `` represents a valid `PasteID` before calling +the `retrieve` route, preventing attacks on the `retrieve` route: + +```rust +#[get("/")] +fn retrieve(id: PasteID) -> Option { + let filename = format!("upload/{id}", id = id); + File::open(&filename).ok() +} +``` + +Note that our `valid_id` function is simple and could be improved by, for +example, checking that the length of the `id` is within some known bound or +potentially blacklisting sensitive files as needed. + +The wonderful thing about using `FromParam` and other Rocket traits is that they +centralize policies. For instance, here, we've centralized the policy for valid +`PasteID`s in dynamic parameters. At any point in the future, if other routes +are added that require a `PasteID`, no further work has to be done: simply use +the type in the signature and Rocket takes care of the rest. + +## Conclusion + +That's it! Ensure that all of your routes are mounted and test your application. +You've now written a simple (~75 line!) pastebin in Rocket! There are many +potential improvements to this small application, and we encourage you to work +through some of them to get a better feel for Rocket. Here are some ideas: + + * Add a web form to the `index` where users can manually input new pastes. + Accept the form at `POST /`. Use `format` and/or `rank` to specify which of + the two `POST /` routes should be called. + * Support **deletion** of pastes by adding a new `DELETE /` route. Use + `PasteID` to validate ``. + * **Limit the upload** to a maximum size. If the upload exceeds that size, + return a **206** partial status code. Otherwise, return a **201** created + status code. + * Set the `Content-Type` of the return value in `upload` and `retrieve` to + `text/plain`. + * **Return a unique "key"** after each upload and require that the key is + present and matches when doing deletion. Use one of Rocket's core traits to + do the key validation. + * Add a `PUT /` route that allows a user with the key for `` to + replace the existing paste, if any. + * Add a new route, `GET //` that syntax highlights the paste with ID + `` for language ``. If `` is not a known language, do no + highlighting. Possibly validate `` with `FromParam`. + * Use the [testing module](https://api.rocket.rs/rocket/testing/) to write + unit tests for your pastebin. + * Dispatch a thread before `launch`ing Rocket in `main` that periodically + cleans up idling old pastes in `upload/`. + +You can find the full source code for the completed pastebin tutorial in the +[Rocket Github +Repo](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/pastebin). diff --git a/site/guide/quickstart.md b/site/guide/quickstart.md new file mode 100644 index 00000000..0bcbba9e --- /dev/null +++ b/site/guide/quickstart.md @@ -0,0 +1,21 @@ +# Quickstart + +Rocket requires a recent nightly version of Rust. We recommend you use +[rustup](https://rustup.rs/) to install or configure such a version. If you +don't have Rust installed, the [getting started](/guide/getting-started) section +guides you through installing Rust. + +## Running Examples + +The absolute fastest way to start experimenting with Rocket is to clone the +Rocket repository and run the included examples in the `examples/` directory. +For instance, the following set of commands runs the `hello_world` example: + +```sh +git clone https://github.com/SergioBenitez/rocket +cd rocket/examples/hello_world +cargo run +``` + +There are numerous examples. They can all be run with `cargo run`. + diff --git a/site/guide/requests.md b/site/guide/requests.md new file mode 100644 index 00000000..4225f0dd --- /dev/null +++ b/site/guide/requests.md @@ -0,0 +1,378 @@ +# Requests + +If all we could do was match against static paths like `"/world"`, Rocket +wouldn't be much fun. Of course, Rocket allows you to match against just about +any information in an incoming request. This section describes the available +options and their effect on the application. + +## Methods + +A Rocket route attribute can be any one of `get`, `put`, `post`, `delete`, +`head`, `patch`, or `options`, each corresponding to the HTTP method to match +against. For example, the following attribute will match against `POST` requests +to the root path: + +```rust +#[post("/")] +``` + +The grammar for these routes is defined formally in the +[rocket_codegen](https://api.rocket.rs/rocket_codegen/) API docs. + +Rocket handles `HEAD` requests automatically when there exists a `GET` route +that would otherwise match. It does this by stripping the body from the +response, if there is one. You can also specialize the handling of a `HEAD` +request by declaring a route for it; Rocket won't interfere with `HEAD` requests +your application handles. + +Because browsers only send `GET` and `POST` requests, Rocket _reinterprets_ +requests under certain conditions. If a `POST` request contains a body of +`Content-Type: application/x-www-form-urlencoded`, and the form's **first** +field has the name `_method` and a valid HTTP method as its value, that field's +value is used as the method for the incoming request. This allows Rocket +applications to submit non-`POST` forms. The [todo +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/todo/static/index.html.tera#L47) +makes use of this feature to submit `PUT` and `DELETE` requests from a web form. + +## Format + +When receiving data, you can specify the Content-Type the route matches against +via the `format` route parameter. The parameter is a string of the Content-Type +expected. For example, to match `application/json` data, a route can be declared +as: + +```rust +#[post("/user", format = "application/json", data = "")] +fn new_user(user: JSON) -> T { ... } +``` + +Note the `format` parameter in the `post` attribute. The `data` parameter is +described later in the [data](#data) section. + +## Dynamic Paths + +You can declare path segments as dynamic by using angle brackets around variable +names in a route's path. For example, if we wanted to say _Hello!_ to anything, +not just the world, we could declare a route and handler like so: + +```rust +#[get("/hello/")] +fn hello(name: &str) -> String { + format!("Hello, {}!", name) +} +``` + +If we were to mount the path at the root (`.mount("/", routes![hello])`), then +any request to a path with two non-empty segments, where the first segment is +`hello`, will be dispatched to the `hello` route. For example, if we were to +visit `/hello/John`, the application would respond with `Hello, John!`. + +You can have any number of dynamic path segments, and the type of the path +segment can be any type that implements the [FromParam +trait](https://api.rocket.rs/rocket/request/trait.FromParam.html), including +your own! Rocket implements `FromParam` for many of the standard library types, +as well as a few special Rocket types. Here's a somewhat complicated route to +illustrate varied usage: + +```rust +#[get("/hello///")] +fn hello(name: &str, age: u8, cool: bool) -> String { + if cool { + format!("You're a cool {} year old, {}!", age, name) + } else { + format!("{}, we need to talk about your coolness.", name) + } +} +``` + +## Forwarding + +In this example above, what if `cool` isn't a `bool`? Or, what if `age` isn't a +`u8`? In this case, the request is _forwarded_ to the next matching route, if +there is any. This continues until a route doesn't forward the request or there +are no remaining routes to try. When there are no remaining matching routes, a +customizable **404 error** is returned. + +Routes are tried in increasing _rank_ order. By default, routes with static +paths have a rank of 0 and routes with dynamic paths have a rank of 1. A route's +rank can be manually set with the `rank` route parameter. + +To illustrate, consider the following routes: + +```rust +#[get("/user/")] +fn user(id: usize) -> T { ... } + +#[get("/user/", rank = 2)] +fn user_int(id: isize) -> T { ... } + +#[get("/user/", rank = 3)] +fn user_str(id: &str) -> T { ... } +``` + +Notice the `rank` parameters in `user_int` and `user_str`. If we run this +application with the routes mounted at the root, requests to `/user/` will +be routed as follows: + + 1. The `user` route matches first. If the string at the `` position is an + unsigned integer, then the `user` handler is called. If it is not, then the + request is forwarded to the next matching route: `user_int`. + + 2. The `user_int` route matches next. If `` is a signed integer, + `user_int` is called. Otherwise, the request is forwarded. + + 3. The `user_str` route matches last. Since `` is a always string, the + route always matches. The `user_str` handler is called. + +Forwards can be _caught_ by using a `Result` or `Option` type. For example, if +the type of `id` in the `user` function was `Result`, then `user` +would never forward. An `Ok` variant would indicate that `` was a valid +`usize`, while an `Err` would indicate that `` was not a `usize`. The +`Err`'s value would contain the string that failed to parse as a `usize`. + +By the way, if you were to omit the `rank` parameter in the `user_str` or +`user_int` routes, Rocket would emit a warning indicating that the routes +_collide_, or can match against similar incoming requests. The `rank` parameter +resolves this collision. + +## Dynamic Segments + +You can also match against multiple segments by using `` in the route +path. The type of such parameters, known as _segments_ parameters, can be any +that implements +[FromSegments](https://api.rocket.rs/rocket/request/trait.FromSegments.html). +Segments parameters must be the final component of the path: any text after a +segments parameter in a path will result in a compile-time error. + +As an example, the following route matches against all paths that begin with +`/page/`: + +```rust +#[get("/page/")] +fn get_page(path: PathBuf) -> T { ... } +``` + +The path after `/page/` will be available in the `path` parameter. The +`FromSegments` implementation for `PathBuf` ensures that `path` cannot lead to +[path traversal attacks](https://www.owasp.org/index.php/Path_Traversal). With +this, a safe and secure static file server can implemented in 4 lines: + +```rust +#[get("/")] +fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(file)).ok() +} +``` + +## Request Guards + +Sometimes we need data associated with a request that isn't a direct input. +Headers and cookies are a good example of this: they simply tag along for the +ride. Rocket makes retrieving and validating such information easy: simply add +any number of parameters to the request handler with types that implement the +[FromRequest](https://api.rocket.rs/rocket/request/trait.FromRequest.html) +trait. If the data can be retrieved from the incoming request and validated, the +handler is called. If it cannot, the handler isn't called, and the request is +forwarded or terminated. In this way, these parameters act as _guards_: they +protect the request handler from being called erroneously. + +For example, to retrieve cookies and the Content-Type header from a request, we +can declare a route as follows: + +```rust +#[get("/")] +fn index(cookies: &Cookies, content: ContentType) -> String { ... } +``` + +The [cookies example on +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/cookies) +illustrates how to use the `Cookies` type to get and set cookies. + +You can implement `FromRequest` for your own types. For instance, to protect a +`sensitive` route from running unless an `APIKey` is present in the request +headers, you might create an `APIKey` type that implements `FromRequest` and use +it as a request guard: + +```rust +#[get("/sensitive")] +fn sensitive(key: APIKey) -> &'static str { ... } +``` + +You might also implement `FromRequest` for an `AdminUser` type that validates +that the cookies in the incoming request authenticate an administrator. Then, +any handler with an `AdminUser` or `APIKey` type in its argument list is assured +to only be invoked if the appropriate conditions are met. Request guards +centralize policies, resulting in a simpler, safer, and more secure +applications. + +## Data + +At some point, your web application will need to process body data, and Rocket +makes it as simple as possible. Data processing, like much of Rocket, is type +directed. To indicate that a handler expects data, annotate it with a `data = +""` parameter, where `param` is an argument in the handler. The +argument's type must implement the +[FromData](https://api.rocket.rs/rocket/data/trait.FromData.html) trait. It +looks like this, where `T: FromData`: + +```rust +#[post("/", data = "")] +fn new(input: T) -> String { ... } +``` + +### Forms + +Forms are the most common type of data handled in web applications, and Rocket +makes handling them easy. Say your application is processing a form submission +for a new todo `Task`. The form contains two fields: `complete`, a checkbox, and +`description`, a text field. You can easily handle the form request in Rocket +as follows: + +```rust +#[derive(FromForm)] +struct Task { + complete: bool, + description: String, +} + +#[post("/todo", data = "")] +fn new(task: Form) -> String { ... } +``` + +The `Form` type implements the `FromData` trait as long as its generic parameter +implements the +[FromForm](https://api.rocket.rs/rocket/request/trait.FromForm.html) trait. In +the example, we've derived the `FromForm` trait automatically for the `Task` +structure. `FromForm` can be derived for any structure whose fields implement +[FromFormValue](https://api.rocket.rs/rocket/request/trait.FromFormValue.html). +If a `POST /todo` request arrives, the form data will automatically be parsed +into the `Task` structure. If the data that arrives isn't of the correct +Content-Type, the request is forwarded. If the data doesn't parse or is simply +invalid, a customizable `400 Bad Request` error is returned. As before, a +forward or failure can be caught by using the `Option` and `Result` types. + +Fields of forms can be easily validated via implementations of the +`FromFormValue` trait. For example, if you'd like to verify that some user is +over some age in a form, then you might define a new `AdultAge` type, use it as +a field in a form structure, and implement `FromFormValue` so that it only +validates integers over that age. If a form is a submitted with a bad age, +Rocket won't call a handler requiring a valid form for that structure. You can +use `Option` or `Result` types for fields to catch parse failures. + +The [forms](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/forms) +and [forms kitchen +sink](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/form_kitchen_sink) +examples on GitHub provide further illustrations. + +### JSON + +Handling JSON data is no harder: simply use the +[JSON](https://api.rocket.rs/rocket_contrib/struct.JSON.html) type: + +```rust +#[derive(Deserialize)] +struct Task { + description: String, + complete: bool +} + +#[post("/todo", data = "")] +fn new(task: JSON) -> String { ... } +``` + +The only condition is that the generic type to `JSON` implements the +`Deserialize` trait. See the [JSON example on +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/json) for a +complete example. + +### Streaming + +Sometimes you just want to handle the incoming data directly. For example, you +might want to stream the incoming data out to a file. Rocket makes this as +simple as possible via the +[Data](https://api.rocket.rs/rocket/data/struct.Data.html) type: + +```rust +#[post("/upload", format = "text/plain", data = "")] +fn upload(data: Data) -> io::Result> { + data.stream_to_file("/tmp/upload.txt").map(|n| Plain(n.to_string())) +} +``` + +The route above accepts any `POST` request to the `/upload` path with +`Content-Type` `text/plain` The incoming data is streamed out to +`tmp/upload.txt` file, and the number of bytes written is returned as a plain +text response if the upload succeeds. If the upload fails, an error response is +returned. The handler above is complete. It really is that simple! See the +[GitHub example +code](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/raw_upload) +for the full crate. + +## Query Strings + +Query strings are handled similarly to `POST` forms. A query string can be +parsed into any structure that implements the `FromForm` trait. They are matched +against by appending a `?` followed by a dynamic parameter `` to the +path. + +For instance, say you change your mind and decide to use query strings instead +of `POST` forms for new todo tasks in the previous forms example, reproduced +below: + +```rust +#[derive(FromForm)] +struct Task { .. } + +#[post("/todo", data = "")] +fn new(task: Form) -> String { ... } +``` + +Rocket makes the transition simple: simply declare `` as a query parameter +as follows: + +```rust +#[get("/todo?")] +fn new(task: Task) -> String { ... } +``` + +Rocket will parse the query string into the `Task` structure automatically by +matching the structure field names to the query parameters. If the parse fails, +the request is forwarded to the next matching route. To catch parse failures, +you can use `Option` or `Result` as the type of the field to catch errors for. + +See [the GitHub +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/query_params) +for a complete illustration. + +## Error Catchers + +When Rocket wants to return an error page to the client, Rocket invokes the +_catcher_ for that error. A catcher is like a route, except it only handles +errors. Catchers are declared via the `error` attribute, which takes a single +integer corresponding to the HTTP status code to catch. For instance, to declare +a catcher for **404** errors, you'd write: + +```rust +#[error(404)] +fn not_found(req: &Request) -> String { } +``` + +As with routes, Rocket needs to know about a catcher before it is used to handle +errors. The process is similar to mounting: call the `catch` method with a list +of catchers via the `errors!` macro. The invocation to add the **404** catcher +declared above looks like this: + +```rust +rocket::ignite().catch(errors![not_found]) +``` + +Unlike request handlers, error handlers can only take 0, 1, or 2 parameters of +types [Request](https://api.rocket.rs/rocket/struct.Request.html) and/or +[Error](https://api.rocket.rs/rocket/enum.Error.html). At present, the `Error` +type is not particularly useful, and so it is often omitted. The +[error catcher +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/errors) on +GitHub illustrates their use in full. + +Rocket has a default catcher for all of the standard HTTP error codes including +**404**, **500**, and more. diff --git a/site/guide/responses.md b/site/guide/responses.md new file mode 100644 index 00000000..6a9fb3f0 --- /dev/null +++ b/site/guide/responses.md @@ -0,0 +1,169 @@ +# Responses + +You may have noticed that the return type of a handler appears to be arbitrary, +and that's because it is! A value of any type that implements the +[Responder](https://api.rocket.rs/rocket/response/trait.Responder.html) trait +can be returned, including your own. + +## Responder + +Types that implement `Responder` know how to generate a +[Response](https://api.rocket.rs/rocket/response/struct.Response.html) from +their values. A `Response` includes the HTTP status, headers, and body of the +response. Rocket implements `Responder` for many built-in types including +`String`, `&str`, `File`, `Option`, `Result`, and others. Rocket also provides +custom types, such as +[Content](https://api.rocket.rs/rocket/response/struct.Content.html) and +[Flash](https://api.rocket.rs/rocket/response/struct.Flash.html), which you can +find in the [response](https://api.rocket.rs/rocket/response/index.html) module. + +The body of a `Response` may either be _fixed-sized_ or _streaming_. The given +`Responder` implementation decides which to use. For instance, `String` uses a +fixed-sized body, while `File` uses a streaming body. + +### Wrapping + +Responders can _wrap_ other responders. That is, responders can be of the +following form, where `R: Responder`: + +```rust +struct WrappingResponder(R); +``` + +When this is the case, the wrapping responder will modify the response returned +by `R` in some way before responding itself. For instance, to override the +status code of some response, you can use the types in the [status +module](https://api.rocket.rs/rocket/response/status/index.html). In particular, +to set the status code of a response for a `String` to **202 Accepted**, you can +return a type of `status::Accepted`: + +```rust +#[get("/")] +fn accept() -> status::Accepted { + status::Accepted(Some("I accept!".to_string())) +} +``` + +By default, the `String` responder sets the status to **200**. By using the +`Accepted` type however, The client will receive an HTTP response with status +code **202**. + +Similarly, the types in the [content +module](https://api.rocket.rs/rocket/response/content/index.html) can be used to +override the Content-Type of the response. For instance, to set the Content-Type +of some `&'static str` to JSON, you can use the +[content::JSON](https://api.rocket.rs/rocket/response/content/struct.JSON.html) +type as follows: + +```rust +#[get("/")] +fn json() -> content::JSON<&'static str> { + content::JSON("{ 'hi': 'world' }") +} +``` + +## Errors + +Responders need not _always_ generate a response. Instead, they can return an +`Err` with a given status code. When this happens, Rocket forwards the request +to the error catcher for the given status code. If none exists, which can only +happen when using custom status codes, Rocket uses the **500** error catcher. + +### Result + +`Result` is one of the most commonly used responders. Returning a `Result` means +one of two things. If the error type implements `Responder`, the `Ok` or `Err` +value will be used, whichever the variant is. If the error type does _not_ +implement `Responder`, the error is printed to the console, and the request is +forwarded to the **500** error catcher. + +### Option + +`Option` is another commonly used responder. If the `Option` is `Some`, the +wrapped responder is used to respond to the client. Otherwise, the request is +forwarded to the **404** error catcher. + +### Failure + +While not encouraged, you can also forward a request to a catcher manually by +using the [Failure](https://api.rocket.rs/rocket/response/struct.Failure.html) +type. For instance, to forward to the catcher for **406 Not Acceptable**, you +would write: + +```rust +#[get("/")] +fn just_fail() -> Failure { + Failure(Status::NotAcceptable) +} +``` + +## JSON + +Responding with JSON data is simple: return a value of type +[JSON](https://api.rocket.rs/rocket_contrib/struct.JSON.html). For example, to +respond with the JSON value of the `Task` structure from previous examples, we +would write: + +```rust +#[derive(Serialize)] +struct Task { ... } + +#[get("/todo")] +fn todo() -> JSON { ... } +``` + +The generic type in `JSON` must implement `Serialize`. The `JSON` type +serializes the structure into JSON, sets the Content-Type to JSON, and emits the +serialization in a fixed-sized body. If serialization fails, the request is +forwarded to the **500** error catcher. + +For a complete example, see the [JSON example on +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/json). + +## Templates + +Rocket has built-in support for templating. To respond with a rendered template, +simply return a +[Template](https://api.rocket.rs/rocket_contrib/struct.Template.html) type. + +```rust +#[get("/")] +fn index() -> Template { + let context = /* object-like value */; + Template::render("index", &context) +} +``` + +Templates are rendered with the `render` method. The method takes in the name of +a template and a context to render the template with. Rocket searches for a +template with that name in the configurable `template_dir` configuration +parameter, which defaults to `templates/`. Templating support in Rocket is +engine agnostic. The engine used to render a template depends on the template +file's extension. For example, if a file ends with `.hbs`, Handlebars is used, +while if a file ends with `.tera`, Tera is used. + +The context can be any type that implements `Serialize` and serializes to an +`Object` value, such as structs, `HashMaps`, and others. The +[Template](https://api.rocket.rs/rocket_contrib/struct.Template.html) API +documentation contains more information about templates, while the [Handlebars +Templates example on +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/handlebars_templates) +is a fully composed application that makes use of Handlebars templates. + +## Streaming + +When a large amount of data needs to be sent to the client, it is better to +stream the data to the client to avoid consuming large amounts of memory. Rocket +provides the [Stream](https://api.rocket.rs/rocket/response/struct.Stream.html) +type, making this easy. The `Stream` type can be created from any `Read` type. +For example, to stream from a local Unix stream, we might write: + +```rust +#[get("/stream")] +fn stream() -> io::Result> { + UnixStream::connect("/path/to/my/socket").map(|s| Stream::from(s)) +} + +``` + +Rocket takes care of the rest. diff --git a/site/guide/state.md b/site/guide/state.md new file mode 100644 index 00000000..ede89ee0 --- /dev/null +++ b/site/guide/state.md @@ -0,0 +1,144 @@ +# State + +Many web applications have a need to maintain state. This can be as simple as +maintaining a counter for the number of visits or as complex as needing to +access job queues and multiple databases. Rocket provides the tools to enable +these kinds of interactions in a safe and simple manner. + +## Managed State + +The enabling feature for maintaining state is _managed state_. Managed state, as +the name implies, is state that Rocket manages for your application. The state +is managed on a per-type basis: Rocket will manage at most one value of a given +type. + +The process for using managed state is simple: + + 1. Call `manage` on the `Rocket` instance corresponding to your application + with the initial value of the state. + 2. Add a `State` type to any request handler, where `T` is the type of the + value passed into `manage`. + +### Adding State + +To instruct Rocket to manage state for your application, call the +[manage](https://api.rocket.rs/rocket/struct.Rocket.html#method.manage) method +on a `Rocket` instance. For example, to ask Rocket to manage a `HitCount` +structure with an internal `AtomicUsize` with an initial value of `0`, we can +write the following: + +```rust +struct HitCount(AtomicUsize); + +rocket::ignite().manage(HitCount(AtomicUsize::new(0))); +``` + +The `manage` method can be called any number of times as long as each call +refers to a value of a different type. For instance, to have Rocket manage both +a `HitCount` value and a `Config` value, we can write: + +```rust +rocket::ignite() + .manage(HitCount(AtomicUsize::new(0))) + .manage(Config::from(user_input)); +``` + +### Retrieving State + +State that is being managed by Rocket can be retrieved via the +[State](https://api.rocket.rs/rocket/struct.State.html) type: a [request +guard](/guide/requests/#request-guards) for managed state. To use the request +guard, add a `State` type to any request handler, where `T` is the +type of the managed state. For example, we can retrieve and respond with the +current `HitCount` in a `count` route as follows: + +```rust +#[get("/count")] +fn count(count: State) -> String { + let current_count = hit_count.0.load(Ordering::Relaxed); + format!("Number of visits: {}", current_count) +} +``` + +You can retrieve more than one `State` type in a single route as well: + +```rust +#[get("/state")] +fn state(count: State, config: State) -> T { ... } +``` + +It can also be useful to retrieve managed state from a `FromRequest` +implementation. To do so, invoke the `from_request` method of a `State` type +directly, passing in the `req` parameter of `from_request`: + +```rust +fn from_request(req: &'a Request<'r>) -> request::Outcome { + let count = match as FromRequest>::from_request(req) { + Outcome::Success(count) => count, + ... + }; + ... +} +``` + +### Unmanaged State + +If you request a `State` for a `T` that is not `managed`, Rocket won't call +the offending route. Instead, Rocket will log an error message and return a +**500** error to the client. + +While this behavior is 100% safe, it isn't fun to return **500** errors to +clients, especially when the issue can be easily avoided. Because of this, +Rocket tries to prevent an application with unmanaged state from ever running +via the `unmanaged_state` lint. The lint reads through your code at compile-time +and emits a warning when a `State` request guard is being used in a mounted +route for a type `T` that isn't being managed. + +As an example, consider the following short application using our `HitCount` +type from previous examples: + +```rust +#[get("/count")] +fn count(count: State) -> String { + let current_count = hit_count.0.load(Ordering::Relaxed); + format!("Number of visits: {}", current_count) +} + +fn main() { + rocket::ignite() + .manage(Config::from(user_input)) + .launch() +} +``` + +The application is buggy: a value for `HitCount` isn't being `managed`, but a +`State` type is being requested in the `count` route. When we compile +this application, Rocket emits the following warning: + +```rust +warning: HitCount is not currently being managed by Rocket + --> src/main.rs:2:17 + | +2 | fn count(count: State) -> String { + | ^^^^^^^^^^^^^^^ + | + = note: this State request guard will always fail +help: maybe add a call to 'manage' here? + --> src/main.rs:8:5 + | +8 | rocket::ignite() + | ^^^^^^^^^^^^^^^^ +``` + +The `unmanaged_state` lint isn't perfect. In particular, it cannot track calls +to `manage` across function boundaries. You can disable the lint on a per-route +basis by adding `#[allow(unmanaged_state)]` to a route handler. If you wish to +disable the lint globally, add `#![allow(unmanaged_state)]` to your crate +attributes. + +You can find a complete example using the `HitCounter` structure in the [state +example on +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/state) and +learn more about the [manage +method](https://api.rocket.rs/rocket/struct.Rocket.html#method.manage) and +[State type](https://api.rocket.rs/rocket/struct.State.html) in the API docs. diff --git a/site/guide/testing.md b/site/guide/testing.md new file mode 100644 index 00000000..6144d5dc --- /dev/null +++ b/site/guide/testing.md @@ -0,0 +1,136 @@ +# Testing + +Every application should be well tested. Rocket provides the tools to perform +unit and integration tests on your application as well as inspect Rocket +generated code. + +## Tests + +Rocket includes a built-in [testing](https://api.rocket.rs/rocket/testing/) +module that allows you to unit and integration test your Rocket applications. +Testing is simple: + + 1. Construct a `Rocket` instance. + 2. Construct a `MockRequest`. + 3. Dispatch the request using the `Rocket` instance. + 4. Inspect, validate, and verify the `Response`. + +After setting up, we'll walk through each of these steps for the "Hello, world!" +program below: + +```rust +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +#[get("/")] +fn hello() -> &'static str { + "Hello, world!" +} +``` + +### Setting Up + +For the `testing` module to be available, Rocket needs to be compiled with the +_testing_ feature enabled. Since this feature should only be enabled when your +application is compiled for testing, the recommended way to enable the _testing_ +feature is via Cargo's `[dev-dependencies]` section in the `Cargo.toml` file as +follows: + +```toml +[dev-dependencies] +rocket = { version = "0.2.5", features = ["testing"] } +``` + +With this in place, running `cargo test` will result in Cargo compiling Rocket +with the _testing_ feature, thus enabling the `testing` module. + +You'll also need a `test` module with the proper imports: + +```rust +#[cfg(test)] +mod test { + use super::rocket; + use rocket::testing::MockRequest; + use rocket::http::{Status, Method}; + + #[test] + fn hello_world() { + ... + } +} +``` + +In the remainder of this section, we'll work on filling in the `hello_world` +testing function to ensure that the `hello` route results in a `Response` with +_"Hello, world!"_ in the body. + +### Testing + +We'll begin by constructing a `Rocket` instance with the `hello` route mounted +at the root path. We do this in the same way we would normally with one +exception: we need to refer to the `testing` route in the `super` namespace: + +```rust +let rocket = rocket::ignite().mount("/", routes![super::hello]); +``` + +Next, we create a `MockRequest` that issues a `Get` request to the `"/"` path: + +```rust +let mut req = MockRequest::new(Method::Get, "/"); +``` + +We now ask Rocket to perform a full dispatch, which includes routing, +pre-processing and post-processing, and retrieve the `Response`: + +```rust +let mut response = req.dispatch_with(&rocket); +``` + +Finally, we can test the +[Response](https://api.rocket.rs/rocket/struct.Response.html) values to ensure +that it contains the information we expect it to. We want to ensure two things: + + 1. The status is `200 OK`. + 2. The body is the string "Hello, world!". + +We do this by querying the `Response` object directly: + +```rust +assert_eq!(response.status(), Status::Ok); + +let body_str = response.body().and_then(|b| b.into_string()); +assert_eq!(body_str, Some("Hello, world!".to_string())); +``` + +That's it! Run the tests with `cargo test`. The complete application, with +testing, can be found in the [GitHub testing +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/testing). + +## Codegen Debug + +It is sometimes useful to inspect the code that Rocket's code generation is +emitting, especially when you get a strange type error. To have Rocket log the +code that it is emitting to the console, set the `ROCKET_CODEGEN_DEBUG` +environment variable when compiling: + +```rust +ROCKET_CODEGEN_DEBUG=1 cargo build +``` + +During compilation, you should see output like this: + +```rust +Emitting item: +fn rocket_route_fn_hello<'_b>(_req: &'_b ::rocket::Request, + _data: ::rocket::Data) + -> ::rocket::handler::Outcome<'_b> { + let responder = hello(); + ::rocket::handler::Outcome::of(responder) +} +``` + +This corresponds to the facade request handler Rocket generated for the `hello` +route. diff --git a/site/index.toml b/site/index.toml new file mode 100644 index 00000000..2e616274 --- /dev/null +++ b/site/index.toml @@ -0,0 +1,193 @@ +############################################################################### +# Top features: displayed in the header under the introductory text. +############################################################################### + +[release] +url = "https://crates.io/crates/rocket" +version = "0.2.5" +date = "Apr 16, 2017" + +[[top_features]] +title = "Type Safe" +text = "From request to response Rocket ensures that your types mean something." +image = "helmet" +button = "Learn More" +url = "/overview/#how-rocket-works" + +[[top_features]] +title = "Boilerplate Free" +text = "Spend your time writing code that really matters, and let Rocket generate the rest." +image = "robot-free" +button = "See Examples" +url = "/overview/#anatomy-of-a-rocket-application" + +[[top_features]] +title = "Easy To Use" +text = "Rocket makes extensive use of Rust's code generation tools to provide a clean API." +image = "sun" +button = "Get Started" +url = "/guide" +margin = 2 + +[[top_features]] +title = "Extensible" +text = "Easily create your own primitives that any Rocket application can use." +image = "telescope" +button = "See How" +url = "/overview/#anatomy-of-a-rocket-application" +margin = 9 + +############################################################################### +# Sections: make sure there are an odd number so colors work out. +############################################################################### + +[[sections]] +title = "Hello, Rocket!" +code = ''' + #![feature(plugin)] + #![plugin(rocket_codegen)] + + extern crate rocket; + + #[get("/hello//")] + fn hello(name: &str, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) + } + + fn main() { + rocket::ignite().mount("/", routes![hello]).launch(); + } +''' +text = ''' + This is a **complete Rocket application**. It does exactly what you would + expect. If you were to visit **http://localhost:8000/hello/John/58**, you’d + see: + + Hello, 58 year old named John! + + If someone visits a path with an `` that isn’t a `u8`, Rocket doesn’t + blindly call `hello`. Instead, it tries other matching routes or returns a + **404**. +''' + +[[sections]] +title = "Forms? Check!" +code = ''' + #[derive(FromForm)] + struct Task { + description: String, + completed: bool + } + + #[post("/", data = "")] + fn new(task: Form) -> Flash { + if task.get().description.is_empty() { + Flash::error(Redirect::to("/"), "Cannot be empty.") + } else { + Flash::success(Redirect::to("/"), "Task added.") + } + } +''' +text = ''' + Handling forms **is simple and easy**. Simply derive `FromForm` for your + structure and let Rocket know which parameter to use. Rocket **parses and + validates** the form request, creates the structure, and calls your function. + + Bad form request? Rocket doesn’t call your function! What if you want to know + if the form was bad? Simple! Change the type of `task` to `Option` or + `Result`! +''' + +[[sections]] +title = "JSON, out of the box." +code = ''' + #[derive(Serialize, Deserialize)] + struct Message { + contents: String, + } + + #[put("/", data = "")] + fn update(id: ID, message: JSON) -> JSON { + if DB.contains_key(&id) { + DB.insert(id, &message.contents); + JSON(json!{ "status": "ok" }) + } else { + JSON(json!{ "status": "error" }) + } + } +''' +text = ''' + Rocket has first-class support for JSON, right out of the box. Simply derive + `Deserialize` or `Serialize` to receive or return JSON, respectively. + + Like other important features, JSON works through Rocket’s `FromData` trait, + Rocket’s approach to deriving types from body data. It works like this: + specify a `data` route parameter of any type that implements `FromData`. A + value of that type will then be created automatically from the incoming + request body. Best of all, you can implement `FromData` for your types! +''' + +############################################################################### +# Buttom features: displayed above the footer. +############################################################################### + +[[bottom_features]] +title = 'Templating' +text = "Rocket makes rendering templates a breeze with built-in templating support." +image = 'templating-icon' +url = '/guide/responses/#templates' +button = 'Learn More' +color = 'blue' + +[[bottom_features]] +title = 'Cookies' +text = "Cookies are first-class in Rocket. View, add, or remove cookies without hassle." +image = 'cookies-icon' +url = '/guide/requests/#request-guards' +button = 'Learn More' +color = 'purple' +margin = -6 + +[[bottom_features]] +title = 'Streams' +text = "Rocket streams all incoming and outgoing data, so size isn't a concern." +image = 'streams-icon' +url = '/guide/requests/#streaming' +button = 'Learn More' +color = 'red' +margin = -29 + +[[bottom_features]] +title = 'Config Environments' +text = "Configure your application your way for development, staging, and production." +image = 'config-icon' +url = '/guide/overview/#configuration' +button = 'Learn More' +color = 'yellow' +margin = -3 + +[[bottom_features]] +title = 'Query Params' +text = "Handling query parameters isn’t an afterthought in Rocket." +image = 'query-icon' +url = '/guide/requests/#query-strings' +button = 'Learn More' +color = 'orange' +margin = -3 + +[[bottom_features]] +title = 'Testing Library' +text = "Unit test your applications with ease using the built-in testing library." +image = 'testing-icon' +url = '/guide/testing#testing' +button = 'Learn More' +color = 'green' + +# Blocked on Hyper/OpenSSL. +# [[bottom_features]] +# title = 'Signed Sessions' +# text = "Safe, secure, signed sessions are built-in to Rocket so your users can stay safe." +# image = 'sessions-icon' +# url = '/overview' +# button = 'Learn More' +# color = 'green' diff --git a/site/news.toml b/site/news.toml new file mode 100644 index 00000000..bcf11e14 --- /dev/null +++ b/site/news.toml @@ -0,0 +1,15 @@ +[[articles]] +title = "Rocket v0.2: Managed State & More" +date = "February 06, 2017" +snippet = """ +Today marks the first major release since Rocket's debut a little over a month +ago. Rocket v0.2 packs a ton of new features, fixes, and general improvements. +Much of the development in v0.2 was led by the community, either through reports +via the [GitHub issue tracker](https://github.com/SergioBenitez/Rocket/issues) +or via direct contributions. In fact, there have been **20 unique contributors** +to Rocket's code since Rocket's initial introduction! Community feedback has +been incredible. As a special thank you, we include the names of these +contributors at the end of this article. +""" +slug = "2017-02-06-version-0.2" + diff --git a/site/news/2017-02-06-version-0.2.md b/site/news/2017-02-06-version-0.2.md new file mode 100644 index 00000000..aa17f036 --- /dev/null +++ b/site/news/2017-02-06-version-0.2.md @@ -0,0 +1,392 @@ +# Rocket v0.2: Managed State & More + +**Posted by [Sergio Benitez](https://sergio.bz) on February 06, 2017** + +Today marks the first major release since Rocket's debut a little over a month +ago. Rocket v0.2 packs a ton of new features, fixes, and general improvements. +Much of the development in v0.2 was led by the community, either through reports +via the [GitHub issue tracker](https://github.com/SergioBenitez/Rocket/issues) +or via direct contributions. In fact, there have been **20 unique contributors** +to Rocket's codebase since Rocket's initial introduction! Community feedback has +been incredible. As a special thank you, we include the names of these +contributors at the end of this article. + +## About Rocket + +Rocket is a web framework for Rust with a focus on ease of use, expressibility, +and speed. Rocket makes it simple to write fast web applications without +sacrificing flexibility or type safety. All with minimal code. + +> Rocket's so simple, you feel like you're doing something wrong. It's like if +> you're making fire with rocks and suddently someone gives you a lighter. Even +> though you know the lighter makes fire, and does it even faster and better and +> with a simple flick, the rock's still in your brain. +> +> -- Artem "impowski" Biryukov, January 17, 2017, on **#rocket** + +## New Features + +Rocket v0.2 includes several new features that make developing Rocket +applications simpler, faster, and safer than ever before. + +### Managed State + +Undoubtedly, the star feature of this release is **managed state**. Managed +state allows you to pass state to Rocket prior to launching your application and +later retrieve that state from any request handler by simply including the +state's type in the function signature. It works in two easy steps: + + 1. Call `manage` on the `Rocket` instance corresponding to your application + with the initial value of the state. + 2. Add a `State` type to any request handler, where `T` is the type of the + value passed into `manage`. + +Rocket takes care of the rest! `State` works through Rocket's [request +guards](/guide/requests/#request-guards). You can call `manage` any number of +times, as long as each call corresponds to a value of a different type. + +As a simple example, consider the following "hit counter" example application: + +```rust +struct HitCount(AtomicUsize); + +#[get("/")] +fn index(hit_count: State) -> &'static str { + hit_count.0.fetch_add(1, Ordering::Relaxed); + "Your visit has been recorded!" +} + +#[get("/count")] +fn count(hit_count: State) -> String { + hit_count.0.load(Ordering::Relaxed).to_string() +} + +fn main() { + rocket::ignite() + .mount("/", routes![index, count]) + .manage(HitCount(AtomicUsize::new(0))) + .launch() +} +``` + +Visiting `/` will record a visit by incrementing the hit count by 1. Visiting +the `/count` path will display the current hit count. + +One concern when using _managed state_ is that you might forget to call `manage` +with some state's value before launching your application. Not to worry: Rocket +has your back! Let's imagine for a second that we forgot to add the call to +`manage` on line 17 in the example above. Here's what the compiler would emit +when we compile our buggy application: + +```rust +warning: HitCount is not currently being managed by Rocket + --> src/main.rs:4:21 + | +4 | fn index(hit_count: State) -> &'static str { + | ^^^^^^^^^^^^^^^ + | + = note: this State request guard will always fail +help: maybe add a call to 'manage' here? + --> src/main.rs:15:5 + | +15| rocket::ignite() + | ^^^^^^^^^^^^^^^^ + +warning: HitCount is not currently being managed by Rocket + --> src/main.rs:10:21 + | +10 | fn count(hit_count: State) -> String { + | ^^^^^^^^^^^^^^^ + | + = note: this State request guard will always fail +help: maybe add a call to 'manage' here? + --> src/main.rs:15:5 + | +15 | rocket::ignite() + | ^^^^^^^^^^^^^^^^ +``` + +You can read more about managed state in the [guide](/guide/state/), the API +docs for +[manage](https://api.rocket.rs/rocket/struct.Rocket.html#method.manage), and the +API docs for [State](https://api.rocket.rs/rocket/struct.State.html). + +### Unmounted Routes Lint + +A common mistake that new Rocketeers make is forgetting to +[mount](/guide/overview/#mounting) declared routes. In Rocket v0.2, Rocket adds +a _lint_ that results in a compile-time warning for unmounted routes. As a +simple illustration, consider the canonical "Hello, world!" Rocket application +below, and note that we've forgotten to mount the `hello` route: + +```rust +#[get("/")] +fn hello() -> &'static str { + "Hello, world!" +} + +fn main() { + rocket::ignite().launch(); +} +``` + +When this program is compiled, the compiler emits the following warning: + +```rust +warning: the 'hello' route is not mounted + --> src/main.rs:2:1 + | +2 | fn hello() -> &'static str { + | _^ starting here... +3 | | "Hello, world!" +4 | | } + | |_^ ...ending here + | + = note: Rocket will not dispatch requests to unmounted routes. +help: maybe add a call to 'mount' here? + --> src/main.rs:7:5 + | +7 | rocket::ignite().launch(); + | ^^^^^^^^^^^^^^^^ +``` + +The lint can be disabled selectively per route by adding an +`#[allow(unmounted_route)]` annotation to a given route declaration. It can also +be disabled globally by adding `#![allow(unmounted_route)]`. You can read more +about this lint in the [codegen +documentation](https://api.rocket.rs/rocket_codegen/index.html). + +### Configuration via Environment Variables + +A new feature that makes deploying Rocket apps to the cloud a little easier is +configuration via environment variables. Simply put, any configuration parameter +can be set via an environment variable of the form `ROCKET_{PARAM}`, where +`{PARAM}` is the name of the configuration parameter. For example, to set the +`port` Rocket listens on, simply set the `ROCKET_PORT` environment variable: + +```sh +ROCKET_PORT=3000 cargo run --release +``` + +Configuration parameters set via environment variables take precedence over +parameters set via the `Rocket.toml` configuration file. Note that _any_ +parameter can be set via an environment variable, include _extras_. For more +about configuration in Rocket, see the [configuration section of the +guide](/guide/overview/#configuration). + +### And Plenty More! + +Rocket v0.2 is full of many new features! In addition to the three features +described above, v0.2 also includes the following: + + * `Config` structures can be built via `ConfigBuilder`, which follows the + builder pattern. + * Logging can be enabled or disabled on custom configuration via a second + parameter to the `Rocket::custom` method. + * `name` and `value` methods were added to `Header` to retrieve the name and + value of a header. + * A new configuration parameter, `workers`, can be used to set the number of + threads Rocket uses. + * The address of the remote connection is available via `Request.remote()`. + Request preprocessing overrides remote IP with value from the `X-Real-IP` + header, if present. + * During testing, the remote address can be set via `MockRequest.remote()`. + * The `SocketAddr` request guard retrieves the remote address. + * A `UUID` type has been added to `contrib`. + * `rocket` and `rocket_codegen` will refuse to build with an incompatible + nightly version and emit nice error messages. + * Major performance and usability improvements were upstreamed to the `cookie` + crate, including the addition of a `CookieBuilder`. + * When a checkbox isn't present in a form, `bool` types in a `FromForm` + structure will parse as `false`. + * The `FormItems` iterator can be queried for a complete parse via `completed` + and `exhausted`. + * Routes for `OPTIONS` requests can be declared via the `options` decorator. + * Strings can be percent-encoded via `URI::percent_encode()`. + +## Breaking Changes + +This release includes several breaking changes. These changes are listed below +along with a short note about how to handle the breaking change in existing +applications. + + * **`Rocket::custom` takes two parameters, the first being `Config` by + value.** + + A call in v0.1 of the form `Rocket::custom(&config)` is now + `Rocket::custom(config, false)`. + + * **Tera templates are named without their extension.** + + A templated named `name.html.tera` is now simply `name`. + + * **`JSON` `unwrap` method has been renamed to `into_inner`.** + + A call to `.unwrap()` should be changed to `.into_inner()`. + + * **The `map!` macro was removed in favor of the `json!` macro.** + + A call of the form `map!{ "a" => b }` can be written as: `json!({ "a": b + })`. + + * **The `hyper::SetCookie` header is no longer exported.** + + Use the `Cookie` type as an `Into

` type directly. + + * **The `Content-Type` for `String` is now `text/plain`.** + + Use `content::HTML` for HTML-based `String` responses. + + * **`Request.content_type()` returns an `Option`.** + + Use `.unwrap_or(ContentType::Any)` to get the old behavior. + + * **The `ContentType` request guard forwards when the request has no + `Content-Type` header.** + + Use an `Option` and `.unwrap_or(ContentType::Any)` for the old + behavior. + + * **A `Rocket` instance must be declared _before_ a `MockRequest`.** + + Change the order of the `rocket::ignite()` and `MockRequest::new()` calls. + + * **A route with `format` specified only matches requests with the same + format.** + + Previously, a route with a `format` would match requests without a format + specified. There is no workaround to this change; simply specify formats + when required. + + * **`FormItems` can no longer be constructed directly.** + + Instead of constructing as `FormItems(string)`, construct as + `FormItems::from(string)`. + + * **`from_from_string(&str)` in `FromForm` removed in favor of + `from_form_items(&mut FormItems)`.** + + Most implementation should be using `FormItems` internally; simply use the + passed in `FormItems`. In other cases, the form string can be retrieved via + the `inner_str` method of `FormItems`. + + * **`Config::{set, default_for}` are deprecated.** + + Use the `set_{param}` methods instead of `set`, and `new` or `build` in + place of `default_for`. + + * **Route paths must be absolute.** + + Prepend a `/` to convert a relative path into an absolute one. + + * **Route paths cannot contain empty segments.** + + Remove any empty segments, including trailing ones, from a route path. + +## Bug Fixes + +Three bugs were fixed in this release: + + * Handlebars partials were not properly registered + ([#122](https://github.com/SergioBenitez/Rocket/issues/122)). + * `Rocket::custom` did not set the custom configuration as the `active` + configuration. + * Route path segments with more than one dynamic parameter were erroneously + allowed. + +## General Improvements + +In addition to new features, Rocket saw the following smaller improvements: + + * Rocket no longer overwrites a catcher's response status. + * The `port` `Config` type is now a proper `u16`. + * Clippy issues injected by codegen are resolved. + * Handlebars was updated to `0.25`. + * The `PartialEq` implementation of `Config` doesn't consider the path or + session key. + * Hyper dependency updated to `0.10`. + * The `Error` type for `JSON as FromData` has been exposed as `SerdeError`. + * SVG was added as a known Content-Type. + * Serde was updated to `0.9`. + * Form parse failure now results in a **422** error code. + * Tera has been updated to `0.7`. + * `pub(crate)` is used throughout to enforce visibility rules. + * Query parameters in routes (`/path?`) are now logged. + * Routes with and without query parameters no longer _collide_. + +Rocket v0.2 also includes all of the new features, bug fixes, and improvements +from versions 0.1.1 through 0.1.6. You can read more about these changes in the +[v0.1 +CHANGELOG](https://github.com/SergioBenitez/Rocket/blob/v0.1/CHANGELOG.md). + +## What's next? + +Work now begins on Rocket v0.3! The focus of the next major release will be on +security. In particular, three major security features are planned: + + 1. **Automatic CSRF protection across all payload-based requests + ([#14](https://github.com/SergioBenitez/Rocket/issues/14)).** + + Rocket will automatically check the origin of requests made for HTTP `PUT`, + `POST`, `DELETE`, and `PATCH` requests, allowing only authorized requests to + be dispatched. This includes checking `POST`s from form submissions and any + requests made via JavaScript. + + 2. **Encryption and signing of session-based cookies + ([#20](https://github.com/SergioBenitez/Rocket/issues/20)).** + + Built-in session support will encrypt and sign cookies using a user supplied + `session_key`. Encryption and signing will occur automatically for + session-based cookies. + + 3. **Explicit typing of raw HTTP data strings + ([#43](https://github.com/SergioBenitez/Rocket/issues/43)).** + + A present, the standard `&str` type is used to represent raw HTTP data + strings. In the next release, a new type, `&RawStr`, will be used for this + purpose. This will make it clear when raw data is being handled. The type + will expose convenient methods such as `.url_decode()` and `.html_escape()`. + +Work on Rocket v0.3 will also involve exploring built-in support for user +authentication and authorization as well as automatic parsing of multipart +forms. + +## Contributors to v0.2 + +The following wonderful people helped make Rocket v0.2 happen: + +
    +
  • Cliff H
  • +
  • Dru Sellers
  • +
  • Eijebong
  • +
  • Eric D. Reichert
  • +
  • Ernestas Poskus
  • +
  • FliegendeWurst
  • +
  • Garrett Squire
  • +
  • Giovanni Capuano
  • +
  • Greg Edwards
  • +
  • Joel Roller
  • +
  • Josh Holmer
  • +
  • Liigo Zhuang
  • +
  • Lori Holden
  • +
  • Marcus Ball
  • +
  • Matt McCoy
  • +
  • Reilly Tucker
  • +
  • Robert Balicki
  • +
  • Sean Griffin
  • +
  • Seth Lopez
  • +
  • tborsa
  • +
+ +Thank you all! Your contributions are greatly appreciated! + +Looking to help with Rocket's development? Head over to [Rocket's +GitHub](https://github.com/SergioBenitez/Rocket#contributing) and start +contributing! + +## Start using Rocket today! + +Not already using Rocket? Rocket is extensively documented, making it easy for +you to start writing your web applications in Rocket! See the +[overview](/overview) or start writing code immediately by reading through [the +guide](/guide). diff --git a/site/overview.toml b/site/overview.toml new file mode 100644 index 00000000..232cd774 --- /dev/null +++ b/site/overview.toml @@ -0,0 +1,231 @@ +############################################################################### +# Panels: displayed in a tabbed arrangement. +############################################################################### + +[[panels]] +name = "Routing" +checked = true +content = ''' +Rocket's main task is to route incoming requests to the appropriate request +handler using your application's declared routes. Routes are declared using +Rocket's _route_ attributes. The attribute describes the requests that match the +route. The attribute is placed on top of a function that is the request handler +for that route. + +As an example, consider the simple route below: + +```rust +#[get("/")] +fn index() -> &'static str { + "Hello, world!" +} +``` + +This route, named `index`, will match against incoming HTTP `GET` requests to +the `/` path, the index. The request handler returns a string. Rocket will use +the string as the body of a fully formed HTTP response. +''' + +[[panels]] +name = "Dynamic Params" +content = ''' +Rocket allows you to interpret segments of a request path dynamically. To +illustrate, let's use the following route: + +```rust +#[get("/hello//")] +fn hello(name: &str, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) +} +``` + +The `hello` route above matches two dynamic path segments declared inside +brackets in the path: `` and ``. _Dynamic_ means that the segment can +be _any_ value the end-user desires. + +Each dynamic parameter (`name` and `age`) must have a type, here `&str` and +`u8`, respectively. Rocket will attempt to parse the string in the parameter's +position in the path into that type. The route will only be called if parsing +succeeds. To parse the string, Rocket uses the +[FromParam](https://api.rocket.rs/rocket/request/trait.FromParam.html) trait, +which you can implement for your own types! +''' + +[[panels]] +name = "Handling Data" +content = ''' +Request body data is handled in a special way in Rocket: via the +[FromData](https://api.rocket.rs/rocket/data/trait.FromData.html) trait. Any +type that implements `FromData` can be derived from incoming body data. To tell +Rocket that you're expecting request body data, the `data` route argument is +used with the name of the parameter in the request handler: + +```rust +#[post("/login", data = "")] +fn login(user_form: Form) -> String { + // Use `user_form`, return a String. +} +``` + +The `login` route above says that it expects `data` of type `Form` in +the `user_form` parameter. The +[Form](https://api.rocket.rs/rocket/request/struct.Form.html) type is a built-in +Rocket type that knows how to parse web forms into structures. Rocket will +automatically attempt to parse the request body into the `Form` and call the +`login` handler if parsing succeeds. Other built-in `FromData` types include +[Data](https://api.rocket.rs/rocket/struct.Data.html), +[JSON](https://api.rocket.rs/rocket_contrib/struct.JSON.html), and +[Flash](https://api.rocket.rs/rocket/response/struct.Flash.html) +''' + +[[panels]] +name = "Request Guards" +content = ''' +In addition to dynamic path and data parameters, request handlers can also +contain a third type of parameter: _request guards_. Request guards aren't +declared in the route attribute, and any number of them can appear in the +request handler signature. + +Request guards _protect_ the handler from running unless some set of conditions +are met by the incoming request metadata. For instance, if you are writing an +API that requires sensitive calls to be accompanied by an API key in the request +header, Rocket can protect those calls via a custom `APIKey` request guard: + +```rust +#[get("/sensitive")] +fn sensitive(key: APIKey) -> &'static str { ... } +``` + +`APIKey` protects the `sensitive` handler from running incorrectly. In order for +Rocket to call the `sensitive` handler, the `APIKey` type needs to be derived +through a +[FromRequest](https://api.rocket.rs/rocket/request/trait.FromRequest.html) +implementation, which in this case, validates the API key header. Request guards +are a powerful and unique Rocket concept; they centralize application policy and +invariants through types. +''' + +[[panels]] +name = "Responders" +content = ''' +The return type of a request handler can by any type that implements +[Responder](https://api.rocket.rs/rocket/response/trait.Responder.html): + +```rust +#[get("/")] +fn route() -> T { ... } +``` + +Above, T must implement `Responder`. Rocket implements `Responder` for many of +the standard library types including `&str`, `String`, `File`, `Option`, and +`Result`. Rocket also implements custom responders such as +[Redirect](https://api.rocket.rs/rocket/response/struct.Redirect.html), +[Flash](https://api.rocket.rs/rocket/response/struct.Flash.html), and +[Template](https://api.rocket.rs/rocket_contrib/struct.Template.html). + +The task of a `Reponder` is to generate a +[Response](https://api.rocket.rs/rocket/response/struct.Response.html), if +possible. `Responder`s can fail with a status code. When they do, Rocket calls +the corresponding `error` route, which can be declared as follows: + +```rust +#[error(404)] +fn not_found() -> T { ... } +``` +''' + +[[panels]] +name = "Launching" +content = ''' +Launching a Rocket application is the funnest part! For Rocket to begin +dispatching requests to routes, the routes need to be _mounted_. After mounting, +the application needs to be _launched_. These two steps, usually done in `main`, +look like: + +```rust +rocket::ignite() + .mount("/base", routes![index, another]) + .launch() +``` + +The `mount` call takes a base path and a set of routes via the `routes!` macro. +The base path (`/base` above) is prepended to the path of every route in the +list. This effectively namespaces the routes, allowing for easier composition. + +The `launch` call starts the server. In development, Rocket prints useful +information to the console to let you know everything is okay. + +```sh +🚀 Rocket has launched from http://localhost:8000... +``` +''' + +############################################################################### +# Steps to "How Rocket Works" +############################################################################### + +[[steps]] +name = "Validation" +color = "blue" +content = ''' +First, Rocket validates a matching request by ensuring that all of the types in +a given handler can be derived from the incoming request. If the types cannot be +derived, the request is forwarded to the next matching route until a route’s +types validate or there are no more routes to try. If all routes fail, a +customizable **404** error is returned. + +```rust +#[post("/user", data = "")] +fn new_user(admin: AdminUser, new_user: Form) -> T { + ... +} +``` + +For the `new_user` handler above to be called, the following conditions must +hold: + +* The request method must be `POST`. +* The request path must be `/user`. +* The request must contain `data` in its body. +* The request metadata must authenticate an `AdminUser`. +* The request body must be a form that parses into a `User` struct. +''' + +[[steps]] +name = "Processing" +color = "purple" +content = ''' +Next, the request is processed by an arbitrary handler. This is where most of +the business logic in an application resides, and the part of your applications +you’ll likely spend the most time writing. In Rocket, handlers are simply +functions - that’s it! The only caveat is that the function’s return type must +implement the `Responder` trait. The `new_user` function above is an example of +a handler. +''' + +[[steps]] +name = "Response" +color = "red" +content = ''' +Finally, Rocket responds to the client by transforming the return value of the +handler into an HTTP response. The HTTP response generated from the returned +value depends on the type’s specific `Responder` trait implementation. + +```rust +fn route() -> T { ... } +``` + +If the function above is used as a handler, for instance, then the type `T` must +implement `Responder`. Rocket provides many useful responder types out of the +box. They include: + + * `JSON`: Serializes the structure T into JSON and returns it to + the client. + * `Template`: Renders a template file and returns it to the client. + * `Redirect`: Returns a properly formatted HTTP redirect. + * `NamedFile`: Streams a given file to the client with the + Content-Type taken from the file’s extension. + * `Stream`: Streams data to the client from an arbitrary `Read` value. + * Many Primitive Types: `String`, `&str`, `File`, `Option`, `Result`, and + others all implement the `Responder` trait. +''' From 90c663682131a6cd7d44e4f0b29e158b5240eb27 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 16 Apr 2017 21:28:36 -0700 Subject: [PATCH 103/297] Remove duplication in site README. --- site/README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/site/README.md b/site/README.md index ea5491b5..b9e99edc 100644 --- a/site/README.md +++ b/site/README.md @@ -10,20 +10,18 @@ This directory contains the following: * `index.toml` - Source data for the index (`/`). * `news.toml` - Source data for the news page (`/news`). * `overview.toml` - Source data for the overview page (`/overview`). - * `guide.md` - Index page for the guide (`/guide`). + * `guide.md` - Index page for the [Rocket Programming Guide] (`/guide`). * `news/*.md` - News articles linked to from `news.toml`. * `guide/*.md` - Guide pages linked to from `guide.md`. -## Guide +[Rocket Programming Guide]: https://rocket.rs/guide/ -The source files for the [Rocket Programming Guide](https://rocket.rs/guide/) -can be found in the `guide/` directory. One exception is the root of the guide, -which is `guide.md`. +### Guide Links Cross-linking to pages in the guide is accomplished via absolute links rooted at -`/guide/`. To link to the page whose source is at `guide/page.md` in this -directory, for instance, link to `/guide/page`. +`/guide/`. To link to the page whose source is at `guide/page.md`, for instance, +link to `/guide/page`. -# License +## License The Rocket website source is licensed under the [GNU General Public License v3.0](LICENSE). From a1c4cc222406552e085dc23de9d382e7843fc04b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 17 Apr 2017 00:34:28 -0700 Subject: [PATCH 104/297] Improve display of table config value. --- lib/src/config/mod.rs | 7 ++++--- lib/src/config/toml_ext.rs | 26 ++++++++++++++++++++++++++ lib/src/rocket.rs | 5 +++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 488d03ba..67244900 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -43,9 +43,9 @@ //! * **session_key**: _[string]_ a 256-bit base64 encoded string (44 //! characters) to use as the session key //! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="` -//! * **tls**: _[dict]_ a dictionary with two keys: 1) `certs`: _[string]_ a -//! path to a certificate chain in PEM format, and 2) `key`: _[string]_ a -//! path to a private key file in PEM format for the certificate in `certs` +//! * **tls**: _[table]_ a table with two keys: 1) `certs`: _[string]_ a path +//! to a certificate chain in PEM format, and 2) `key`: _[string]_ a path to a +//! private key file in PEM format for the certificate in `certs` //! * example: `{ certs = "/path/to/certs.pem", key = "/path/to/key.pem" }` //! //! ### Rocket.toml @@ -196,6 +196,7 @@ pub use self::environment::Environment; pub use self::config::Config; pub use self::builder::ConfigBuilder; pub use self::toml_ext::IntoValue; +pub(crate) use self::toml_ext::LoggedValue; use self::Environment::*; use self::environment::CONFIG_ENV; diff --git a/lib/src/config/toml_ext.rs b/lib/src/config/toml_ext.rs index ac2b7e21..a52e46d9 100644 --- a/lib/src/config/toml_ext.rs +++ b/lib/src/config/toml_ext.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::collections::{HashMap, BTreeMap}; use std::hash::Hash; use std::str::FromStr; @@ -133,6 +134,31 @@ impl_into_value!(Boolean: bool); impl_into_value!(Float: f64); impl_into_value!(Float: f32, as f64); +/// A simple wrapper over a `Value` reference with a custom implementation of +/// `Display`. This is used to log config values at initialization. +pub(crate) struct LoggedValue<'a>(pub &'a Value); + +impl<'a> fmt::Display for LoggedValue<'a> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use config::Value::*; + match *self.0 { + String(_) | Integer(_) | Float(_) | Boolean(_) | Datetime(_) | Array(_) => { + self.0.fmt(f) + } + Table(ref map) => { + write!(f, "{{ ")?; + for (i, (key, val)) in map.iter().enumerate() { + write!(f, "{} = {}", key, LoggedValue(val))?; + if i != map.len() - 1 { write!(f, ", ")?; } + } + + write!(f, " }}") + } + } + } +} + #[cfg(test)] mod test { use std::collections::BTreeMap; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 9170e9e0..5874e638 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -11,7 +11,7 @@ use state::Container; #[cfg(feature = "tls")] use hyper_rustls::TlsServer; use {logger, handler}; use ext::ReadExt; -use config::{self, Config}; +use config::{self, Config, LoggedValue}; use request::{Request, FormItems}; use data::Data; use response::{Body, Response}; @@ -396,7 +396,8 @@ impl Rocket { } for (name, value) in config.extras() { - info_!("{} {}: {}", Yellow.paint("[extra]"), name, White.paint(value)); + info_!("{} {}: {}", + Yellow.paint("[extra]"), name, White.paint(LoggedValue(value))); } Rocket { From 3c51d30e66647a313c09d226c9a4a8f1cd826a90 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 17 Apr 2017 15:54:44 -0700 Subject: [PATCH 105/297] Avoid collision in FromForm derive by using weird names. Fixes #265. --- codegen/src/decorators/derive_form.rs | 20 ++++++++++---------- codegen/tests/run-pass/derive_form.rs | 11 +++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index b8945689..107d22ee 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -70,11 +70,11 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem, is_unsafe: false, supports_unions: false, span: span, - // We add this attribute because some `FromFormValue` implementations + // We add these attribute because some `FromFormValue` implementations // can't fail. This is indicated via the `!` type. Rust checks if a // match is made with something of that type, and since we always emit // an `Err` match, we'll get this lint warning. - attributes: vec![quote_attr!(ecx, #[allow(unreachable_code)])], + attributes: vec![quote_attr!(ecx, #[allow(unreachable_code, unreachable_patterns)])], path: ty::Path { path: vec!["rocket", "request", "FromForm"], lifetime: lifetime_var, @@ -241,12 +241,12 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct for &(ref ident, _, ref name) in &fields_info { arms.push(quote_tokens!(cx, $name => { - let r = ::rocket::http::RawStr::from_str(v); - $ident = match ::rocket::request::FromFormValue::from_form_value(r) { - Ok(v) => Some(v), - Err(e) => { + let __r = ::rocket::http::RawStr::from_str(__v); + $ident = match ::rocket::request::FromFormValue::from_form_value(__r) { + Ok(__v) => Some(__v), + Err(__e) => { println!(" => Error parsing form val '{}': {:?}", - $name, e); + $name, __e); $return_err_stmt } }; @@ -257,8 +257,8 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct // The actual match statement. Iterate through all of the fields in the form // and use the $arms generated above. stmts.push(quote_stmt!(cx, - for (k, v) in $arg { - match k.as_str() { + for (__k, __v) in $arg { + match __k.as_str() { $arms "_method" => { /* This is a Rocket-specific field. If the user hasn't asked @@ -267,7 +267,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct } _ => { println!(" => {}={} has no matching field in struct.", - k, v); + __k, __v); $return_err_stmt } }; diff --git a/codegen/tests/run-pass/derive_form.rs b/codegen/tests/run-pass/derive_form.rs index 08c2756b..522f0935 100644 --- a/codegen/tests/run-pass/derive_form.rs +++ b/codegen/tests/run-pass/derive_form.rs @@ -65,6 +65,11 @@ struct UnpresentCheckboxTwo<'r> { something: &'r RawStr } +#[derive(Debug, PartialEq, FromForm)] +struct FieldNamedV<'r> { + v: &'r RawStr, +} + fn parse<'f, T: FromForm<'f>>(string: &'f str) -> Option { let mut items = FormItems::from(string); let result = T::from_form_items(items.by_ref()); @@ -141,4 +146,10 @@ fn main() { checkbox: false, something: "hello".into() })); + + // Check that a structure with one field `v` parses correctly. + let manual: Option = parse("v=abc"); + assert_eq!(manual, Some(FieldNamedV { + v: "abc".into() + })); } From e6bbeacb1c90be5f91c79fe503b42e5ea74af21b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 17 Apr 2017 16:21:56 -0700 Subject: [PATCH 106/297] New version: 0.2.6. --- CHANGELOG.md | 9 +++++++++ codegen/Cargo.toml | 4 ++-- contrib/Cargo.toml | 4 ++-- lib/Cargo.toml | 4 ++-- site/guide/conclusion.md | 4 ++-- site/guide/getting-started.md | 4 ++-- site/guide/overview.md | 4 ++-- site/guide/pastebin.md | 6 +++--- site/guide/requests.md | 16 ++++++++-------- site/guide/responses.md | 4 ++-- site/guide/state.md | 2 +- site/guide/testing.md | 4 ++-- site/index.toml | 4 ++-- 13 files changed, 39 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a9dd38..c6d3ae1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# Version 0.2.6 (Apr 17, 2017) + +## Codegen + + * Allow `k` and `v` to be used as fields in `FromForm` structures by avoiding + identifier collisions ([#265]). + +[#265]: https://github.com/SergioBenitez/Rocket/issues/265 + # Version 0.2.5 (Apr 16, 2017) ## Codegen diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 93b2d2eb..d01287dd 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.2.5" +version = "0.2.6" authors = ["Sergio Benitez "] description = "Code generation for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_codegen/" @@ -15,7 +15,7 @@ build = "build.rs" plugin = true [dependencies] -rocket = { version = "0.2.5", path = "../lib/" } +rocket = { version = "0.2.6", path = "../lib/" } log = "^0.3" [dev-dependencies] diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 4ac2bed3..73cafa1c 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_contrib" -version = "0.2.5" +version = "0.2.6" authors = ["Sergio Benitez "] description = "Community contributed libraries for the Rocket web framework." documentation = "https://api.rocket.rs/rocket_contrib/" @@ -22,7 +22,7 @@ templates = ["serde", "serde_json", "lazy_static_macro", "glob"] lazy_static_macro = ["lazy_static"] [dependencies] -rocket = { version = "0.2.5", path = "../lib/" } +rocket = { version = "0.2.6", path = "../lib/" } log = "^0.3" # UUID dependencies. diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f32f38d5..90b654a3 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.2.5" +version = "0.2.6" authors = ["Sergio Benitez "] description = """ Web framework for nightly with a focus on ease-of-use, expressibility, and speed. @@ -43,7 +43,7 @@ optional = true [dev-dependencies] lazy_static = "0.2" -rocket_codegen = { version = "0.2.5", path = "../codegen" } +rocket_codegen = { version = "0.2.6", path = "../codegen" } [build-dependencies] ansi_term = "0.9" diff --git a/site/guide/conclusion.md b/site/guide/conclusion.md index d7d337a4..1dbfc3d3 100644 --- a/site/guide/conclusion.md +++ b/site/guide/conclusion.md @@ -22,7 +22,7 @@ guide. The best way to learn Rocket is to _build something_. It should be fun and easy, and there's always someone to help. Alternatively, you can read through the -[Rocket examples](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples) +[Rocket examples](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples) or the [Rocket source -code](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/lib/src). Whatever you +code](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/lib/src). Whatever you decide to do next, we hope you have a blast! diff --git a/site/guide/getting-started.md b/site/guide/getting-started.md index 9b601608..5e4ceda4 100644 --- a/site/guide/getting-started.md +++ b/site/guide/getting-started.md @@ -53,8 +53,8 @@ project by ensuring your `Cargo.toml` contains the following: ``` [dependencies] -rocket = "0.2.5" -rocket_codegen = "0.2.5" +rocket = "0.2.6" +rocket_codegen = "0.2.6" ``` Modify `src/main.rs` so that it contains the code for the Rocket `Hello, world!` diff --git a/site/guide/overview.md b/site/guide/overview.md index 53a7ef0c..e109ad6c 100644 --- a/site/guide/overview.md +++ b/site/guide/overview.md @@ -172,10 +172,10 @@ we expected. A version of this example's complete crate, ready to `cargo run`, can be found on -[GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/hello_world). +[GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/hello_world). You can find dozens of other complete examples, spanning all of Rocket's features, in the [GitHub examples -directory](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/). +directory](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/). ## Configuration diff --git a/site/guide/pastebin.md b/site/guide/pastebin.md index adf440f3..2935ecee 100644 --- a/site/guide/pastebin.md +++ b/site/guide/pastebin.md @@ -43,8 +43,8 @@ Then add the usual Rocket dependencies to the `Cargo.toml` file: ```toml [dependencies] -rocket = "0.2.5" -rocket_codegen = "0.2.5" +rocket = "0.2.6" +rocket_codegen = "0.2.6" ``` And finally, create a skeleton Rocket application to work off of in @@ -402,4 +402,4 @@ through some of them to get a better feel for Rocket. Here are some ideas: You can find the full source code for the completed pastebin tutorial in the [Rocket Github -Repo](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/pastebin). +Repo](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/pastebin). diff --git a/site/guide/requests.md b/site/guide/requests.md index 4225f0dd..57b5dec1 100644 --- a/site/guide/requests.md +++ b/site/guide/requests.md @@ -31,7 +31,7 @@ requests under certain conditions. If a `POST` request contains a body of field has the name `_method` and a valid HTTP method as its value, that field's value is used as the method for the incoming request. This allows Rocket applications to submit non-`POST` forms. The [todo -example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/todo/static/index.html.tera#L47) +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/todo/static/index.html.tera#L47) makes use of this feature to submit `PUT` and `DELETE` requests from a web form. ## Format @@ -185,7 +185,7 @@ fn index(cookies: &Cookies, content: ContentType) -> String { ... } ``` The [cookies example on -GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/cookies) +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/cookies) illustrates how to use the `Cookies` type to get and set cookies. You can implement `FromRequest` for your own types. For instance, to protect a @@ -259,9 +259,9 @@ validates integers over that age. If a form is a submitted with a bad age, Rocket won't call a handler requiring a valid form for that structure. You can use `Option` or `Result` types for fields to catch parse failures. -The [forms](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/forms) +The [forms](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/forms) and [forms kitchen -sink](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/form_kitchen_sink) +sink](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/form_kitchen_sink) examples on GitHub provide further illustrations. ### JSON @@ -282,7 +282,7 @@ fn new(task: JSON) -> String { ... } The only condition is that the generic type to `JSON` implements the `Deserialize` trait. See the [JSON example on -GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/json) for a +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/json) for a complete example. ### Streaming @@ -305,7 +305,7 @@ The route above accepts any `POST` request to the `/upload` path with text response if the upload succeeds. If the upload fails, an error response is returned. The handler above is complete. It really is that simple! See the [GitHub example -code](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/raw_upload) +code](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/raw_upload) for the full crate. ## Query Strings @@ -341,7 +341,7 @@ the request is forwarded to the next matching route. To catch parse failures, you can use `Option` or `Result` as the type of the field to catch errors for. See [the GitHub -example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/query_params) +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/query_params) for a complete illustration. ## Error Catchers @@ -371,7 +371,7 @@ types [Request](https://api.rocket.rs/rocket/struct.Request.html) and/or [Error](https://api.rocket.rs/rocket/enum.Error.html). At present, the `Error` type is not particularly useful, and so it is often omitted. The [error catcher -example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/errors) on +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/errors) on GitHub illustrates their use in full. Rocket has a default catcher for all of the standard HTTP error codes including diff --git a/site/guide/responses.md b/site/guide/responses.md index 6a9fb3f0..8a46cb9e 100644 --- a/site/guide/responses.md +++ b/site/guide/responses.md @@ -118,7 +118,7 @@ serialization in a fixed-sized body. If serialization fails, the request is forwarded to the **500** error catcher. For a complete example, see the [JSON example on -GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/json). +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/json). ## Templates @@ -147,7 +147,7 @@ The context can be any type that implements `Serialize` and serializes to an [Template](https://api.rocket.rs/rocket_contrib/struct.Template.html) API documentation contains more information about templates, while the [Handlebars Templates example on -GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/handlebars_templates) +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/handlebars_templates) is a fully composed application that makes use of Handlebars templates. ## Streaming diff --git a/site/guide/state.md b/site/guide/state.md index ede89ee0..fff7fcef 100644 --- a/site/guide/state.md +++ b/site/guide/state.md @@ -138,7 +138,7 @@ attributes. You can find a complete example using the `HitCounter` structure in the [state example on -GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/state) and +GitHub](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/state) and learn more about the [manage method](https://api.rocket.rs/rocket/struct.Rocket.html#method.manage) and [State type](https://api.rocket.rs/rocket/struct.State.html) in the API docs. diff --git a/site/guide/testing.md b/site/guide/testing.md index 6144d5dc..73641201 100644 --- a/site/guide/testing.md +++ b/site/guide/testing.md @@ -40,7 +40,7 @@ follows: ```toml [dev-dependencies] -rocket = { version = "0.2.5", features = ["testing"] } +rocket = { version = "0.2.6", features = ["testing"] } ``` With this in place, running `cargo test` will result in Cargo compiling Rocket @@ -107,7 +107,7 @@ assert_eq!(body_str, Some("Hello, world!".to_string())); That's it! Run the tests with `cargo test`. The complete application, with testing, can be found in the [GitHub testing -example](https://github.com/SergioBenitez/Rocket/tree/v0.2.5/examples/testing). +example](https://github.com/SergioBenitez/Rocket/tree/v0.2.6/examples/testing). ## Codegen Debug diff --git a/site/index.toml b/site/index.toml index 2e616274..b9630003 100644 --- a/site/index.toml +++ b/site/index.toml @@ -4,8 +4,8 @@ [release] url = "https://crates.io/crates/rocket" -version = "0.2.5" -date = "Apr 16, 2017" +version = "0.2.6" +date = "Apr 17, 2017" [[top_features]] title = "Type Safe" From 6dc21e5380ed61b5691a258af8c33312729598c8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 00:25:13 -0700 Subject: [PATCH 107/297] Add support for configurable size limits. --- contrib/src/json.rs | 12 ++- contrib/src/msgpack.rs | 12 ++- examples/config/Rocket.toml | 7 +- lib/src/config/builder.rs | 30 +++++- lib/src/config/config.rs | 100 ++++------------- lib/src/config/custom_values.rs | 165 +++++++++++++++++++++++++++++ lib/src/config/mod.rs | 2 + lib/src/request/form/form_items.rs | 1 + lib/src/request/form/mod.rs | 7 +- lib/src/rocket.rs | 1 + lib/tests/limits.rs | 83 +++++++++++++++ 11 files changed, 323 insertions(+), 97 deletions(-) create mode 100644 lib/src/config/custom_values.rs create mode 100644 lib/tests/limits.rs diff --git a/contrib/src/json.rs b/contrib/src/json.rs index f8ab601b..b31ba43b 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use std::io::Read; +use rocket::config; use rocket::outcome::Outcome; use rocket::request::Request; use rocket::data::{self, Data, FromData}; @@ -65,9 +66,8 @@ impl JSON { } } -/// Maximum size of JSON is 1MB. -/// TODO: Determine this size from some configuration parameter. -const MAX_SIZE: u64 = 1048576; +/// Default limit for JSON is 1MB. +const LIMIT: u64 = 1 << 20; impl FromData for JSON { type Error = SerdeError; @@ -78,7 +78,11 @@ impl FromData for JSON { return Outcome::Forward(data); } - let reader = data.open().take(MAX_SIZE); + let size_limit = config::active() + .and_then(|c| c.limits.get("json")) + .unwrap_or(LIMIT); + + let reader = data.open().take(size_limit); match serde_json::from_reader(reader).map(|val| JSON(val)) { Ok(value) => Outcome::Success(value), Err(e) => { diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index f33ede0c..05927c96 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -3,6 +3,7 @@ extern crate rmp_serde; use std::ops::{Deref, DerefMut}; use std::io::{Cursor, Read}; +use rocket::config; use rocket::outcome::Outcome; use rocket::request::Request; use rocket::data::{self, Data, FromData}; @@ -70,9 +71,8 @@ impl MsgPack { } } -/// Maximum size of MessagePack data is 1MB. -/// TODO: Determine this size from some configuration parameter. -const MAX_SIZE: u64 = 1048576; +/// Default limit for MessagePack is 1MB. +const LIMIT: u64 = 1 << 20; /// Accepted content types are: `application/msgpack`, `application/x-msgpack`, /// `bin/msgpack`, and `bin/x-msgpack`. @@ -91,8 +91,12 @@ impl FromData for MsgPack { return Outcome::Forward(data); } + let size_limit = config::active() + .and_then(|c| c.limits.get("msgpack")) + .unwrap_or(LIMIT); + let mut buf = Vec::new(); - if let Err(e) = data.open().take(MAX_SIZE).read_to_end(&mut buf) { + if let Err(e) = data.open().take(size_limit).read_to_end(&mut buf) { let e = MsgPackError::InvalidDataRead(e); error_!("Couldn't read request data: {:?}", e); return Outcome::Failure((Status::BadRequest, e)); diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index 2ae9bd0f..64ebdfc2 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -1,6 +1,11 @@ -# Except for the session key, none of these are actually needed; Rocket has sane +# Except for the session key, nothing here is necessary; Rocket has sane # defaults. We show all of them here explicitly for demonstrative purposes. +[global.limits] +forms = 32768 +json = 1048576 # this is an extra used by the json contrib module +msgpack = 1048576 # this is an extra used by the msgpack contrib module + [development] address = "localhost" port = 8000 diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index 251ef98b..5916a9d1 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use config::{Result, Config, Value, Environment}; +use config::{Result, Config, Value, Environment, Limits}; use config::toml_ext::IntoValue; use logger::LoggingLevel; @@ -21,7 +21,9 @@ pub struct ConfigBuilder { /// The session key. pub session_key: Option, /// TLS configuration (path to certificates file, path to private key file). - pub tls_config: Option<(String, String)>, + pub tls: Option<(String, String)>, + /// Size limits. + pub limits: Limits, /// Any extra parameters that aren't part of Rocket's config. pub extras: HashMap, /// The root directory of this config. @@ -65,7 +67,8 @@ impl ConfigBuilder { workers: config.workers, log_level: config.log_level, session_key: None, - tls_config: None, + tls: None, + limits: config.limits, extras: config.extras, root: root_dir, } @@ -165,6 +168,22 @@ impl ConfigBuilder { self } + /// Sets the `limits` in the configuration being built. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::{Config, Environment, Limits}; + /// + /// let mut config = Config::build(Environment::Staging) + /// .limits(Limits::default().add("json", 5 * (1 << 20))) + /// .unwrap(); + /// ``` + pub fn limits(mut self, limits: Limits) -> Self { + self.limits = limits; + self + } + /// Sets the `tls_config` in the configuration being built. /// /// # Example @@ -181,7 +200,7 @@ impl ConfigBuilder { pub fn tls(mut self, certs_path: C, key_path: K) -> Self where C: Into, K: Into { - self.tls_config = Some((certs_path.into(), key_path.into())); + self.tls = Some((certs_path.into(), key_path.into())); self } @@ -282,8 +301,9 @@ impl ConfigBuilder { config.set_log_level(self.log_level); config.set_extras(self.extras); config.set_root(self.root); + config.set_limits(self.limits); - if let Some((certs_path, key_path)) = self.tls_config { + if let Some((certs_path, key_path)) = self.tls { config.set_tls(&certs_path, &key_path)?; } diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 8f6b3d51..e51fb90d 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -5,45 +5,13 @@ use std::convert::AsRef; use std::fmt; use std::env; -#[cfg(feature = "tls")] use rustls::{Certificate, PrivateKey}; - +use super::custom_values::*; use {num_cpus, base64}; use config::Environment::*; use config::{Result, Table, Value, ConfigBuilder, Environment, ConfigError}; use logger::LoggingLevel; use http::Key; -pub enum SessionKey { - Generated(Key), - Provided(Key) -} - -impl SessionKey { - #[inline(always)] - pub fn kind(&self) -> &'static str { - match *self { - SessionKey::Generated(_) => "generated", - SessionKey::Provided(_) => "provided", - } - } - - #[inline(always)] - fn inner(&self) -> &Key { - match *self { - SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key - } - } -} - -#[cfg(feature = "tls")] -pub struct TlsConfig { - pub certs: Vec, - pub key: PrivateKey -} - -#[cfg(not(feature = "tls"))] -pub struct TlsConfig; - /// Structure for Rocket application configuration. /// /// A `Config` structure is typically built using the [build](#method.build) @@ -75,6 +43,8 @@ pub struct Config { pub(crate) session_key: SessionKey, /// TLS configuration. pub(crate) tls: Option, + /// Streaming read size limits. + pub limits: Limits, /// Extra parameters that aren't part of Rocket's core config. pub extras: HashMap, /// The path to the configuration file this config belongs to. @@ -94,52 +64,6 @@ macro_rules! config_from_raw { ) } -#[inline(always)] -fn value_as_str<'a>(config: &Config, name: &str, value: &'a Value) -> Result<&'a str> { - value.as_str().ok_or(config.bad_type(name, value.type_str(), "a string")) -} - -#[inline(always)] -fn value_as_u16(config: &Config, name: &str, value: &Value) -> Result { - match value.as_integer() { - Some(x) if x >= 0 && x <= (u16::max_value() as i64) => Ok(x as u16), - _ => Err(config.bad_type(name, value.type_str(), "a 16-bit unsigned integer")) - } -} - -#[inline(always)] -fn value_as_log_level(config: &Config, name: &str, value: &Value) -> Result { - value_as_str(config, name, value) - .and_then(|s| s.parse().map_err(|e| config.bad_type(name, value.type_str(), e))) -} - -#[inline(always)] -fn value_as_tls_config<'v>(config: &Config, - name: &str, - value: &'v Value, - ) -> Result<(&'v str, &'v str)> -{ - let (mut certs_path, mut key_path) = (None, None); - let table = value.as_table() - .ok_or_else(|| config.bad_type(name, value.type_str(), "a table"))?; - - let env = config.environment; - for (key, value) in table { - match key.as_str() { - "certs" => certs_path = Some(value_as_str(config, "tls.certs", value)?), - "key" => key_path = Some(value_as_str(config, "tls.key", value)?), - _ => return Err(ConfigError::UnknownKey(format!("{}.tls.{}", env, key))) - } - } - - if let (Some(certs), Some(key)) = (certs_path, key_path) { - Ok((certs, key)) - } else { - Err(config.bad_type(name, "a table with missing entries", - "a table with `certs` and `key` entries")) - } -} - impl Config { /// Returns a builder for `Config` structure where the default parameters /// are set to those of `env`. The root configuration directory is set to @@ -219,6 +143,7 @@ impl Config { log_level: LoggingLevel::Normal, session_key: key, tls: None, + limits: Limits::default(), extras: HashMap::new(), config_path: config_path, } @@ -232,6 +157,7 @@ impl Config { log_level: LoggingLevel::Normal, session_key: key, tls: None, + limits: Limits::default(), extras: HashMap::new(), config_path: config_path, } @@ -245,6 +171,7 @@ impl Config { log_level: LoggingLevel::Critical, session_key: key, tls: None, + limits: Limits::default(), extras: HashMap::new(), config_path: config_path, } @@ -255,8 +182,10 @@ impl Config { /// Constructs a `BadType` error given the entry `name`, the invalid `val` /// at that entry, and the `expect`ed type name. #[inline(always)] - fn bad_type(&self, name: &str, actual: &'static str, expect: &'static str) - -> ConfigError { + pub(crate) fn bad_type(&self, + name: &str, + actual: &'static str, + expect: &'static str) -> ConfigError { let id = format!("{}.{}", self.environment, name); ConfigError::BadType(id, expect, actual, self.config_path.clone()) } @@ -284,7 +213,8 @@ impl Config { workers => (u16, set_workers, ok), session_key => (str, set_session_key, id), log => (log_level, set_log_level, ok), - tls => (tls_config, set_raw_tls, id) + tls => (tls_config, set_raw_tls, id), + limits => (limits, set_limits, ok) | _ => { self.extras.insert(name.into(), val.clone()); Ok(()) @@ -442,6 +372,12 @@ impl Config { self.log_level = log_level; } + /// Sets limits. + #[inline] + pub fn set_limits(&mut self, limits: Limits) { + self.limits = limits; + } + #[cfg(feature = "tls")] pub fn set_tls(&mut self, certs_path: &str, key_path: &str) -> Result<()> { use hyper_rustls::util as tls; diff --git a/lib/src/config/custom_values.rs b/lib/src/config/custom_values.rs new file mode 100644 index 00000000..948bdacf --- /dev/null +++ b/lib/src/config/custom_values.rs @@ -0,0 +1,165 @@ +use std::fmt; + +#[cfg(feature = "tls")] use rustls::{Certificate, PrivateKey}; + +use logger::LoggingLevel; +use config::{Result, Config, Value, ConfigError}; +use http::Key; + +pub enum SessionKey { + Generated(Key), + Provided(Key) +} + +impl SessionKey { + #[inline(always)] + pub fn kind(&self) -> &'static str { + match *self { + SessionKey::Generated(_) => "generated", + SessionKey::Provided(_) => "provided", + } + } + + #[inline(always)] + pub(crate) fn inner(&self) -> &Key { + match *self { + SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key + } + } +} + +#[cfg(feature = "tls")] +pub struct TlsConfig { + pub certs: Vec, + pub key: PrivateKey +} + +#[cfg(not(feature = "tls"))] +pub struct TlsConfig; + +// Size limit configuration. We cache those used by Rocket internally but don't +// share that fact in the API. +#[derive(Debug, Clone)] +pub struct Limits { + pub(crate) forms: u64, + extra: Vec<(String, u64)> +} + +impl Default for Limits { + fn default() -> Limits { + Limits { forms: 1024 * 32, extra: Vec::new() } + } +} + +impl Limits { + pub fn add>(mut self, name: S, limit: u64) -> Self { + let name = name.into(); + match name.as_str() { + "forms" => self.forms = limit, + _ => self.extra.push((name, limit)) + } + + self + } + + pub fn get(&self, name: &str) -> Option { + if name == "forms" { + return Some(self.forms); + } + + for &(ref key, val) in &self.extra { + if key == name { + return Some(val); + } + } + + None + } +} + +impl fmt::Display for Limits { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt_size(n: u64, f: &mut fmt::Formatter) -> fmt::Result { + if (n & ((1 << 20) - 1)) == 0 { + write!(f, "{}MiB", n >> 20) + } else if (n & ((1 << 10) - 1)) == 0 { + write!(f, "{}KiB", n >> 10) + } else { + write!(f, "{}B", n) + } + } + + write!(f, "forms = ")?; + fmt_size(self.forms, f)?; + for &(ref key, val) in &self.extra { + write!(f, ", {}* = ", key)?; + fmt_size(val, f)?; + } + + Ok(()) + } +} + +pub fn value_as_str<'a>(conf: &Config, name: &str, v: &'a Value) -> Result<&'a str> { + v.as_str().ok_or(conf.bad_type(name, v.type_str(), "a string")) +} + +pub fn value_as_u64(conf: &Config, name: &str, value: &Value) -> Result { + match value.as_integer() { + Some(x) if x >= 0 => Ok(x as u64), + _ => Err(conf.bad_type(name, value.type_str(), "an unsigned integer")) + } +} + +pub fn value_as_u16(conf: &Config, name: &str, value: &Value) -> Result { + match value.as_integer() { + Some(x) if x >= 0 && x <= (u16::max_value() as i64) => Ok(x as u16), + _ => Err(conf.bad_type(name, value.type_str(), "a 16-bit unsigned integer")) + } +} + +pub fn value_as_log_level(conf: &Config, + name: &str, + value: &Value + ) -> Result { + value_as_str(conf, name, value) + .and_then(|s| s.parse().map_err(|e| conf.bad_type(name, value.type_str(), e))) +} + +pub fn value_as_tls_config<'v>(conf: &Config, + name: &str, + value: &'v Value, + ) -> Result<(&'v str, &'v str)> { + let (mut certs_path, mut key_path) = (None, None); + let table = value.as_table() + .ok_or_else(|| conf.bad_type(name, value.type_str(), "a table"))?; + + let env = conf.environment; + for (key, value) in table { + match key.as_str() { + "certs" => certs_path = Some(value_as_str(conf, "tls.certs", value)?), + "key" => key_path = Some(value_as_str(conf, "tls.key", value)?), + _ => return Err(ConfigError::UnknownKey(format!("{}.tls.{}", env, key))) + } + } + + if let (Some(certs), Some(key)) = (certs_path, key_path) { + Ok((certs, key)) + } else { + Err(conf.bad_type(name, "a table with missing entries", + "a table with `certs` and `key` entries")) + } +} + +pub fn value_as_limits(conf: &Config, name: &str, value: &Value) -> Result { + let table = value.as_table() + .ok_or_else(|| conf.bad_type(name, value.type_str(), "a table"))?; + + let mut limits = Limits::default(); + for (key, val) in table { + let val = value_as_u64(conf, &format!("limits.{}", key), val)?; + limits = limits.add(key.as_str(), val); + } + + Ok(limits) +} diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 67244900..e03798a2 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -179,6 +179,7 @@ mod environment; mod config; mod builder; mod toml_ext; +mod custom_values; use std::sync::{Once, ONCE_INIT}; use std::fs::{self, File}; @@ -190,6 +191,7 @@ use std::env; use toml; +pub use self::custom_values::Limits; pub use toml::{Array, Table, Value}; pub use self::error::{ConfigError, ParsingError}; pub use self::environment::Environment; diff --git a/lib/src/request/form/form_items.rs b/lib/src/request/form/form_items.rs index ab26d0e4..8a0744af 100644 --- a/lib/src/request/form/form_items.rs +++ b/lib/src/request/form/form_items.rs @@ -277,6 +277,7 @@ mod test { &[("user", ""), ("password", "pass")]); check_form!("a=b", &[("a", "b")]); + check_form!("value=Hello+World", &[("value", "Hello+World")]); check_form!("user=", &[("user", "")]); check_form!("user=&", &[("user", "")]); diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs index 16c43a64..0fd9da52 100644 --- a/lib/src/request/form/mod.rs +++ b/lib/src/request/form/mod.rs @@ -28,6 +28,7 @@ use std::marker::PhantomData; use std::fmt::{self, Debug}; use std::io::Read; +use config; use http::Status; use request::Request; use data::{self, Data, FromData}; @@ -255,6 +256,9 @@ impl<'f, T: FromForm<'f> + Debug + 'f> Debug for Form<'f, T> { } } +/// Default limit for forms is 32KiB. +const LIMIT: u64 = 32 * (1 << 10); + /// Parses a `Form` from incoming form data. /// /// If the content type of the request data is not @@ -279,7 +283,8 @@ impl<'f, T: FromForm<'f>> FromData for Form<'f, T> where T::Error: Debug { } let mut form_string = String::with_capacity(4096); - let mut stream = data.open().take(32768); // TODO: Make this configurable? + let limit = config::active().map(|c| c.limits.forms).unwrap_or(LIMIT); + let mut stream = data.open().take(limit); if let Err(e) = stream.read_to_string(&mut form_string) { error_!("IO Error: {:?}", e); Failure((Status::InternalServerError, None)) diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 5874e638..fefc5e79 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -382,6 +382,7 @@ impl Rocket { info_!("log: {}", White.paint(config.log_level)); info_!("workers: {}", White.paint(config.workers)); info_!("session key: {}", White.paint(config.session_key.kind())); + info_!("limits: {}", White.paint(&config.limits)); let tls_configured = config.tls.is_some(); if tls_configured && cfg!(feature = "tls") { diff --git a/lib/tests/limits.rs b/lib/tests/limits.rs new file mode 100644 index 00000000..9070c42f --- /dev/null +++ b/lib/tests/limits.rs @@ -0,0 +1,83 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use rocket::request::Form; + +#[derive(FromForm)] +struct Simple { + value: String +} + +#[post("/", data = "")] +fn index(form: Form) -> String { + form.into_inner().value +} + +#[cfg(feature = "testing")] +mod tests { + use rocket; + use rocket::config::{Environment, Config, Limits}; + use rocket::testing::MockRequest; + use rocket::http::Method::*; + use rocket::http::{Status, ContentType}; + + fn rocket_with_forms_limit(limit: u64) -> rocket::Rocket { + let config = Config::build(Environment::Development) + .limits(Limits::default().add("forms", limit)) + .unwrap(); + + rocket::custom(config, true).mount("/", routes![super::index]) + } + + // FIXME: Config is global (it's the only global thing). Each of these tests + // will run in different threads in the same process, so the config used by + // all of the tests will be indentical: whichever of these gets executed + // first. As such, only one test will pass; the rest will fail. Make config + // _not_ global so we can actually do these tests. + + // #[test] + // fn large_enough() { + // let rocket = rocket_with_forms_limit(128); + // let mut req = MockRequest::new(Post, "/") + // .body("value=Hello+world") + // .header(ContentType::Form); + + // let mut response = req.dispatch_with(&rocket); + // assert_eq!(response.body_string(), Some("Hello world".into())); + // } + + // #[test] + // fn just_large_enough() { + // let rocket = rocket_with_forms_limit(17); + // let mut req = MockRequest::new(Post, "/") + // .body("value=Hello+world") + // .header(ContentType::Form); + + // let mut response = req.dispatch_with(&rocket); + // assert_eq!(response.body_string(), Some("Hello world".into())); + // } + + // #[test] + // fn much_too_small() { + // let rocket = rocket_with_forms_limit(4); + // let mut req = MockRequest::new(Post, "/") + // .body("value=Hello+world") + // .header(ContentType::Form); + + // let response = req.dispatch_with(&rocket); + // assert_eq!(response.status(), Status::BadRequest); + // } + + #[test] + fn contracted() { + let rocket = rocket_with_forms_limit(10); + let mut req = MockRequest::new(Post, "/") + .body("value=Hello+world") + .header(ContentType::Form); + + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.body_string(), Some("Hell".into())); + } +} From 1524b9a6b2636d150d5c59da61e7566b62bd1317 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 00:36:39 -0700 Subject: [PATCH 108/297] Document size limits. --- contrib/src/json.rs | 18 ++++++++++++++++++ contrib/src/msgpack.rs | 18 ++++++++++++++++++ lib/src/config/mod.rs | 11 +++++++++-- lib/src/request/form/mod.rs | 26 ++++++++++++++++++++------ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/contrib/src/json.rs b/contrib/src/json.rs index b31ba43b..437c3022 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -18,6 +18,8 @@ pub use serde_json::error::Error as SerdeError; /// The JSON type: implements `FromData` and `Responder`, allowing you to easily /// consume and respond with JSON. /// +/// ## Receiving JSON +/// /// If you're receiving JSON data, simply add a `data` parameter to your route /// arguments and ensure the type of the parameter is a `JSON`, where `T` is /// some type you'd like to parse from JSON. `T` must implement `Deserialize` @@ -36,6 +38,8 @@ pub use serde_json::error::Error as SerdeError; /// doesn't specify "application/json" as its `Content-Type` header value will /// not be routed to the handler. /// +/// ## Sending JSON +/// /// If you're responding with JSON data, return a `JSON` type, where `T` /// implements `Serialize` from [Serde](https://github.com/serde-rs/json). The /// content type of the response is set to `application/json` automatically. @@ -48,6 +52,20 @@ pub use serde_json::error::Error as SerdeError; /// JSON(user_from_id) /// } /// ``` +/// +/// ## Incoming Data Limits +/// +/// The default size limit for incoming JSON data is 1MiB. Setting a limit +/// protects your application from denial of service (DOS) attacks and from +/// resource exhaustion through high memory consumption. The limit can be +/// increased by setting the `limits.json` configuration parameter. For +/// instance, to increase the JSON limit to 5MiB for all environments, you may +/// add the following to your `Rocket.toml`: +/// +/// ```toml +/// [global.limits] +/// json = 5242880 +/// ``` #[derive(Debug)] pub struct JSON(pub T); diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index 05927c96..ce417bcc 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -17,6 +17,8 @@ pub use self::rmp_serde::decode::Error as MsgPackError; /// The `MsgPack` type: implements `FromData` and `Responder`, allowing you to /// easily consume and respond with MessagePack data. /// +/// ## Receiving MessagePack +/// /// If you're receiving MessagePack data, simply add a `data` parameter to your /// route arguments and ensure the type of the parameter is a `MsgPack`, /// where `T` is some type you'd like to parse from MessagePack. `T` must @@ -38,6 +40,8 @@ pub use self::rmp_serde::decode::Error as MsgPackError; /// `application/msgpack`, `application/x-msgpack`, `bin/msgpack`, or /// `bin/x-msgpack`. /// +/// ## Sending MessagePack +/// /// If you're responding with MessagePack data, return a `MsgPack` type, /// where `T` implements `Serialize` from /// [Serde](https://github.com/serde-rs/serde). The content type of the response @@ -51,6 +55,20 @@ pub use self::rmp_serde::decode::Error as MsgPackError; /// MsgPack(user_from_id) /// } /// ``` +/// +/// ## Incoming Data Limits +/// +/// The default size limit for incoming MessagePack data is 1MiB. Setting a +/// limit protects your application from denial of service (DOS) attacks and +/// from resource exhaustion through high memory consumption. The limit can be +/// increased by setting the `limits.msgpack` configuration parameter. For +/// instance, to increase the MessagePack limit to 5MiB for all environments, +/// you may add the following to your `Rocket.toml`: +/// +/// ```toml +/// [global.limits] +/// msgpack = 5242880 +/// ``` #[derive(Debug)] pub struct MsgPack(pub T); diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index e03798a2..866c6c17 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -44,9 +44,13 @@ //! characters) to use as the session key //! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="` //! * **tls**: _[table]_ a table with two keys: 1) `certs`: _[string]_ a path -//! to a certificate chain in PEM format, and 2) `key`: _[string]_ a path to a -//! private key file in PEM format for the certificate in `certs` +//! to a certificate chain in PEM format, and 2) `key`: _[string]_ a path to a +//! private key file in PEM format for the certificate in `certs` //! * example: `{ certs = "/path/to/certs.pem", key = "/path/to/key.pem" }` +//! * **limits**: _[table]_ a table where the key _[string]_ corresponds to a +//! data type and the value _[u64]_ corresponds to the maximum size in bytes +//! Rocket should accept for that type. +//! * example: `{ forms = 65536 }` (maximum form size to 64KiB) //! //! ### Rocket.toml //! @@ -68,6 +72,7 @@ //! workers = max(number_of_cpus, 2) //! log = "normal" //! session_key = [randomly generated at launch] +//! limits = { forms = 32768 } //! //! [staging] //! address = "0.0.0.0" @@ -75,6 +80,7 @@ //! workers = max(number_of_cpus, 2) //! log = "normal" //! session_key = [randomly generated at launch] +//! limits = { forms = 32768 } //! //! [production] //! address = "0.0.0.0" @@ -82,6 +88,7 @@ //! workers = max(number_of_cpus, 2) //! log = "critical" //! session_key = [randomly generated at launch] +//! limits = { forms = 32768 } //! ``` //! //! The `workers` and `session_key` default parameters are computed by Rocket diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs index 0fd9da52..b075142f 100644 --- a/lib/src/request/form/mod.rs +++ b/lib/src/request/form/mod.rs @@ -148,16 +148,30 @@ use outcome::Outcome::*; /// /// ## Performance and Correctness Considerations /// -/// Whether you should use a `str` or `String` in your `FromForm` type depends -/// on your use case. The primary question to answer is: _Can the input contain -/// characters that must be URL encoded?_ Note that this includes commmon -/// characters such as spaces. If so, then you must use `String`, whose +/// Whether you should use a `&RawStr` or `String` in your `FromForm` type +/// depends on your use case. The primary question to answer is: _Can the input +/// contain characters that must be URL encoded?_ Note that this includes +/// commmon characters such as spaces. If so, then you must use `String`, whose /// `FromFormValue` implementation deserializes the URL encoded string for you. /// Because the `str` references will refer directly to the underlying form /// data, they will be raw and URL encoded. /// -/// If your string values will not contain URL encoded characters, using `str` -/// will result in fewer allocation and is thus spreferred. +/// If your string values will not contain URL encoded characters, using +/// `RawStr` will result in fewer allocation and is thus preferred. +/// +/// ## Incoming Data Limits +/// +/// The default size limit for incoming form data is 32KiB. Setting a limit +/// protects your application from denial of service (DOS) attacks and from +/// resource exhaustion through high memory consumption. The limit can be +/// increased by setting the `limits.forms` configuration parameter. For +/// instance, to increase the forms limit to 512KiB for all environments, you +/// may add the following to your `Rocket.toml`: +/// +/// ```toml +/// [global.limits] +/// forms = 524288 +/// ``` pub struct Form<'f, T: FromForm<'f> + 'f> { object: T, form_string: String, From f97b02dda651b8acbcb5dc07f7d2158b664c3760 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 00:40:33 -0700 Subject: [PATCH 109/297] Note the extras in the config example. --- examples/config/Rocket.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index 64ebdfc2..4002abf6 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -11,8 +11,8 @@ address = "localhost" port = 8000 workers = 1 log = "normal" -hi = "Hello!" -is_extra = true +hi = "Hello!" # this is an unused extra; maybe application specific? +is_extra = true # this is an unused extra; maybe application specific? [staging] address = "0.0.0.0" From d6e86be1b0cf8a2b33d980189e0edc16a9c753bc Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 17:42:44 -0700 Subject: [PATCH 110/297] Make route collisions a hard error. This is a breaking change. Previously, route collisions were warnings. --- examples/errors/src/main.rs | 1 + lib/src/error.rs | 20 ++++++++++++++++++-- lib/src/rocket.rs | 4 ++-- lib/src/router/mod.rs | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/examples/errors/src/main.rs b/examples/errors/src/main.rs index edcb3981..8ce806cf 100644 --- a/examples/errors/src/main.rs +++ b/examples/errors/src/main.rs @@ -21,6 +21,7 @@ fn not_found(req: &rocket::Request) -> content::HTML { fn main() { let e = rocket::ignite() + // .mount("/", routes![hello, hello]) // uncoment this to get an error .mount("/", routes![hello]) .catch(errors![not_found]) .launch(); diff --git a/lib/src/error.rs b/lib/src/error.rs index ebbf14c5..65008a36 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -23,11 +23,14 @@ pub enum Error { /// The kind of launch error that occured. /// /// In almost every instance, a launch error occurs because of an I/O error; -/// this represented by the `Io` variant. The `Unknown` variant captures all -/// other kinds of launch errors. +/// this is represented by the `Io` variant. A launch error may also occur +/// because of ill-defined routes that lead to collisions; this is represented +/// by the `Collision` variant. The `Unknown` variant captures all other kinds +/// of launch errors. #[derive(Debug)] pub enum LaunchErrorKind { Io(io::Error), + Collision, Unknown(Box<::std::error::Error + Send + Sync>) } @@ -110,6 +113,13 @@ impl LaunchError { } } +impl From for LaunchError { + #[inline] + fn from(kind: LaunchErrorKind) -> LaunchError { + LaunchError::new(kind) + } +} + impl From for LaunchError { #[inline] fn from(error: hyper::Error) -> LaunchError { @@ -125,6 +135,7 @@ impl fmt::Display for LaunchErrorKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), + LaunchErrorKind::Collision => write!(f, "route collisions detected"), LaunchErrorKind::Unknown(ref e) => write!(f, "unknown error: {}", e) } } @@ -152,6 +163,7 @@ impl ::std::error::Error for LaunchError { self.mark_handled(); match *self.kind() { LaunchErrorKind::Io(_) => "an I/O error occured during launch", + LaunchErrorKind::Collision => "route collisions were detected", LaunchErrorKind::Unknown(_) => "an unknown error occured during launch" } } @@ -168,6 +180,10 @@ impl Drop for LaunchError { error!("Rocket failed to launch due to an I/O error."); panic!("{}", e); } + LaunchErrorKind::Collision => { + error!("Rocket failed to launch due to routing collisions."); + panic!("route collisions detected"); + } LaunchErrorKind::Unknown(ref e) => { error!("Rocket failed to launch due to an unknown error."); panic!("{}", e); diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index fefc5e79..0131db00 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -18,7 +18,7 @@ use response::{Body, Response}; use router::{Router, Route}; use catcher::{self, Catcher}; use outcome::Outcome; -use error::{Error, LaunchError}; +use error::{Error, LaunchError, LaunchErrorKind}; use http::{Method, Status, Header, Session}; use http::hyper::{self, header}; @@ -596,7 +596,7 @@ impl Rocket { /// ``` pub fn launch(self) -> LaunchError { if self.router.has_collisions() { - warn!("Route collisions detected!"); + return LaunchError::from(LaunchErrorKind::Collision); } let full_addr = format!("{}:{}", self.config.address, self.config.port); diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 799e9298..cb8155b4 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -51,7 +51,7 @@ impl Router { for b_route in routes.iter().skip(i + 1) { if a_route.collides_with(b_route) { result = true; - warn!("{} and {} collide!", a_route, b_route); + error!("{} and {} collide!", a_route, b_route); } } } From 0e759edf78a696f8fc3ffcd7f181720dace2382d Mon Sep 17 00:00:00 2001 From: Joshua Rombauer Date: Tue, 18 Apr 2017 23:10:18 +0200 Subject: [PATCH 111/297] Implement 'From' for 'Stream' instead of custom 'from'. Closes #267. --- lib/src/response/stream.rs | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/src/response/stream.rs b/lib/src/response/stream.rs index 37e91c4f..2d424d18 100644 --- a/lib/src/response/stream.rs +++ b/lib/src/response/stream.rs @@ -13,24 +13,6 @@ use http::Status; pub struct Stream(T, u64); impl Stream { - /// Create a new stream from the given `reader`. - /// - /// # Example - /// - /// Stream a response from whatever is in `stdin`. Note: you probably - /// shouldn't do this. - /// - /// ```rust - /// use std::io; - /// use rocket::response::Stream; - /// - /// # #[allow(unused_variables)] - /// let response = Stream::from(io::stdin()); - /// ``` - pub fn from(reader: T) -> Stream { - Stream(reader, DEFAULT_CHUNK_SIZE) - } - /// Create a new stream from the given `reader` and sets the chunk size for /// each streamed chunk to `chunk_size` bytes. /// @@ -57,6 +39,26 @@ impl Debug for Stream { } } +/// Create a new stream from the given `reader`. +/// +/// # Example +/// +/// Stream a response from whatever is in `stdin`. Note: you probably +/// shouldn't do this. +/// +/// ```rust +/// use std::io; +/// use rocket::response::Stream; +/// +/// # #[allow(unused_variables)] +/// let response = Stream::from(io::stdin()); +/// ``` +impl From for Stream { + fn from(reader: T) -> Self { + Stream(reader, DEFAULT_CHUNK_SIZE) + } +} + /// Sends a response to the client using the "Chunked" transfer encoding. The /// maximum chunk size is 4KiB. /// From 8555a0fad50664233e58833189458f575ec6b8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Fro=C5=82ow?= Date: Tue, 18 Apr 2017 21:45:30 +0200 Subject: [PATCH 112/297] Fix typo in Template documentation: words -> works. --- contrib/src/templates/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/src/templates/mod.rs b/contrib/src/templates/mod.rs index c7169a75..84d5ee8e 100644 --- a/contrib/src/templates/mod.rs +++ b/contrib/src/templates/mod.rs @@ -24,7 +24,7 @@ use rocket::http::{ContentType, Status}; /// The Template type implements generic support for template rendering in /// Rocket. /// -/// Templating in Rocket words by first discovering all of the templates inside +/// Templating in Rocket works by first discovering all of the templates inside /// the template directory. The template directory is configurable via the /// `template_dir` configuration parameter and defaults to `templates/`. The /// path set in `template_dir` should be relative to the Rocket configuration From 7b48ca710319f40f6f597f09c15588dd72733db7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 21:52:02 -0700 Subject: [PATCH 113/297] Add optional input for IntoOutcome. Add mapper methods to Outcome. This is a breaking change to `IntoOutcome`. The MsgPack and JSON types now use `into_outcome` to generate the final `Outcome` from their `FromData` implementations. Resolves #98. --- contrib/src/json.rs | 14 +++---- contrib/src/msgpack.rs | 12 ++---- lib/src/data/from_data.rs | 6 ++- lib/src/outcome.rs | 70 ++++++++++++++++++++++++++++++++- lib/src/request/from_request.rs | 7 +++- lib/src/response/flash.rs | 2 +- 6 files changed, 88 insertions(+), 23 deletions(-) diff --git a/contrib/src/json.rs b/contrib/src/json.rs index 437c3022..3c7b412a 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut}; use std::io::Read; use rocket::config; -use rocket::outcome::Outcome; +use rocket::outcome::{Outcome, IntoOutcome}; use rocket::request::Request; use rocket::data::{self, Data, FromData}; use rocket::response::{self, Responder, content}; @@ -100,14 +100,10 @@ impl FromData for JSON { .and_then(|c| c.limits.get("json")) .unwrap_or(LIMIT); - let reader = data.open().take(size_limit); - match serde_json::from_reader(reader).map(|val| JSON(val)) { - Ok(value) => Outcome::Success(value), - Err(e) => { - error_!("Couldn't parse JSON body: {:?}", e); - Outcome::Failure((Status::BadRequest, e)) - } - } + serde_json::from_reader(data.open().take(size_limit)) + .map(|val| JSON(val)) + .map_err(|e| { error_!("Couldn't parse JSON body: {:?}", e); e }) + .into_outcome(Status::BadRequest) } } diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index ce417bcc..f8a47d1d 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, DerefMut}; use std::io::{Cursor, Read}; use rocket::config; -use rocket::outcome::Outcome; +use rocket::outcome::{Outcome, IntoOutcome}; use rocket::request::Request; use rocket::data::{self, Data, FromData}; use rocket::response::{self, Responder, Response}; @@ -120,13 +120,9 @@ impl FromData for MsgPack { return Outcome::Failure((Status::BadRequest, e)); }; - match rmp_serde::from_slice(&buf).map(|val| MsgPack(val)) { - Ok(value) => Outcome::Success(value), - Err(e) => { - error_!("Couldn't parse MessagePack body: {:?}", e); - Outcome::Failure((Status::BadRequest, e)) - } - } + rmp_serde::from_slice(&buf).map(|val| MsgPack(val)) + .map_err(|e| { error_!("Couldn't parse MessagePack body: {:?}", e); e }) + .into_outcome(Status::BadRequest) } } diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index 57ad8203..bd233dcd 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -10,11 +10,13 @@ use data::Data; pub type Outcome = outcome::Outcome; impl<'a, S, E> IntoOutcome for Result { + type Input = Status; + #[inline] - fn into_outcome(self) -> Outcome { + fn into_outcome(self, status: Status) -> Outcome { match self { Ok(val) => Success(val), - Err(err) => Failure((Status::InternalServerError, err)) + Err(err) => Failure((status, err)) } } } diff --git a/lib/src/outcome.rs b/lib/src/outcome.rs index 2ce8b634..b9dc5415 100644 --- a/lib/src/outcome.rs +++ b/lib/src/outcome.rs @@ -105,7 +105,9 @@ pub enum Outcome { /// Conversion trait from some type into an Outcome type. pub trait IntoOutcome { - fn into_outcome(self) -> Outcome; + type Input: Sized; + + fn into_outcome(self, input: Self::Input) -> Outcome; } impl Outcome { @@ -329,6 +331,72 @@ impl Outcome { } } + /// Maps an `Outcome` to an `Outcome` by applying the + /// function `f` to the value of type `S` in `self` if `self` is an + /// `Outcome::Success`. + /// + /// ```rust + /// # use rocket::outcome::Outcome; + /// # use rocket::outcome::Outcome::*; + /// # + /// let x: Outcome = Success(10); + /// + /// let mapped = x.map(|v| if v == 10 { "10" } else { "not 10" }); + /// assert_eq!(mapped, Success("10")); + /// ``` + #[inline] + pub fn map T>(self, f: M) -> Outcome { + match self { + Success(val) => Success(f(val)), + Failure(val) => Failure(val), + Forward(val) => Forward(val), + } + } + + /// Maps an `Outcome` to an `Outcome` by applying the + /// function `f` to the value of type `E` in `self` if `self` is an + /// `Outcome::Failure`. + /// + /// ```rust + /// # use rocket::outcome::Outcome; + /// # use rocket::outcome::Outcome::*; + /// # + /// let x: Outcome = Failure("hi"); + /// + /// let mapped = x.map_failure(|v| if v == "hi" { 10 } else { 0 }); + /// assert_eq!(mapped, Failure(10)); + /// ``` + #[inline] + pub fn map_failure T>(self, f: M) -> Outcome { + match self { + Success(val) => Success(val), + Failure(val) => Failure(f(val)), + Forward(val) => Forward(val), + } + } + + /// Maps an `Outcome` to an `Outcome` by applying the + /// function `f` to the value of type `F` in `self` if `self` is an + /// `Outcome::Forward`. + /// + /// ```rust + /// # use rocket::outcome::Outcome; + /// # use rocket::outcome::Outcome::*; + /// # + /// let x: Outcome = Forward(5); + /// + /// let mapped = x.map_forward(|v| if v == 5 { "a" } else { "b" }); + /// assert_eq!(mapped, Forward("a")); + /// ``` + #[inline] + pub fn map_forward T>(self, f: M) -> Outcome { + match self { + Success(val) => Success(val), + Failure(val) => Failure(val), + Forward(val) => Forward(f(val)), + } + } + /// Converts from `Outcome` to `Outcome<&mut S, &mut E, &mut F>`. /// /// ```rust diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 0f1dc395..30428974 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -12,10 +12,13 @@ use http::uri::URI; pub type Outcome = outcome::Outcome; impl IntoOutcome for Result { - fn into_outcome(self) -> Outcome { + type Input = Status; + + #[inline] + fn into_outcome(self, status: Status) -> Outcome { match self { Ok(val) => Success(val), - Err(val) => Failure((Status::BadRequest, val)) + Err(err) => Failure((status, err)) } } } diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs index 1fe1cf7f..bde8949a 100644 --- a/lib/src/response/flash.rs +++ b/lib/src/response/flash.rs @@ -242,6 +242,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> { request.cookies().remove(cookie); } - r.into_outcome() + r.into_outcome(Status::BadRequest) } } From 0d18faf91ee473b29e91bb26e94908c2976777b9 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 Apr 2017 22:05:56 -0700 Subject: [PATCH 114/297] Add a docstring to the emitted static route info. Resolves #258. --- codegen/src/decorators/route.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index 6b7ac7d6..b060f300 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -269,6 +269,7 @@ fn generic_route_decorator(known_method: Option>, let struct_name = user_fn_name.prepend(ROUTE_STRUCT_PREFIX); let (path, method, media_type, rank) = route.explode(ecx); let static_route_info_item = quote_item!(ecx, + /// Rocket code generated static route information structure. #[allow(non_upper_case_globals)] pub static $struct_name: ::rocket::StaticRouteInfo = ::rocket::StaticRouteInfo { From 41386cfb78d7724f9acc39821337285d604444e8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 19 Apr 2017 02:23:06 -0700 Subject: [PATCH 115/297] Display the port that was resolved, not configured. --- Cargo.toml | 3 +++ lib/Cargo.toml | 7 ++++++- lib/src/error.rs | 7 +++++++ lib/src/rocket.rs | 9 +++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97ae1a1f..858417c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ members = [ "examples/raw_sqlite", "examples/hello_tls", ] + +[replace] +"hyper:0.10.8" = { git = 'https://github.com/SergioBenitez/hyper', branch = "0.10.x" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 90b654a3..88c1161e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -22,7 +22,6 @@ tls = ["rustls", "hyper-rustls"] term-painter = "0.2" log = "0.3" url = "1" -hyper = { version = "0.10.4", default-features = false } toml = { version = "0.2", default-features = false } num_cpus = "1" state = "0.2.1" @@ -35,6 +34,12 @@ pear_codegen = "0.0.8" rustls = { version = "0.5.8", optional = true } cookie = { version = "0.7.4", features = ["percent-encode", "secure"] } +[dependencies.hyper] +git = "https://github.com/SergioBenitez/hyper" +branch = "0.10.x" +version = "0.10.4" +default-features = false + [dependencies.hyper-rustls] git = "https://github.com/SergioBenitez/hyper-rustls" default-features = false diff --git a/lib/src/error.rs b/lib/src/error.rs index 65008a36..4cecd2be 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -130,6 +130,13 @@ impl From for LaunchError { } } +impl From for LaunchError { + #[inline] + fn from(error: io::Error) -> LaunchError { + LaunchError::new(LaunchErrorKind::Io(error)) + } +} + impl fmt::Display for LaunchErrorKind { #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 0131db00..b298578b 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -601,15 +601,20 @@ impl Rocket { let full_addr = format!("{}:{}", self.config.address, self.config.port); serve!(self, &full_addr, |server, proto| { - let server = match server { + let mut server = match server { Ok(server) => server, Err(e) => return LaunchError::from(e) }; + let (addr, port) = match server.local_addr() { + Ok(server_addr) => (&self.config.address, server_addr.port()), + Err(e) => return LaunchError::from(e) + }; + launch_info!("🚀 {} {}{}", White.paint("Rocket has launched from"), White.bold().paint(proto), - White.bold().paint(&full_addr)); + White.bold().paint(&format!("{}:{}", addr, port))); let threads = self.config.workers as usize; if let Err(e) = server.handle_threads(self, threads) { From f2d054c4a2cf974bdb66eab997642b02ecd60c47 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 19 Apr 2017 23:42:12 -0700 Subject: [PATCH 116/297] Use upstream hyper. --- Cargo.toml | 3 --- lib/Cargo.toml | 7 +------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 858417c0..97ae1a1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,3 @@ members = [ "examples/raw_sqlite", "examples/hello_tls", ] - -[replace] -"hyper:0.10.8" = { git = 'https://github.com/SergioBenitez/hyper', branch = "0.10.x" } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 88c1161e..2a8741cd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -33,12 +33,7 @@ pear = "0.0.8" pear_codegen = "0.0.8" rustls = { version = "0.5.8", optional = true } cookie = { version = "0.7.4", features = ["percent-encode", "secure"] } - -[dependencies.hyper] -git = "https://github.com/SergioBenitez/hyper" -branch = "0.10.x" -version = "0.10.4" -default-features = false +hyper = { version = "0.10.9", default-features = false } [dependencies.hyper-rustls] git = "https://github.com/SergioBenitez/hyper-rustls" From ac0c78a0cd52042237cd5bb085548b0ae64bdb40 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 20 Apr 2017 13:43:01 -0700 Subject: [PATCH 117/297] Initial implementation of fairings: structured middleware for Rocket. Closes #55. --- Cargo.toml | 1 + examples/fairings/Cargo.toml | 11 ++ examples/fairings/src/main.rs | 38 +++++++ examples/fairings/src/tests.rs | 11 ++ lib/Cargo.toml | 2 +- lib/src/config/config.rs | 1 + lib/src/config/custom_values.rs | 3 + lib/src/error.rs | 14 ++- lib/src/ext.rs | 46 ++++++++ lib/src/fairing/mod.rs | 192 ++++++++++++++++++++++++++++++++ lib/src/http/accept.rs | 3 +- lib/src/http/content_type.rs | 3 +- lib/src/http/media_type.rs | 2 +- lib/src/http/mod.rs | 45 -------- lib/src/lib.rs | 2 + lib/src/rocket.rs | 106 ++++++++++++++---- 16 files changed, 408 insertions(+), 72 deletions(-) create mode 100644 examples/fairings/Cargo.toml create mode 100644 examples/fairings/src/main.rs create mode 100644 examples/fairings/src/tests.rs create mode 100644 lib/src/fairing/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 97ae1a1f..cdd48faa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,4 +38,5 @@ members = [ "examples/session", "examples/raw_sqlite", "examples/hello_tls", + "examples/fairings", ] diff --git a/examples/fairings/Cargo.toml b/examples/fairings/Cargo.toml new file mode 100644 index 00000000..c45bc723 --- /dev/null +++ b/examples/fairings/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fairings" +version = "0.0.0" +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } + +[dev-dependencies] +rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/fairings/src/main.rs b/examples/fairings/src/main.rs new file mode 100644 index 00000000..f28ebbcb --- /dev/null +++ b/examples/fairings/src/main.rs @@ -0,0 +1,38 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use std::io::Cursor; + +use rocket::Fairing; +use rocket::http::Method; + +#[cfg(test)] mod tests; + +#[put("/")] +fn hello() -> &'static str { + "Hello, world!" +} + +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount("/", routes![hello]) + .attach(Fairing::Launch(Box::new(|rocket| { + println!("Rocket is about to launch! Exciting! Here we go..."); + Ok(rocket) + }))) + .attach(Fairing::Request(Box::new(|req, _| { + println!(" => Incoming request: {}", req); + println!(" => Changing method to `PUT`."); + req.set_method(Method::Put); + }))) + .attach(Fairing::Response(Box::new(|_, res| { + println!(" => Rewriting response body."); + res.set_sized_body(Cursor::new("Hello, fairings!")); + }))) +} + +fn main() { + rocket().launch(); +} diff --git a/examples/fairings/src/tests.rs b/examples/fairings/src/tests.rs new file mode 100644 index 00000000..197cefd2 --- /dev/null +++ b/examples/fairings/src/tests.rs @@ -0,0 +1,11 @@ +use super::rocket; +use rocket::testing::MockRequest; +use rocket::http::Method::*; + +#[test] +fn fairings() { + let rocket = rocket(); + let mut req = MockRequest::new(Get, "/"); + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.body_string(), Some("Hello, fairings!".into())); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 2a8741cd..8dcecad5 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -32,7 +32,7 @@ smallvec = "0.3.3" pear = "0.0.8" pear_codegen = "0.0.8" rustls = { version = "0.5.8", optional = true } -cookie = { version = "0.7.4", features = ["percent-encode", "secure"] } +cookie = { version = "0.7.5", features = ["percent-encode", "secure"] } hyper = { version = "0.10.9", default-features = false } [dependencies.hyper-rustls] diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index e51fb90d..6b88f056 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -28,6 +28,7 @@ use http::Key; /// .workers(12) /// .unwrap(); /// ``` +#[derive(Clone)] pub struct Config { /// The environment that this configuration corresponds to. pub environment: Environment, diff --git a/lib/src/config/custom_values.rs b/lib/src/config/custom_values.rs index 948bdacf..c534e669 100644 --- a/lib/src/config/custom_values.rs +++ b/lib/src/config/custom_values.rs @@ -6,6 +6,7 @@ use logger::LoggingLevel; use config::{Result, Config, Value, ConfigError}; use http::Key; +#[derive(Clone)] pub enum SessionKey { Generated(Key), Provided(Key) @@ -29,12 +30,14 @@ impl SessionKey { } #[cfg(feature = "tls")] +#[derive(Clone)] pub struct TlsConfig { pub certs: Vec, pub key: PrivateKey } #[cfg(not(feature = "tls"))] +#[derive(Clone)] pub struct TlsConfig; // Size limit configuration. We cache those used by Rocket internally but don't diff --git a/lib/src/error.rs b/lib/src/error.rs index 4cecd2be..0210d886 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -24,13 +24,15 @@ pub enum Error { /// /// In almost every instance, a launch error occurs because of an I/O error; /// this is represented by the `Io` variant. A launch error may also occur -/// because of ill-defined routes that lead to collisions; this is represented -/// by the `Collision` variant. The `Unknown` variant captures all other kinds -/// of launch errors. +/// because of ill-defined routes that lead to collisions or because a launch +/// fairing encounted an error; these are represented by the `Collision` and +/// `FailedFairing` variants, respectively. The `Unknown` variant captures all +/// other kinds of launch errors. #[derive(Debug)] pub enum LaunchErrorKind { Io(io::Error), Collision, + FailedFairing, Unknown(Box<::std::error::Error + Send + Sync>) } @@ -143,6 +145,7 @@ impl fmt::Display for LaunchErrorKind { match *self { LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), LaunchErrorKind::Collision => write!(f, "route collisions detected"), + LaunchErrorKind::FailedFairing => write!(f, "a launch fairing failed"), LaunchErrorKind::Unknown(ref e) => write!(f, "unknown error: {}", e) } } @@ -171,6 +174,7 @@ impl ::std::error::Error for LaunchError { match *self.kind() { LaunchErrorKind::Io(_) => "an I/O error occured during launch", LaunchErrorKind::Collision => "route collisions were detected", + LaunchErrorKind::FailedFairing => "a launch fairing reported an error", LaunchErrorKind::Unknown(_) => "an unknown error occured during launch" } } @@ -191,6 +195,10 @@ impl Drop for LaunchError { error!("Rocket failed to launch due to routing collisions."); panic!("route collisions detected"); } + LaunchErrorKind::FailedFairing => { + error!("Rocket failed to launch due to a failing launch fairing."); + panic!("launch fairing failure"); + } LaunchErrorKind::Unknown(ref e) => { error!("Rocket failed to launch due to an unknown error."); panic!("{}", e); diff --git a/lib/src/ext.rs b/lib/src/ext.rs index 8813b741..394d0997 100644 --- a/lib/src/ext.rs +++ b/lib/src/ext.rs @@ -1,4 +1,5 @@ use std::io; +use smallvec::{Array, SmallVec}; pub trait ReadExt: io::Read { fn read_max(&mut self, mut buf: &mut [u8]) -> io::Result { @@ -17,3 +18,48 @@ pub trait ReadExt: io::Read { } impl ReadExt for T { } + +// TODO: It would be nice if we could somehow have one trait that could give us +// either SmallVec or Vec. +pub trait IntoCollection { + fn into_collection>(self) -> SmallVec
; + fn mapped U, A: Array>(self, f: F) -> SmallVec; +} + +impl IntoCollection for T { + #[inline] + fn into_collection>(self) -> SmallVec { + let mut vec = SmallVec::new(); + vec.push(self); + vec + } + + #[inline(always)] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + f(self).into_collection() + } +} + +impl IntoCollection for Vec { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + SmallVec::from_vec(self) + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.into_iter().map(|item| f(item)).collect() + } +} + +impl<'a, T: Clone> IntoCollection for &'a [T] { + #[inline(always)] + fn into_collection>(self) -> SmallVec { + self.iter().cloned().collect() + } + + #[inline] + fn mapped U, A: Array>(self, mut f: F) -> SmallVec { + self.iter().cloned().map(|item| f(item)).collect() + } +} diff --git a/lib/src/fairing/mod.rs b/lib/src/fairing/mod.rs new file mode 100644 index 00000000..e2069a65 --- /dev/null +++ b/lib/src/fairing/mod.rs @@ -0,0 +1,192 @@ +//! Fairings: structured interposition at launch, request, and response time. +//! +//! Fairings allow for structured interposition at various points in the +//! application lifetime. Fairings can be seen as a restricted form of +//! "middleware". A fairing is simply a function with a particular signature +//! that Rocket will run at a requested point in a program. You can use fairings +//! to rewrite or record information about requests and responses, or to perform +//! an action once a Rocket application has launched. +//! +//! ## Attaching +//! +//! You must inform Rocket about fairings that you wish to be active by calling +//! the [`attach`](/rocket/struct.Rocket.html#method.attach) method on the +//! [`Rocket`](/rocket/struct.Rocket.html) instance and passing in the +//! appropriate [`Fairing`](/rocket/fairing/enum.Fairing.html). For instance, to +//! attach `Request` and `Response` fairings named `req_fairing` and +//! `res_fairing` to a new Rocket instance, you might write: +//! +//! ```rust +//! # use rocket::Fairing; +//! # let req_fairing = Fairing::Request(Box::new(|_, _| ())); +//! # let res_fairing = Fairing::Response(Box::new(|_, _| ())); +//! # #[allow(unused_variables)] +//! let rocket = rocket::ignite() +//! .attach(vec![req_fairing, res_fairing]); +//! ``` +//! +//! Once a fairing is attached, Rocket will execute it at the appropiate time, +//! which varies depending on the fairing type. + +use {Rocket, Request, Response, Data}; + +// We might imagine that a request fairing returns an `Outcome`. If it returns +// `Success`, we don't do any routing and use that response directly. Same if it +// returns `Failure`. We only route if it returns `Forward`. I've chosen not to +// go this direction because I feel like request guards are the correct +// mechanism to use here. In other words, enabling this at the fairing level +// encourages implicit handling, a bad practice. Fairings can still, however, +// return a default `Response` if routing fails via a response fairing. For +// instance, to automatically handle preflight in CORS, a response fairing can +// check that the user didn't handle the `OPTIONS` request (404) and return an +// appropriate response. This allows the users to handle `OPTIONS` requests +// when they'd like but default to the fairing when they don't want to. + +/// The type of a **launch** fairing callback. +/// +/// The `Rocket` parameter is the `Rocket` instance being built. The launch +/// fairing can modify the `Rocket` instance arbitrarily. +/// +/// TODO: Document fully with examples before 0.3. +pub type LaunchFn = Box Result + Send + Sync + 'static>; +/// The type of a **request** fairing callback. +/// +/// The `&mut Request` parameter is the incoming request, and the `&Data` +/// parameter is the incoming data in the request. +/// +/// TODO: Document fully with examples before 0.3. +pub type RequestFn = Box; +/// The type of a **response** fairing callback. +/// +/// The `&Request` parameter is the request that was routed, and the `&mut +/// Response` parameter is the result response. +/// +/// TODO: Document fully with examples before 0.3. +pub type ResponseFn = Box; + +/// An enum representing the three fairing types: launch, request, and response. +/// +/// ## Fairing Types +/// +/// The three types of fairings, launch, request, and response, operate as +/// follows: +/// +/// * *Launch Fairings* +/// +/// An attached launch fairing will be called immediately before the Rocket +/// application has launched. At this point, Rocket has opened a socket for +/// listening but has not yet begun accepting connections. A launch fairing +/// can arbitrarily modify the `Rocket` instance being launched. It returns +/// `Ok` if it would like launching to proceed nominally and `Err` +/// otherwise. If a launch fairing returns `Err`, launch is aborted. The +/// [`LaunchFn`](/rocket/fairing/type.LaunchFn.html) documentation contains +/// further information and tips on the function signature. +/// +/// * *Request Fairings* +/// +/// An attached request fairing is called when a request is received. At +/// this point, Rocket has parsed the incoming HTTP into a +/// [Request](/rocket/struct.Request.html) and +/// [Data](/rocket/struct.Data.html) object but has not routed the request. +/// A request fairing can modify the request at will and +/// [peek](/rocket/struct.Data.html#method.peek) into the incoming data. It +/// may not, however, abort or respond directly to the request; these issues +/// are better handled via [request +/// guards](/rocket/request/trait.FromRequest.html) or via response +/// fairings. A modified request is routed as if it was the original +/// request. The [`RequestFn`](/rocket/fairing/type.RequestFn.html) +/// documentation contains further information and tips on the function +/// signature. +/// +/// * *Response Fairings* +/// +/// An attached response fairing is called when a response is ready to be +/// sent to the client. At this point, Rocket has completed all routing, +/// including to error catchers, and has generated the would-be final +/// response. A response fairing can modify the response at will. A response +/// fairing, can, for example, provide a default response when the user +/// fails to handle the request by checking for 404 responses. The +/// [`ResponseFn`](/rocket/fairing/type.ResponseFn.html) documentation +/// contains further information and tips on the function signature. +/// +/// See the [top-level documentation](/rocket/fairing/) for general information. +pub enum Fairing { + /// A launch fairing. Called just before Rocket launches. + Launch(LaunchFn), + /// A request fairing. Called when a request is received. + Request(RequestFn), + /// A response fairing. Called when a response is ready to be sent. + Response(ResponseFn), +} + +#[derive(Default)] +pub(crate) struct Fairings { + pub launch: Vec, + pub request: Vec, + pub response: Vec, +} + +impl Fairings { + #[inline] + pub fn new() -> Fairings { + Fairings::default() + } + + #[inline(always)] + pub fn attach_all(&mut self, fairings: Vec) { + for fairing in fairings { + self.attach(fairing) + } + } + + #[inline] + pub fn attach(&mut self, fairing: Fairing) { + match fairing { + Fairing::Launch(f) => self.launch.push(f), + Fairing::Request(f) => self.request.push(f), + Fairing::Response(f) => self.response.push(f), + } + } + + #[inline(always)] + pub fn handle_launch(&mut self, mut rocket: Rocket) -> Option { + let mut success = Some(()); + let launch_fairings = ::std::mem::replace(&mut self.launch, vec![]); + for fairing in launch_fairings { + rocket = fairing(rocket).unwrap_or_else(|r| { success = None; r }); + } + + success.map(|_| rocket) + } + + #[inline(always)] + pub fn handle_request(&self, req: &mut Request, data: &Data) { + for fairing in &self.request { + fairing(req, data); + } + } + + #[inline(always)] + pub fn handle_response(&self, request: &Request, response: &mut Response) { + for fairing in &self.response { + fairing(request, response); + } + } + + pub fn pretty_print_counts(&self) { + use term_painter::ToStyle; + use term_painter::Color::White; + + if !self.launch.is_empty() { + info_!("{} launch", White.paint(self.launch.len())); + } + + if !self.request.is_empty() { + info_!("{} request", White.paint(self.request.len())); + } + + if !self.response.is_empty() { + info_!("{} response", White.paint(self.response.len())); + } + } +} diff --git a/lib/src/http/accept.rs b/lib/src/http/accept.rs index dbb811e6..577709d1 100644 --- a/lib/src/http/accept.rs +++ b/lib/src/http/accept.rs @@ -4,7 +4,8 @@ use std::fmt; use smallvec::SmallVec; -use http::{Header, IntoCollection, MediaType}; +use ext::IntoCollection; +use http::{Header, MediaType}; use http::parse::parse_accept; #[derive(Debug, Clone, PartialEq)] diff --git a/lib/src/http/content_type.rs b/lib/src/http/content_type.rs index 276dc2c0..ccc06791 100644 --- a/lib/src/http/content_type.rs +++ b/lib/src/http/content_type.rs @@ -3,7 +3,8 @@ use std::ops::Deref; use std::str::FromStr; use std::fmt; -use http::{IntoCollection, Header, MediaType}; +use ext::IntoCollection; +use http::{Header, MediaType}; use http::hyper::mime::Mime; /// Representation of HTTP Content-Types. diff --git a/lib/src/http/media_type.rs b/lib/src/http/media_type.rs index 081b330d..dd685de0 100644 --- a/lib/src/http/media_type.rs +++ b/lib/src/http/media_type.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use std::fmt; use std::hash::{Hash, Hasher}; -use http::IntoCollection; +use ext::IntoCollection; use http::uncased::{uncased_eq, UncasedStr}; use http::parse::{IndexedStr, parse_media_type}; diff --git a/lib/src/http/mod.rs b/lib/src/http/mod.rs index 1225a01f..faf34c4d 100644 --- a/lib/src/http/mod.rs +++ b/lib/src/http/mod.rs @@ -38,48 +38,3 @@ pub use self::raw_str::RawStr; pub use self::media_type::MediaType; pub use self::cookies::*; pub use self::session::*; - -use smallvec::{Array, SmallVec}; - -pub trait IntoCollection { - fn into_collection>(self) -> SmallVec; - fn mapped U, A: Array>(self, f: F) -> SmallVec; -} - -impl IntoCollection for T { - #[inline] - fn into_collection>(self) -> SmallVec { - let mut vec = SmallVec::new(); - vec.push(self); - vec - } - - #[inline(always)] - fn mapped U, A: Array>(self, mut f: F) -> SmallVec { - f(self).into_collection() - } -} - -impl IntoCollection for Vec { - #[inline(always)] - fn into_collection>(self) -> SmallVec { - SmallVec::from_vec(self) - } - - #[inline] - fn mapped U, A: Array>(self, mut f: F) -> SmallVec { - self.into_iter().map(|item| f(item)).collect() - } -} - -impl<'a, T: Clone> IntoCollection for &'a [T] { - #[inline(always)] - fn into_collection>(self) -> SmallVec { - self.iter().cloned().collect() - } - - #[inline] - fn mapped U, A: Array>(self, mut f: F) -> SmallVec { - self.iter().cloned().map(|item| f(item)).collect() - } -} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index b2842639..318d1ad6 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -126,6 +126,7 @@ pub mod config; pub mod data; pub mod handler; pub mod error; +pub mod fairing; mod router; mod rocket; @@ -139,6 +140,7 @@ mod ext; #[doc(hidden)] pub use codegen::{StaticRouteInfo, StaticCatchInfo}; #[doc(inline)] pub use outcome::Outcome; #[doc(inline)] pub use data::Data; +#[doc(inline)] pub use fairing::Fairing; pub use router::Route; pub use request::{Request, State}; pub use error::{Error, LaunchError}; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index b298578b..4940e665 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -3,6 +3,7 @@ use std::str::from_utf8_unchecked; use std::cmp::min; use std::net::SocketAddr; use std::io::{self, Write}; +use std::mem; use term_painter::Color::*; use term_painter::ToStyle; @@ -10,7 +11,7 @@ use state::Container; #[cfg(feature = "tls")] use hyper_rustls::TlsServer; use {logger, handler}; -use ext::ReadExt; +use ext::{ReadExt, IntoCollection}; use config::{self, Config, LoggedValue}; use request::{Request, FormItems}; use data::Data; @@ -19,6 +20,7 @@ use router::{Router, Route}; use catcher::{self, Catcher}; use outcome::Outcome; use error::{Error, LaunchError, LaunchErrorKind}; +use fairing::{Fairing, Fairings}; use http::{Method, Status, Header, Session}; use http::hyper::{self, header}; @@ -31,7 +33,8 @@ pub struct Rocket { router: Router, default_catchers: HashMap, catchers: HashMap, - state: Container + state: Container, + fairings: Fairings } #[doc(hidden)] @@ -69,6 +72,7 @@ impl hyper::Handler for Rocket { }; // Dispatch the request to get a response, then write that response out. + // let req = UnsafeCell::new(req); let response = self.dispatch(&mut req, data); self.issue_response(response, res) } @@ -105,11 +109,7 @@ macro_rules! serve { impl Rocket { #[inline] - fn issue_response(&self, mut response: Response, hyp_res: hyper::FreshResponse) { - // Add the 'rocket' server header, and write out the response. - // TODO: If removing Hyper, write out `Date` header too. - response.set_header(Header::new("Server", "Rocket")); - + fn issue_response(&self, response: Response, hyp_res: hyper::FreshResponse) { match self.write_response(response, hyp_res) { Ok(_) => info_!("{}", Green.paint("Response succeeded.")), Err(e) => error_!("Failed to write response: {:?}.", e) @@ -193,6 +193,8 @@ impl Rocket { let (min_len, max_len) = ("_method=get".len(), "_method=delete".len()); let is_form = req.content_type().map_or(false, |ct| ct.is_form()); if is_form && req.method() == Method::Post && data_len >= min_len { + // We're only using this for comparison and throwing it away + // afterwards, so it doesn't matter if we have invalid UTF8. let form = unsafe { from_utf8_unchecked(&data.peek()[..min(data_len, max_len)]) }; @@ -207,19 +209,22 @@ impl Rocket { } } + // TODO: Explain this `UnsafeCell` business at a macro level. #[inline] - pub(crate) fn dispatch<'s, 'r>(&'s self, request: &'r mut Request<'s>, data: Data) - -> Response<'r> { + pub(crate) fn dispatch<'s, 'r>(&'s self, + request: &'r mut Request<'s>, + data: Data) -> Response<'r> { info!("{}:", request); // Inform the request about all of the precomputed state. request.set_preset_state(&self.config.session_key(), &self.state); - // Do a bit of preprocessing before routing. + // Do a bit of preprocessing before routing; run the attached fairings. self.preprocess_request(request, &data); + self.fairings.handle_request(request, &data); // Route the request to get a response. - match self.route(request, data) { + let mut response = match self.route(request, data) { Outcome::Success(mut response) => { // A user's route responded! Set the regular cookies. for cookie in request.cookies().delta() { @@ -234,17 +239,16 @@ impl Rocket { response } Outcome::Forward(data) => { - // There was no matching route. // Rust thinks `request` is still borrowed here, but it's // obviously not (data has nothing to do with it), so we // convince it to give us another mutable reference. - // FIXME: Pay the cost to copy Request into UnsafeCell? Pay the - // cost to use RefCell? Move the call to `issue_response` here - // to move Request and move directly into an UnsafeCell? - let request: &'r mut Request = unsafe { - &mut *(request as *const Request as *mut Request) + // TODO: Use something that is well defined, like UnsafeCell. + // But that causes variance issues...so wait for NLL. + let request: &'r mut Request<'s> = unsafe { + (&mut *(request as *const _ as *mut _)) }; + // There was no matching route. if request.method() == Method::Head { info_!("Autohandling {} request.", White.paint("HEAD")); request.set_method(Method::Get); @@ -256,7 +260,14 @@ impl Rocket { } } Outcome::Failure(status) => self.handle_error(status, request), - } + }; + + // Add the 'rocket' server header to the response and run fairings. + // TODO: If removing Hyper, write out `Date` header too. + response.set_header(Header::new("Server", "Rocket")); + self.fairings.handle_response(request, &mut response); + + response } /// Tries to find a `Responder` for a given `request`. It does this by @@ -406,7 +417,8 @@ impl Rocket { router: Router::new(), default_catchers: catcher::defaults::get(), catchers: catcher::defaults::get(), - state: Container::new() + state: Container::new(), + fairings: Fairings::new() } } @@ -574,6 +586,40 @@ impl Rocket { self } + /// Attaches zero or more fairings to this instance of Rocket. + /// + /// The `fairings` parameter to this function is generic: it may be either + /// a `Vec`, `&[Fairing]`, or simply `Fairing`. In all cases, all + /// supplied fairings are attached. + /// + /// # Examples + /// + /// ```rust + /// # #![feature(plugin)] + /// # #![plugin(rocket_codegen)] + /// # extern crate rocket; + /// use rocket::{Rocket, Fairing}; + /// + /// fn launch_fairing(rocket: Rocket) -> Result { + /// println!("Rocket is about to launch! You just see..."); + /// Ok(rocket) + /// } + /// + /// fn main() { + /// # if false { // We don't actually want to launch the server in an example. + /// rocket::ignite() + /// .attach(Fairing::Launch(Box::new(launch_fairing))) + /// .launch(); + /// # } + /// } + /// ``` + #[inline] + pub fn attach>(mut self, fairings: C) -> Self { + let fairings = fairings.into_collection::<[Fairing; 1]>().into_vec(); + self.fairings.attach_all(fairings); + self + } + /// Starts the application server and begins listening for and dispatching /// requests to mounted routes and catchers. Unless there is an error, this /// function does not return and blocks until program termination. @@ -594,11 +640,14 @@ impl Rocket { /// rocket::ignite().launch(); /// # } /// ``` - pub fn launch(self) -> LaunchError { + pub fn launch(mut self) -> LaunchError { if self.router.has_collisions() { return LaunchError::from(LaunchErrorKind::Collision); } + info!("📡 {}:", Magenta.paint("Fairings")); + self.fairings.pretty_print_counts(); + let full_addr = format!("{}:{}", self.config.address, self.config.port); serve!(self, &full_addr, |server, proto| { let mut server = match server { @@ -606,11 +655,22 @@ impl Rocket { Err(e) => return LaunchError::from(e) }; + // Determine the port we actually binded to. let (addr, port) = match server.local_addr() { Ok(server_addr) => (&self.config.address, server_addr.port()), Err(e) => return LaunchError::from(e) }; + // Run all of the launch fairings. + let mut fairings = mem::replace(&mut self.fairings, Fairings::new()); + self = match fairings.handle_launch(self) { + Some(rocket) => rocket, + None => return LaunchError::from(LaunchErrorKind::FailedFairing) + }; + + // Make sure we keep the request/response fairings! + self.fairings = fairings; + launch_info!("🚀 {} {}{}", White.paint("Rocket has launched from"), White.bold().paint(proto), @@ -630,4 +690,10 @@ impl Rocket { pub fn routes<'a>(&'a self) -> impl Iterator + 'a { self.router.routes() } + + /// Retrieve the configuration. + #[inline(always)] + pub fn config(&self) -> &Config { + self.config + } } From 1e5a1b89409feb827b0d1ee20525191d17ad5c13 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 20 Apr 2017 20:30:41 -0700 Subject: [PATCH 118/297] Remove 'testing' feature. Close stream on network error. This is a breaking change. The `testing` feature no longer exists. Testing structures can now be accessed without any features enabled. Prior to this change, Rocket would panic when draining from a network stream failed. With this change, Rocket force closes the stream on any error. This change also ensures that the `Fairings` launch output only prints if at least one fairing has been attached. --- examples/config/Cargo.toml | 3 - examples/content_types/Cargo.toml | 3 - examples/cookies/Cargo.toml | 3 - examples/errors/Cargo.toml | 3 - examples/extended_validation/Cargo.toml | 3 - examples/fairings/Cargo.toml | 3 - examples/forms/Cargo.toml | 3 - examples/from_request/Cargo.toml | 3 - examples/handlebars_templates/Cargo.toml | 3 - examples/hello_alt_methods/Cargo.toml | 3 - examples/hello_person/Cargo.toml | 3 - examples/hello_ranks/Cargo.toml | 3 - examples/hello_tls/Cargo.toml | 3 - examples/hello_world/Cargo.toml | 3 - examples/json/Cargo.toml | 3 - examples/managed_queue/Cargo.toml | 3 - examples/manual_routes/Cargo.toml | 3 - examples/msgpack/Cargo.toml | 3 - examples/optional_redirect/Cargo.toml | 3 - examples/optional_result/Cargo.toml | 3 - examples/pastebin/Cargo.toml | 3 - examples/query_params/Cargo.toml | 3 - examples/raw_sqlite/Cargo.toml | 3 - examples/redirect/Cargo.toml | 3 - examples/session/Cargo.toml | 3 - examples/state/Cargo.toml | 3 - examples/static_files/Cargo.toml | 3 - examples/testing/Cargo.toml | 3 - examples/uuid/Cargo.toml | 3 - lib/Cargo.toml | 1 - lib/benches/format-routing.rs | 1 - lib/benches/ranked-routing.rs | 1 - lib/benches/simple-routing.rs | 1 - lib/src/data/data.rs | 43 +++++++-- lib/src/data/data_stream.rs | 91 +++---------------- lib/src/data/mod.rs | 27 +----- lib/src/data/net_stream.rs | 87 ++++++++++++++++++ lib/src/data/test_data.rs | 75 --------------- lib/src/fairing/mod.rs | 10 +- lib/src/lib.rs | 2 +- lib/src/rocket.rs | 1 - lib/src/testing.rs | 41 ++------- lib/tests/form_method-issue-45.rs | 1 - lib/tests/form_value_decoding-issue-82.rs | 1 - lib/tests/head_handling.rs | 1 - lib/tests/limits.rs | 1 - lib/tests/precise-content-type-matching.rs | 1 - lib/tests/query-and-non-query-dont-collide.rs | 1 - lib/tests/redirect_from_catcher-issue-113.rs | 1 - lib/tests/remote-rewrite.rs | 1 - lib/tests/segments-issues-41-86.rs | 1 - 51 files changed, 155 insertions(+), 322 deletions(-) create mode 100644 lib/src/data/net_stream.rs delete mode 100644 lib/src/data/test_data.rs diff --git a/examples/config/Cargo.toml b/examples/config/Cargo.toml index 884c1fa8..dc5343ce 100644 --- a/examples/config/Cargo.toml +++ b/examples/config/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/content_types/Cargo.toml b/examples/content_types/Cargo.toml index e1455c7e..d999bedd 100644 --- a/examples/content_types/Cargo.toml +++ b/examples/content_types/Cargo.toml @@ -9,6 +9,3 @@ rocket_codegen = { path = "../../codegen" } serde = "0.9" serde_json = "0.9" serde_derive = "0.9" - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/cookies/Cargo.toml b/examples/cookies/Cargo.toml index ee0da5be..d10f4b89 100644 --- a/examples/cookies/Cargo.toml +++ b/examples/cookies/Cargo.toml @@ -11,6 +11,3 @@ rocket_codegen = { path = "../../codegen" } path = "../../contrib" default-features = false features = ["handlebars_templates"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/errors/Cargo.toml b/examples/errors/Cargo.toml index 9ef31a5c..2bb7eec2 100644 --- a/examples/errors/Cargo.toml +++ b/examples/errors/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/extended_validation/Cargo.toml b/examples/extended_validation/Cargo.toml index c2c1ffa3..dd719fa7 100644 --- a/examples/extended_validation/Cargo.toml +++ b/examples/extended_validation/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/fairings/Cargo.toml b/examples/fairings/Cargo.toml index c45bc723..40368ab6 100644 --- a/examples/fairings/Cargo.toml +++ b/examples/fairings/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/forms/Cargo.toml b/examples/forms/Cargo.toml index 6c73b2df..1c4cc4ee 100644 --- a/examples/forms/Cargo.toml +++ b/examples/forms/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/from_request/Cargo.toml b/examples/from_request/Cargo.toml index 31b232a0..f6551ac2 100644 --- a/examples/from_request/Cargo.toml +++ b/examples/from_request/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/handlebars_templates/Cargo.toml b/examples/handlebars_templates/Cargo.toml index 3a5b4266..5384c9e4 100644 --- a/examples/handlebars_templates/Cargo.toml +++ b/examples/handlebars_templates/Cargo.toml @@ -14,6 +14,3 @@ serde_json = "0.9" path = "../../contrib" default-features = false features = ["handlebars_templates"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_alt_methods/Cargo.toml b/examples/hello_alt_methods/Cargo.toml index d3f67872..d0731a51 100644 --- a/examples/hello_alt_methods/Cargo.toml +++ b/examples/hello_alt_methods/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_person/Cargo.toml b/examples/hello_person/Cargo.toml index 93ba8a66..6628c1af 100644 --- a/examples/hello_person/Cargo.toml +++ b/examples/hello_person/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_ranks/Cargo.toml b/examples/hello_ranks/Cargo.toml index f3b6dc95..99fd4aae 100644 --- a/examples/hello_ranks/Cargo.toml +++ b/examples/hello_ranks/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_tls/Cargo.toml b/examples/hello_tls/Cargo.toml index d691440e..1bfa6f1a 100644 --- a/examples/hello_tls/Cargo.toml +++ b/examples/hello_tls/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib", features = ["tls"] } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 9e726476..ccd454ee 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 3bee1177..5e96c61a 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -14,6 +14,3 @@ serde_derive = "0.9" path = "../../contrib" default-features = false features = ["json"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/managed_queue/Cargo.toml b/examples/managed_queue/Cargo.toml index da95be2f..24f7fe59 100644 --- a/examples/managed_queue/Cargo.toml +++ b/examples/managed_queue/Cargo.toml @@ -7,6 +7,3 @@ workspace = "../.." crossbeam = "*" rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/manual_routes/Cargo.toml b/examples/manual_routes/Cargo.toml index 3b20079f..af75e891 100644 --- a/examples/manual_routes/Cargo.toml +++ b/examples/manual_routes/Cargo.toml @@ -5,6 +5,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/msgpack/Cargo.toml b/examples/msgpack/Cargo.toml index 4d0bd76b..e2c904dd 100644 --- a/examples/msgpack/Cargo.toml +++ b/examples/msgpack/Cargo.toml @@ -13,6 +13,3 @@ serde_derive = "0.9" path = "../../contrib" default-features = false features = ["msgpack"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/optional_redirect/Cargo.toml b/examples/optional_redirect/Cargo.toml index 90ee8ab0..e945bd34 100644 --- a/examples/optional_redirect/Cargo.toml +++ b/examples/optional_redirect/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/optional_result/Cargo.toml b/examples/optional_result/Cargo.toml index b5ecb5b9..f0bd8b4e 100644 --- a/examples/optional_result/Cargo.toml +++ b/examples/optional_result/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/pastebin/Cargo.toml b/examples/pastebin/Cargo.toml index de14c65e..907ff59a 100644 --- a/examples/pastebin/Cargo.toml +++ b/examples/pastebin/Cargo.toml @@ -7,6 +7,3 @@ workspace = "../../" rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } rand = "0.3" - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/query_params/Cargo.toml b/examples/query_params/Cargo.toml index 45dd3c2c..0b8c86f6 100644 --- a/examples/query_params/Cargo.toml +++ b/examples/query_params/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/raw_sqlite/Cargo.toml b/examples/raw_sqlite/Cargo.toml index 9df1c4b5..9252323e 100644 --- a/examples/raw_sqlite/Cargo.toml +++ b/examples/raw_sqlite/Cargo.toml @@ -7,6 +7,3 @@ workspace = "../../" rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } rusqlite = "0.10" - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/redirect/Cargo.toml b/examples/redirect/Cargo.toml index 2e4693be..0f38fae7 100644 --- a/examples/redirect/Cargo.toml +++ b/examples/redirect/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml index 64c480e8..390ef1f6 100644 --- a/examples/session/Cargo.toml +++ b/examples/session/Cargo.toml @@ -11,6 +11,3 @@ rocket_codegen = { path = "../../codegen" } path = "../../contrib" default-features = false features = ["handlebars_templates"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/state/Cargo.toml b/examples/state/Cargo.toml index c41a40eb..008d3ae7 100644 --- a/examples/state/Cargo.toml +++ b/examples/state/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/static_files/Cargo.toml b/examples/static_files/Cargo.toml index 09159834..4c3355eb 100644 --- a/examples/static_files/Cargo.toml +++ b/examples/static_files/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/testing/Cargo.toml b/examples/testing/Cargo.toml index 98770f56..7a728c3d 100644 --- a/examples/testing/Cargo.toml +++ b/examples/testing/Cargo.toml @@ -6,6 +6,3 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/examples/uuid/Cargo.toml b/examples/uuid/Cargo.toml index 06ab2b76..28841e1e 100644 --- a/examples/uuid/Cargo.toml +++ b/examples/uuid/Cargo.toml @@ -13,6 +13,3 @@ lazy_static = "^0.2" default-features = false path = "../../contrib" features = ["uuid"] - -[dev-dependencies] -rocket = { path = "../../lib", features = ["testing"] } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8dcecad5..8cda74a2 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -15,7 +15,6 @@ build = "build.rs" categories = ["web-programming::http-server"] [features] -testing = [] tls = ["rustls", "hyper-rustls"] [dependencies] diff --git a/lib/benches/format-routing.rs b/lib/benches/format-routing.rs index a10d0880..362b829d 100644 --- a/lib/benches/format-routing.rs +++ b/lib/benches/format-routing.rs @@ -18,7 +18,6 @@ fn rocket() -> rocket::Rocket { .mount("/", routes![get, post]) } -#[cfg(feature = "testing")] mod benches { extern crate test; diff --git a/lib/benches/ranked-routing.rs b/lib/benches/ranked-routing.rs index d1d44503..1caef283 100644 --- a/lib/benches/ranked-routing.rs +++ b/lib/benches/ranked-routing.rs @@ -31,7 +31,6 @@ fn rocket() -> rocket::Rocket { .mount("/", routes![post, post2, post3]) } -#[cfg(feature = "testing")] mod benches { extern crate test; diff --git a/lib/benches/simple-routing.rs b/lib/benches/simple-routing.rs index 6363a7b7..be4d9cce 100644 --- a/lib/benches/simple-routing.rs +++ b/lib/benches/simple-routing.rs @@ -34,7 +34,6 @@ fn rocket() -> rocket::Rocket { index_b, index_c, index_dyn_a]) } -#[cfg(feature = "testing")] mod benches { extern crate test; diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index d6e92e1d..74d2fe79 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -6,8 +6,9 @@ use std::mem::transmute; #[cfg(feature = "tls")] use hyper_rustls::WrappedStream; +use super::data_stream::{DataStream, StreamReader, kill_stream}; +use super::net_stream::NetStream; use ext::ReadExt; -use super::data_stream::{DataStream, HyperNetStream, StreamReader, kill_stream}; use http::hyper::h1::HttpReader; use http::hyper::buffer; @@ -17,6 +18,9 @@ use http::hyper::net::{HttpStream, NetworkStream}; pub type BodyReader<'a, 'b> = self::HttpReader<&'a mut self::buffer::BufReader<&'b mut NetworkStream>>; +/// The number of bytes to read into the "peek" buffer. +const PEEK_BYTES: usize = 4096; + /// Type representing the data in the body of an incoming request. /// /// This type is the only means by which the body of a request can be retrieved. @@ -90,19 +94,19 @@ impl Data { let net_stream = h_body.get_ref().get_ref(); #[cfg(feature = "tls")] - fn concrete_stream(stream: &&mut NetworkStream) -> Option { + fn concrete_stream(stream: &&mut NetworkStream) -> Option { stream.downcast_ref::() - .map(|s| HyperNetStream::Http(s.clone())) + .map(|s| NetStream::Http(s.clone())) .or_else(|| { stream.downcast_ref::() - .map(|s| HyperNetStream::Https(s.clone())) + .map(|s| NetStream::Https(s.clone())) }) } #[cfg(not(feature = "tls"))] - fn concrete_stream(stream: &&mut NetworkStream) -> Option { + fn concrete_stream(stream: &&mut NetworkStream) -> Option { stream.downcast_ref::() - .map(|s| HyperNetStream::Http(s.clone())) + .map(|s| NetStream::Http(s.clone())) } // Retrieve the underlying HTTPStream from Hyper. @@ -164,15 +168,15 @@ impl Data { } // Creates a new data object with an internal buffer `buf`, where the cursor - // in the buffer is at `pos` and the buffer has `cap` valid bytes. The - // remainder of the data bytes can be read from `stream`. + // in the buffer is at `pos` and the buffer has `cap` valid bytes. Thus, the + // bytes `vec[pos..cap]` are buffered and unread. The remainder of the data + // bytes can be read from `stream`. pub(crate) fn new(mut buf: Vec, pos: usize, mut cap: usize, mut stream: StreamReader ) -> Data { // Make sure the buffer is large enough for the bytes we want to peek. - const PEEK_BYTES: usize = 4096; if buf.len() < PEEK_BYTES { trace_!("Resizing peek buffer from {} to {}.", buf.len(), PEEK_BYTES); buf.resize(PEEK_BYTES, 0); @@ -203,6 +207,27 @@ impl Data { capacity: cap, } } + + /// This creates a `data` object from a local data source `data`. + pub(crate) fn local(mut data: Vec) -> Data { + // Emulate peek buffering. + let (buf, rest) = if data.len() <= PEEK_BYTES { + (data, vec![]) + } else { + let rest = data.split_off(PEEK_BYTES); + (data, rest) + }; + + let (buf_len, stream_len) = (buf.len(), rest.len() as u64); + let stream = NetStream::Local(Cursor::new(rest)); + Data { + buffer: buf, + stream: HttpReader::SizedReader(stream, stream_len), + is_done: stream_len == 0, + position: 0, + capacity: buf_len, + } + } } impl Drop for Data { diff --git a/lib/src/data/data_stream.rs b/lib/src/data/data_stream.rs index a63371d4..c98b8293 100644 --- a/lib/src/data/data_stream.rs +++ b/lib/src/data/data_stream.rs @@ -1,80 +1,14 @@ use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take}; -use std::net::{SocketAddr, Shutdown}; -use std::time::Duration; +use std::net::Shutdown; -#[cfg(feature = "tls")] use hyper_rustls::WrappedStream as RustlsStream; +use super::net_stream::NetStream; -use http::hyper::net::{HttpStream, NetworkStream}; +use http::hyper::net::NetworkStream; use http::hyper::h1::HttpReader; -pub type StreamReader = HttpReader; +pub type StreamReader = HttpReader; pub type InnerStream = Chain>>, BufReader>; -#[derive(Clone)] -pub enum HyperNetStream { - Http(HttpStream), - #[cfg(feature = "tls")] - Https(RustlsStream) -} - -macro_rules! with_inner { - ($net:expr, |$stream:ident| $body:expr) => ({ - trace!("{}:{}", file!(), line!()); - match *$net { - HyperNetStream::Http(ref $stream) => $body, - #[cfg(feature = "tls")] HyperNetStream::Https(ref $stream) => $body - } - }); - ($net:expr, |mut $stream:ident| $body:expr) => ({ - trace!("{}:{}", file!(), line!()); - match *$net { - HyperNetStream::Http(ref mut $stream) => $body, - #[cfg(feature = "tls")] HyperNetStream::Https(ref mut $stream) => $body - } - }) -} - -impl io::Read for HyperNetStream { - #[inline(always)] - fn read(&mut self, buf: &mut [u8]) -> io::Result { - with_inner!(self, |mut stream| io::Read::read(stream, buf)) - } -} - -impl io::Write for HyperNetStream { - #[inline(always)] - fn write(&mut self, buf: &[u8]) -> io::Result { - with_inner!(self, |mut stream| io::Write::write(stream, buf)) - } - - #[inline(always)] - fn flush(&mut self) -> io::Result<()> { - with_inner!(self, |mut stream| io::Write::flush(stream)) - } -} - -impl NetworkStream for HyperNetStream { - #[inline(always)] - fn peer_addr(&mut self) -> io::Result { - with_inner!(self, |mut stream| NetworkStream::peer_addr(stream)) - } - - #[inline(always)] - fn set_read_timeout(&self, dur: Option) -> io::Result<()> { - with_inner!(self, |stream| NetworkStream::set_read_timeout(stream, dur)) - } - - #[inline(always)] - fn set_write_timeout(&self, dur: Option) -> io::Result<()> { - with_inner!(self, |stream| NetworkStream::set_write_timeout(stream, dur)) - } - - #[inline(always)] - fn close(&mut self, how: Shutdown) -> io::Result<()> { - with_inner!(self, |mut stream| NetworkStream::close(stream, how)) - } -} - /// Raw data stream of a request body. /// /// This stream can only be obtained by calling @@ -83,12 +17,12 @@ impl NetworkStream for HyperNetStream { /// Instead, it must be used as an opaque `Read` or `BufRead` structure. pub struct DataStream { stream: InnerStream, - network: HyperNetStream, + network: NetStream, } impl DataStream { #[inline(always)] - pub(crate) fn new(stream: InnerStream, network: HyperNetStream) -> DataStream { + pub(crate) fn new(stream: InnerStream, network: NetStream) -> DataStream { DataStream { stream, network } } } @@ -112,19 +46,18 @@ impl BufRead for DataStream { } } -// pub fn kill_stream(stream: &mut S, network: &mut HyperNetStream) { -pub fn kill_stream(stream: &mut S, network: &mut N) { - io::copy(&mut stream.take(1024), &mut io::sink()).expect("kill_stream: sink"); - // If there are any more bytes, kill it. - let mut buf = [0]; - if let Ok(n) = stream.read(&mut buf) { - if n > 0 { +pub fn kill_stream(stream: &mut S, network: &mut N) { + // Take <= 1k from the stream. If there might be more data, force close. + const FLUSH_LEN: u64 = 1024; + match io::copy(&mut stream.take(FLUSH_LEN), &mut io::sink()) { + Ok(FLUSH_LEN) | Err(_) => { warn_!("Data left unread. Force closing network stream."); if let Err(e) = network.close(Shutdown::Both) { error_!("Failed to close network stream: {:?}", e); } } + Ok(n) => debug!("flushed {} unread bytes", n) } } diff --git a/lib/src/data/mod.rs b/lib/src/data/mod.rs index af868a66..5f1bb391 100644 --- a/lib/src/data/mod.rs +++ b/lib/src/data/mod.rs @@ -1,25 +1,8 @@ -//! Types and traits for reading and parsing request body data. - -#[cfg(any(test, feature = "testing"))] -#[path = "."] -mod items { - mod test_data; - - pub use self::test_data::Data; - pub use self::test_data::DataStream; -} - -#[cfg(not(any(test, feature = "testing")))] -#[path = "."] -mod items { - mod data; - mod data_stream; - - pub use self::data::Data; - pub use self::data_stream::DataStream; -} - +mod data; +mod data_stream; +mod net_stream; mod from_data; +pub use self::data::Data; +pub use self::data_stream::DataStream; pub use self::from_data::{FromData, Outcome}; -pub use self::items::{Data, DataStream}; diff --git a/lib/src/data/net_stream.rs b/lib/src/data/net_stream.rs new file mode 100644 index 00000000..28c3d766 --- /dev/null +++ b/lib/src/data/net_stream.rs @@ -0,0 +1,87 @@ +use std::io::{self, Cursor}; +use std::net::{SocketAddr, Shutdown}; +use std::time::Duration; + +#[cfg(feature = "tls")] use hyper_rustls::WrappedStream as RustlsStream; +use http::hyper::net::{HttpStream, NetworkStream}; + +use self::NetStream::*; + +// This is a representation of all of the possible network streams we might get. +// This really shouldn't be necessary, but, you know, Hyper. +#[derive(Clone)] +pub enum NetStream { + Http(HttpStream), + #[cfg(feature = "tls")] + Https(RustlsStream), + Local(Cursor>) +} + +impl io::Read for NetStream { + #[inline(always)] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match *self { + Http(ref mut stream) => stream.read(buf), + Local(ref mut stream) => stream.read(buf), + #[cfg(feature = "tls")] Https(ref mut stream) => stream.read(buf) + } + } +} + +impl io::Write for NetStream { + #[inline(always)] + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + Http(ref mut stream) => stream.write(buf), + Local(ref mut stream) => stream.write(buf), + #[cfg(feature = "tls")] Https(ref mut stream) => stream.write(buf) + } + } + + #[inline(always)] + fn flush(&mut self) -> io::Result<()> { + match *self { + Http(ref mut stream) => stream.flush(), + Local(ref mut stream) => stream.flush(), + #[cfg(feature = "tls")] Https(ref mut stream) => stream.flush() + } + } +} + +impl NetworkStream for NetStream { + #[inline(always)] + fn peer_addr(&mut self) -> io::Result { + match *self { + Http(ref mut stream) => stream.peer_addr(), + #[cfg(feature = "tls")] Https(ref mut stream) => stream.peer_addr(), + Local(_) => Err(io::Error::from(io::ErrorKind::AddrNotAvailable)), + } + } + + #[inline(always)] + fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + match *self { + Http(ref stream) => stream.set_read_timeout(dur), + #[cfg(feature = "tls")] Https(ref stream) => stream.set_read_timeout(dur), + Local(_) => Ok(()), + } + } + + #[inline(always)] + fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + match *self { + Http(ref stream) => stream.set_write_timeout(dur), + #[cfg(feature = "tls")] Https(ref stream) => stream.set_write_timeout(dur), + Local(_) => Ok(()), + } + } + + #[inline(always)] + fn close(&mut self, how: Shutdown) -> io::Result<()> { + match *self { + Http(ref mut stream) => stream.close(how), + #[cfg(feature = "tls")] Https(ref mut stream) => stream.close(how), + Local(_) => Ok(()), + } + } +} diff --git a/lib/src/data/test_data.rs b/lib/src/data/test_data.rs deleted file mode 100644 index 5970108b..00000000 --- a/lib/src/data/test_data.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::io::{self, Read, BufRead, Write, Cursor, BufReader}; -use std::path::Path; -use std::fs::File; - -use http::hyper::h1::HttpReader; -use http::hyper::net::NetworkStream; -use http::hyper::buffer; - -pub type BodyReader<'a, 'b> = - self::HttpReader<&'a mut self::buffer::BufReader<&'b mut NetworkStream>>; - -const PEEK_BYTES: usize = 4096; - -pub struct DataStream { - stream: BufReader>>, -} - -impl Read for DataStream { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) - } -} - -impl BufRead for DataStream { - fn fill_buf(&mut self) -> io::Result<&[u8]> { - self.stream.fill_buf() - } - - fn consume(&mut self, amt: usize) { - self.stream.consume(amt) - } -} - -pub struct Data { - data: Vec, -} - -impl Data { - pub fn open(self) -> DataStream { - DataStream { stream: BufReader::new(Cursor::new(self.data)) } - } - - #[inline] - pub fn peek(&self) -> &[u8] { - &self.data[..::std::cmp::min(PEEK_BYTES, self.data.len())] - } - - #[inline] - pub fn peek_complete(&self) -> bool { - self.data.len() <= PEEK_BYTES - } - - #[inline] - pub fn stream_to(self, writer: &mut W) -> io::Result { - io::copy(&mut self.open(), writer) - } - - #[inline] - pub fn stream_to_file>(self, path: P) -> io::Result { - io::copy(&mut self.open(), &mut File::create(path)?) - } - - pub(crate) fn from_hyp(mut h_body: BodyReader) -> Result { - let mut vec = Vec::new(); - if let Err(_) = io::copy(&mut h_body, &mut vec) { - return Err("Reading from body failed."); - }; - - Ok(Data::new(vec)) - } - - pub(crate) fn new(data: Vec) -> Data { - Data { data: data } - } -} diff --git a/lib/src/fairing/mod.rs b/lib/src/fairing/mod.rs index e2069a65..dd176d49 100644 --- a/lib/src/fairing/mod.rs +++ b/lib/src/fairing/mod.rs @@ -173,9 +173,17 @@ impl Fairings { } } + fn num_attached(&self) -> usize { + self.launch.len() + self.request.len() + self.response.len() + } + pub fn pretty_print_counts(&self) { use term_painter::ToStyle; - use term_painter::Color::White; + use term_painter::Color::{White, Magenta}; + + if self.num_attached() > 0 { + info!("📡 {}:", Magenta.paint("Fairings")); + } if !self.launch.is_empty() { info_!("{} launch", White.paint(self.launch.len())); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 318d1ad6..9221c04e 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -117,7 +117,7 @@ extern crate smallvec; #[cfg(test)] #[macro_use] extern crate lazy_static; #[doc(hidden)] #[macro_use] pub mod logger; -#[cfg(any(test, feature = "testing"))] pub mod testing; +pub mod testing; pub mod http; pub mod request; pub mod response; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 4940e665..754cf213 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -645,7 +645,6 @@ impl Rocket { return LaunchError::from(LaunchErrorKind::Collision); } - info!("📡 {}:", Magenta.paint("Fairings")); self.fairings.pretty_print_counts(); let full_addr = format!("{}:{}", self.config.address, self.config.port); diff --git a/lib/src/testing.rs b/lib/src/testing.rs index 733d00ff..46445aa4 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -1,33 +1,5 @@ //! A tiny module for testing Rocket applications. //! -//! # Enabling -//! -//! The `testing` module is only available when Rocket is compiled with the -//! `testing` feature flag. The suggested way to enable the `testing` module is -//! through Cargo's `[dev-dependencies]` feature which allows features (and -//! other dependencies) to be enabled exclusively when testing/benchmarking your -//! application. -//! -//! To compile Rocket with the `testing` feature for testing/benchmarking, add -//! the following to your `Cargo.toml`: -//! -//! ```toml -//! [dev-dependencies] -//! rocket = { version = "*", features = ["testing"] } -//! ``` -//! -//! Then, in your testing module, `use` the testing types. This typically looks -//! as follows: -//! -//! ```rust,ignore -//! #[cfg(test)] -//! mod test { -//! use super::rocket; -//! use rocket::testing::MockRequest; -//! use rocket::http::Method::*; -//! } -//! ``` -//! //! # Usage //! //! The testing methadology is simple: @@ -51,9 +23,10 @@ //! builds a request for submitting a login form with three fields: //! //! ```rust -//! # use rocket::http::Method::*; -//! # use rocket::testing::MockRequest; -//! # use rocket::http::ContentType; +//! use rocket::http::Method::*; +//! use rocket::http::ContentType; +//! use rocket::testing::MockRequest; +//! //! let (username, password, age) = ("user", "password", 32); //! MockRequest::new(Post, "/login") //! .header(ContentType::Form) @@ -119,7 +92,7 @@ impl<'r> MockRequest<'r> { pub fn new>(method: Method, uri: S) -> Self { MockRequest { request: Request::new(method, uri.as_ref().to_string()), - data: Data::new(vec![]) + data: Data::local(vec![]) } } @@ -222,7 +195,7 @@ impl<'r> MockRequest<'r> { /// ``` #[inline] pub fn body>(mut self, body: S) -> Self { - self.data = Data::new(body.as_ref().into()); + self.data = Data::local(body.as_ref().into()); self } @@ -261,7 +234,7 @@ impl<'r> MockRequest<'r> { /// # } /// ``` pub fn dispatch_with<'s>(&'s mut self, rocket: &'r Rocket) -> Response<'s> { - let data = ::std::mem::replace(&mut self.data, Data::new(vec![])); + let data = ::std::mem::replace(&mut self.data, Data::local(vec![])); rocket.dispatch(&mut self.request, data) } } diff --git a/lib/tests/form_method-issue-45.rs b/lib/tests/form_method-issue-45.rs index 0100dfae..a271842a 100644 --- a/lib/tests/form_method-issue-45.rs +++ b/lib/tests/form_method-issue-45.rs @@ -16,7 +16,6 @@ fn bug(form_data: Form) -> &'static str { "OK" } -#[cfg(feature = "testing")] mod tests { use super::*; use rocket::testing::MockRequest; diff --git a/lib/tests/form_value_decoding-issue-82.rs b/lib/tests/form_value_decoding-issue-82.rs index 32e5260b..904d4cca 100644 --- a/lib/tests/form_value_decoding-issue-82.rs +++ b/lib/tests/form_value_decoding-issue-82.rs @@ -15,7 +15,6 @@ fn bug(form_data: Form) -> String { form_data.into_inner().form_data } -#[cfg(feature = "testing")] mod tests { use super::*; use rocket::testing::MockRequest; diff --git a/lib/tests/head_handling.rs b/lib/tests/head_handling.rs index 0c86c067..c4cc52aa 100644 --- a/lib/tests/head_handling.rs +++ b/lib/tests/head_handling.rs @@ -20,7 +20,6 @@ fn other() -> content::JSON<()> { content::JSON(()) } -#[cfg(feature = "testing")] mod tests { use super::*; diff --git a/lib/tests/limits.rs b/lib/tests/limits.rs index 9070c42f..c42bf6f0 100644 --- a/lib/tests/limits.rs +++ b/lib/tests/limits.rs @@ -15,7 +15,6 @@ fn index(form: Form) -> String { form.into_inner().value } -#[cfg(feature = "testing")] mod tests { use rocket; use rocket::config::{Environment, Config, Limits}; diff --git a/lib/tests/precise-content-type-matching.rs b/lib/tests/precise-content-type-matching.rs index 8a717555..18e4538c 100644 --- a/lib/tests/precise-content-type-matching.rs +++ b/lib/tests/precise-content-type-matching.rs @@ -23,7 +23,6 @@ fn specified_html() -> &'static str { "specified_html" } -#[cfg(feature = "testing")] mod tests { use super::*; diff --git a/lib/tests/query-and-non-query-dont-collide.rs b/lib/tests/query-and-non-query-dont-collide.rs index 45105251..3e1c14a2 100644 --- a/lib/tests/query-and-non-query-dont-collide.rs +++ b/lib/tests/query-and-non-query-dont-collide.rs @@ -18,7 +18,6 @@ fn second() -> &'static str { "no query" } -#[cfg(feature = "testing")] mod tests { use super::*; diff --git a/lib/tests/redirect_from_catcher-issue-113.rs b/lib/tests/redirect_from_catcher-issue-113.rs index c68152a6..03dc5d81 100644 --- a/lib/tests/redirect_from_catcher-issue-113.rs +++ b/lib/tests/redirect_from_catcher-issue-113.rs @@ -10,7 +10,6 @@ fn not_found() -> Redirect { Redirect::to("/") } -#[cfg(feature = "testing")] mod tests { use super::*; use rocket::testing::MockRequest; diff --git a/lib/tests/remote-rewrite.rs b/lib/tests/remote-rewrite.rs index 4ef8b494..c7befbc5 100644 --- a/lib/tests/remote-rewrite.rs +++ b/lib/tests/remote-rewrite.rs @@ -10,7 +10,6 @@ fn get_ip(remote: SocketAddr) -> String { remote.to_string() } -#[cfg(feature = "testing")] mod remote_rewrite_tests { use super::*; use rocket::testing::MockRequest; diff --git a/lib/tests/segments-issues-41-86.rs b/lib/tests/segments-issues-41-86.rs index 834b92e1..34d55ad4 100644 --- a/lib/tests/segments-issues-41-86.rs +++ b/lib/tests/segments-issues-41-86.rs @@ -30,7 +30,6 @@ fn dual(user: String, path: Segments) -> String { user + "/is/" + &path.collect::>().join("/") } -#[cfg(feature = "testing")] mod tests { use super::*; use rocket::testing::MockRequest; From 0a8de2f0a61785efd72f77994136a3866cde4885 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 22 Apr 2017 22:06:33 -0700 Subject: [PATCH 119/297] Ensure that all raw HTTP data is transfer-decoded. --- lib/src/data/data.rs | 153 ++++++++++++++++-------------------- lib/src/data/data_stream.rs | 81 +++++++------------ lib/src/data/net_stream.rs | 25 +++++- 3 files changed, 122 insertions(+), 137 deletions(-) diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 74d2fe79..df51eef9 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -1,22 +1,24 @@ -use std::io::{self, Read, Write, Cursor, BufReader}; +use std::io::{self, Read, Write, Cursor, BufReader, Chain, Take}; use std::path::Path; use std::fs::File; use std::time::Duration; -use std::mem::transmute; #[cfg(feature = "tls")] use hyper_rustls::WrappedStream; -use super::data_stream::{DataStream, StreamReader, kill_stream}; +use super::data_stream::DataStream; use super::net_stream::NetStream; use ext::ReadExt; +use http::hyper; use http::hyper::h1::HttpReader; -use http::hyper::buffer; use http::hyper::h1::HttpReader::*; use http::hyper::net::{HttpStream, NetworkStream}; -pub type BodyReader<'a, 'b> = - self::HttpReader<&'a mut self::buffer::BufReader<&'b mut NetworkStream>>; +pub type HyperBodyReader<'a, 'b> = + self::HttpReader<&'a mut hyper::buffer::BufReader<&'b mut NetworkStream>>; + +// |---- from hyper ----| +pub type BodyReader = HttpReader>>, BufReader>>; /// The number of bytes to read into the "peek" buffer. const PEEK_BYTES: usize = 4096; @@ -51,12 +53,8 @@ const PEEK_BYTES: usize = 4096; /// without consuming the `Data` object. pub struct Data { buffer: Vec, - is_done: bool, - // TODO: This sucks as it depends on a TCPStream. Oh, hyper. - stream: StreamReader, - // Ideally we wouldn't have these, but Hyper forces us to. - position: usize, - capacity: usize, + is_complete: bool, + stream: BodyReader, } impl Data { @@ -67,39 +65,28 @@ impl Data { /// instance. This ensures that a `Data` type _always_ represents _all_ of /// the data in a request. pub fn open(mut self) -> DataStream { - // Swap out the buffer and stream for empty ones so we can move. - let mut buffer = vec![]; - let mut stream = EmptyReader(self.stream.get_ref().clone()); - ::std::mem::swap(&mut buffer, &mut self.buffer); - ::std::mem::swap(&mut stream, &mut self.stream); + let buffer = ::std::mem::replace(&mut self.buffer, vec![]); + let empty_stream = Cursor::new(vec![]).take(0) + .chain(BufReader::new(NetStream::Local(Cursor::new(vec![])))); - // Setup the underlying reader at the correct pointers. - let mut cursor = Cursor::new(buffer); - cursor.set_position(self.position as u64); - - // Get a reference to the underlying network stream. - let network = stream.get_ref().clone(); - - // The first part of the stream is the buffer. Then the real steam. - let buf = cursor.take((self.capacity - self.position) as u64); - let stream = buf.chain(BufReader::new(stream)); - - DataStream::new(stream, network) + let empty_http_stream = HttpReader::SizedReader(empty_stream, 0); + let stream = ::std::mem::replace(&mut self.stream, empty_http_stream); + DataStream(Cursor::new(buffer).chain(stream)) } // FIXME: This is absolutely terrible (downcasting!), thanks to Hyper. - pub(crate) fn from_hyp(mut h_body: BodyReader) -> Result { - // Create the Data object from hyper's buffer. - let (vec, pos, cap) = h_body.get_mut().take_buf(); - let net_stream = h_body.get_ref().get_ref(); + pub(crate) fn from_hyp(mut body: HyperBodyReader) -> Result { + // Steal the internal, undecoded data buffer and net stream from Hyper. + let (hyper_buf, pos, cap) = body.get_mut().take_buf(); + let hyper_net_stream = body.get_ref().get_ref(); #[cfg(feature = "tls")] fn concrete_stream(stream: &&mut NetworkStream) -> Option { - stream.downcast_ref::() - .map(|s| NetStream::Http(s.clone())) + stream.downcast_ref::() + .map(|s| NetStream::Https(s.clone())) .or_else(|| { - stream.downcast_ref::() - .map(|s| NetStream::Https(s.clone())) + stream.downcast_ref::() + .map(|s| NetStream::Http(s.clone())) }) } @@ -109,25 +96,32 @@ impl Data { .map(|s| NetStream::Http(s.clone())) } - // Retrieve the underlying HTTPStream from Hyper. - let stream = match concrete_stream(net_stream) { - Some(stream) => stream, + // Retrieve the underlying Http(s)Stream from Hyper. + let net_stream = match concrete_stream(hyper_net_stream) { + Some(net_stream) => net_stream, None => return Err("Stream is not an HTTP(s) stream!") }; // Set the read timeout to 5 seconds. - stream.set_read_timeout(Some(Duration::from_secs(5))).expect("timeout set"); + net_stream.set_read_timeout(Some(Duration::from_secs(5))).expect("timeout set"); - // Create a reader from the stream. Don't read what's already buffered. - let buffered = (cap - pos) as u64; - let reader = match h_body { - SizedReader(_, n) => SizedReader(stream, n - buffered), - EofReader(_) => EofReader(stream), - EmptyReader(_) => EmptyReader(stream), - ChunkedReader(_, n) => ChunkedReader(stream, n.map(|k| k - buffered)), + // TODO: Explain this. + trace_!("Hyper buffer: [{}..{}] ({} bytes).", pos, cap, cap - pos); + let (start, remaining) = (pos as u64, (cap - pos) as u64); + let mut cursor = Cursor::new(hyper_buf); + cursor.set_position(start); + let inner_data = cursor.take(remaining) + .chain(BufReader::new(net_stream.clone())); + + // Create an HTTP reader from the stream. + let http_stream = match body { + SizedReader(_, n) => SizedReader(inner_data, n), + EofReader(_) => EofReader(inner_data), + EmptyReader(_) => EmptyReader(inner_data), + ChunkedReader(_, n) => ChunkedReader(inner_data, n) }; - Ok(Data::new(vec, pos, cap, reader)) + Ok(Data::new(http_stream)) } /// Retrieve the `peek` buffer. @@ -138,7 +132,7 @@ impl Data { /// buffer contains _all_ of the data in the body of the request. #[inline(always)] pub fn peek(&self) -> &[u8] { - &self.buffer[self.position..self.capacity] + &self.buffer } /// Returns true if the `peek` buffer contains all of the data in the body @@ -146,7 +140,7 @@ impl Data { /// it does. #[inline(always)] pub fn peek_complete(&self) -> bool { - self.is_done + self.is_complete } /// A helper method to write the body of the request to any `Write` type. @@ -171,40 +165,32 @@ impl Data { // in the buffer is at `pos` and the buffer has `cap` valid bytes. Thus, the // bytes `vec[pos..cap]` are buffered and unread. The remainder of the data // bytes can be read from `stream`. - pub(crate) fn new(mut buf: Vec, - pos: usize, - mut cap: usize, - mut stream: StreamReader - ) -> Data { - // Make sure the buffer is large enough for the bytes we want to peek. - if buf.len() < PEEK_BYTES { - trace_!("Resizing peek buffer from {} to {}.", buf.len(), PEEK_BYTES); - buf.resize(PEEK_BYTES, 0); - } + pub(crate) fn new(mut stream: BodyReader) -> Data { + trace_!("Date::new({:?})", stream); + let mut peek_buf = vec![0; PEEK_BYTES]; // Fill the buffer with as many bytes as possible. If we read less than // that buffer's length, we know we reached the EOF. Otherwise, it's // unclear, so we just say we didn't reach EOF. - trace!("Init buffer cap: {}", cap); - let eof = match stream.read_max(&mut buf[cap..]) { + let eof = match stream.read_max(&mut peek_buf[..]) { Ok(n) => { trace_!("Filled peek buf with {} bytes.", n); - cap += n; - cap < buf.len() + // TODO: Explain this. + unsafe { peek_buf.set_len(n); } + n < PEEK_BYTES } Err(e) => { error_!("Failed to read into peek buffer: {:?}.", e); + unsafe { peek_buf.set_len(0); } false }, }; - trace_!("Peek buffer size: {}, remaining: {}", buf.len(), buf.len() - cap); + trace_!("Peek bytes: {}/{} bytes.", peek_buf.len(), PEEK_BYTES); Data { - buffer: buf, + buffer: peek_buf, stream: stream, - is_done: eof, - position: pos, - capacity: cap, + is_complete: eof, } } @@ -218,24 +204,23 @@ impl Data { (data, rest) }; - let (buf_len, stream_len) = (buf.len(), rest.len() as u64); - let stream = NetStream::Local(Cursor::new(rest)); + let stream_len = rest.len() as u64; + let stream = Cursor::new(vec![]).take(0) + .chain(BufReader::new(NetStream::Local(Cursor::new(rest)))); + Data { buffer: buf, stream: HttpReader::SizedReader(stream, stream_len), - is_done: stream_len == 0, - position: 0, - capacity: buf_len, + is_complete: stream_len == 0, } } } -impl Drop for Data { - fn drop(&mut self) { - // This is okay since the network stream expects to be shared mutably. - unsafe { - let stream: &mut StreamReader = transmute(self.stream.by_ref()); - kill_stream(stream, self.stream.get_mut()); - } - } -} +// impl Drop for Data { +// fn drop(&mut self) { +// // FIXME: Do a read; if > 1024, kill the stream. Need access to the +// // internals of `Chain` to do this efficiently/without crazy baggage. +// // https://github.com/rust-lang/rust/pull/41463 +// let _ = io::copy(&mut self.stream, &mut io::sink()); +// } +// } diff --git a/lib/src/data/data_stream.rs b/lib/src/data/data_stream.rs index c98b8293..58432f14 100644 --- a/lib/src/data/data_stream.rs +++ b/lib/src/data/data_stream.rs @@ -1,13 +1,14 @@ -use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take}; -use std::net::Shutdown; +use std::io::{self, BufRead, Read, Cursor, BufReader, Chain}; -use super::net_stream::NetStream; +use super::data::BodyReader; -use http::hyper::net::NetworkStream; -use http::hyper::h1::HttpReader; - -pub type StreamReader = HttpReader; -pub type InnerStream = Chain>>, BufReader>; +// It's very unfortunate that we have to wrap `BodyReader` in a `BufReader` +// since it already contains another `BufReader`. The issue is that Hyper's +// `HttpReader` doesn't implement `BufRead`. Unfortunately, this will likely +// stay "double buffered" until we switch HTTP libraries. +// |-- peek buf --| +// pub type InnerStream = Chain>, BufReader>; +pub type InnerStream = Chain>, BodyReader>; /// Raw data stream of a request body. /// @@ -15,55 +16,33 @@ pub type InnerStream = Chain>>, BufReader>; /// [Data::open](/rocket/data/struct.Data.html#method.open). The stream contains /// all of the data in the body of the request. It exposes no methods directly. /// Instead, it must be used as an opaque `Read` or `BufRead` structure. -pub struct DataStream { - stream: InnerStream, - network: NetStream, -} - -impl DataStream { - #[inline(always)] - pub(crate) fn new(stream: InnerStream, network: NetStream) -> DataStream { - DataStream { stream, network } - } -} +pub struct DataStream(pub(crate) InnerStream); impl Read for DataStream { #[inline(always)] fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) + trace_!("DataStream::read()"); + self.0.read(buf) } } -impl BufRead for DataStream { - #[inline(always)] - fn fill_buf(&mut self) -> io::Result<&[u8]> { - self.stream.fill_buf() - } +// impl BufRead for DataStream { +// #[inline(always)] +// fn fill_buf(&mut self) -> io::Result<&[u8]> { +// self.0.fill_buf() +// } - #[inline(always)] - fn consume(&mut self, amt: usize) { - self.stream.consume(amt) - } -} +// #[inline(always)] +// fn consume(&mut self, amt: usize) { +// self.0.consume(amt) +// } +// } - -pub fn kill_stream(stream: &mut S, network: &mut N) { - // Take <= 1k from the stream. If there might be more data, force close. - const FLUSH_LEN: u64 = 1024; - match io::copy(&mut stream.take(FLUSH_LEN), &mut io::sink()) { - Ok(FLUSH_LEN) | Err(_) => { - warn_!("Data left unread. Force closing network stream."); - if let Err(e) = network.close(Shutdown::Both) { - error_!("Failed to close network stream: {:?}", e); - } - } - Ok(n) => debug!("flushed {} unread bytes", n) - } -} - -impl Drop for DataStream { - // Be a bad citizen and close the TCP stream if there's unread data. - fn drop(&mut self) { - kill_stream(&mut self.stream, &mut self.network); - } -} +// impl Drop for DataStream { +// fn drop(&mut self) { +// // FIXME: Do a read; if > 1024, kill the stream. Need access to the +// // internals of `Chain` to do this efficiently/without crazy baggage. +// // https://github.com/rust-lang/rust/pull/41463 +// let _ = io::copy(&mut self.0, &mut io::sink()); +// } +// } diff --git a/lib/src/data/net_stream.rs b/lib/src/data/net_stream.rs index 28c3d766..908eb43a 100644 --- a/lib/src/data/net_stream.rs +++ b/lib/src/data/net_stream.rs @@ -20,17 +20,21 @@ pub enum NetStream { impl io::Read for NetStream { #[inline(always)] fn read(&mut self, buf: &mut [u8]) -> io::Result { - match *self { + trace_!("NetStream::read()"); + let res = match *self { Http(ref mut stream) => stream.read(buf), Local(ref mut stream) => stream.read(buf), #[cfg(feature = "tls")] Https(ref mut stream) => stream.read(buf) - } + }; + trace_!("NetStream::read() -- complete"); + res } } impl io::Write for NetStream { #[inline(always)] fn write(&mut self, buf: &[u8]) -> io::Result { + trace_!("NetStream::write()"); match *self { Http(ref mut stream) => stream.write(buf), Local(ref mut stream) => stream.write(buf), @@ -85,3 +89,20 @@ impl NetworkStream for NetStream { } } } + +// impl Drop for NetStream { +// fn drop(&mut self) { +// // Take <= 1k from the stream. If there might be more data, force close. +// trace_!("Dropping the network stream..."); +// // const FLUSH_LEN: u64 = 1024; +// // match io::copy(&mut self.take(FLUSH_LEN), &mut io::sink()) { +// // Ok(FLUSH_LEN) | Err(_) => { +// // warn_!("Data left unread. Force closing network stream."); +// // if let Err(e) = self.close(Shutdown::Both) { +// // error_!("Failed to close network stream: {:?}", e); +// // } +// // } +// // Ok(n) => debug!("flushed {} unread bytes", n) +// // } +// } +// } From 45eb475607f4a79f0f19230648fe18bfbaa53fb3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 22 Apr 2017 22:17:21 -0700 Subject: [PATCH 120/297] Use unboxed WrappedStream. --- lib/src/data/data.rs | 4 ++-- lib/src/data/net_stream.rs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index df51eef9..7662061c 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::fs::File; use std::time::Duration; -#[cfg(feature = "tls")] use hyper_rustls::WrappedStream; +#[cfg(feature = "tls")] use super::net_stream::HttpsStream; use super::data_stream::DataStream; use super::net_stream::NetStream; @@ -82,7 +82,7 @@ impl Data { #[cfg(feature = "tls")] fn concrete_stream(stream: &&mut NetworkStream) -> Option { - stream.downcast_ref::() + stream.downcast_ref::() .map(|s| NetStream::Https(s.clone())) .or_else(|| { stream.downcast_ref::() diff --git a/lib/src/data/net_stream.rs b/lib/src/data/net_stream.rs index 908eb43a..340ed792 100644 --- a/lib/src/data/net_stream.rs +++ b/lib/src/data/net_stream.rs @@ -2,18 +2,20 @@ use std::io::{self, Cursor}; use std::net::{SocketAddr, Shutdown}; use std::time::Duration; -#[cfg(feature = "tls")] use hyper_rustls::WrappedStream as RustlsStream; +#[cfg(feature = "tls")] use hyper_rustls::{WrappedStream, ServerSession}; use http::hyper::net::{HttpStream, NetworkStream}; use self::NetStream::*; +#[cfg(feature = "tls")] pub type HttpsStream = WrappedStream; + // This is a representation of all of the possible network streams we might get. // This really shouldn't be necessary, but, you know, Hyper. #[derive(Clone)] pub enum NetStream { Http(HttpStream), #[cfg(feature = "tls")] - Https(RustlsStream), + Https(HttpsStream), Local(Cursor>) } From 6f5b840d0081f5b13464baf11b5c6b616a02f7d0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 23 Apr 2017 00:03:14 -0700 Subject: [PATCH 121/297] Remove now-unneeded doc shenanigans. --- scripts/mk-docs.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/mk-docs.sh b/scripts/mk-docs.sh index 4d5771ea..2983c819 100755 --- a/scripts/mk-docs.sh +++ b/scripts/mk-docs.sh @@ -12,11 +12,10 @@ source $SCRIPT_DIR/config.sh function mk_doc() { local dir=$1 local flag=$2 - pushd $dir > /dev/null + pushd $dir > /dev/null 2>&1 echo ":: Documenting '${dir}'..." cargo doc --no-deps --all-features - cargo doc --no-deps $flag - popd > /dev/null + popd > /dev/null 2>&1 } # We need to clean-up beforehand so we don't get all of the dependencies. @@ -24,7 +23,7 @@ cargo clean mk_doc $LIB_DIR mk_doc $CODEGEN_DIR -mk_doc $CONTRIB_DIR --all-features +mk_doc $CONTRIB_DIR # Blank index, for redirection. touch ${DOC_DIR}/index.html From 423acdd32a05ff83ad8cb310fe3fcae32c0d4fb8 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 23 Apr 2017 20:27:34 -0700 Subject: [PATCH 122/297] Use Reilly's full name in news article. --- site/news/2017-02-06-version-0.2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/news/2017-02-06-version-0.2.md b/site/news/2017-02-06-version-0.2.md index aa17f036..2bfd87c0 100644 --- a/site/news/2017-02-06-version-0.2.md +++ b/site/news/2017-02-06-version-0.2.md @@ -371,7 +371,7 @@ The following wonderful people helped make Rocket v0.2 happen:
  • Lori Holden
  • Marcus Ball
  • Matt McCoy
  • -
  • Reilly Tucker
  • +
  • Reilly Tucker Siemens
  • Robert Balicki
  • Sean Griffin
  • Seth Lopez
  • From 40d11929d763982f807777a77337ed7144cfc2d3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 24 Apr 2017 01:33:00 -0700 Subject: [PATCH 123/297] Optimize the creation of the Data structure. --- lib/src/data/data.rs | 51 +++++++++++++++++++------------------- lib/src/rocket.rs | 1 + lib/src/router/collider.rs | 13 ++++++---- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 7662061c..5061686c 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -18,10 +18,10 @@ pub type HyperBodyReader<'a, 'b> = self::HttpReader<&'a mut hyper::buffer::BufReader<&'b mut NetworkStream>>; // |---- from hyper ----| -pub type BodyReader = HttpReader>>, BufReader>>; +pub type BodyReader = HttpReader>, NetStream>>; /// The number of bytes to read into the "peek" buffer. -const PEEK_BYTES: usize = 4096; +const PEEK_BYTES: usize = 512; /// Type representing the data in the body of an incoming request. /// @@ -48,7 +48,7 @@ const PEEK_BYTES: usize = 4096; /// object ensures that holding a `Data` object means that all of the data is /// available for reading. /// -/// The `peek` method returns a slice containing at most 4096 bytes of buffered +/// The `peek` method returns a slice containing at most 512 bytes of buffered /// body data. This enables partially or fully reading from a `Data` object /// without consuming the `Data` object. pub struct Data { @@ -66,9 +66,12 @@ impl Data { /// the data in a request. pub fn open(mut self) -> DataStream { let buffer = ::std::mem::replace(&mut self.buffer, vec![]); - let empty_stream = Cursor::new(vec![]).take(0) - .chain(BufReader::new(NetStream::Local(Cursor::new(vec![])))); + let empty_stream = Cursor::new(vec![]) + .chain(NetStream::Local(Cursor::new(vec![]))); + // FIXME: Insert a `BufReader` in front of the `NetStream` with capacity + // 4096. We need the new `Chain` methods to get the inner reader to + // actually do this, however. let empty_http_stream = HttpReader::SizedReader(empty_stream, 0); let stream = ::std::mem::replace(&mut self.stream, empty_http_stream); DataStream(Cursor::new(buffer).chain(stream)) @@ -77,10 +80,12 @@ impl Data { // FIXME: This is absolutely terrible (downcasting!), thanks to Hyper. pub(crate) fn from_hyp(mut body: HyperBodyReader) -> Result { // Steal the internal, undecoded data buffer and net stream from Hyper. - let (hyper_buf, pos, cap) = body.get_mut().take_buf(); + let (mut hyper_buf, pos, cap) = body.get_mut().take_buf(); + unsafe { hyper_buf.set_len(cap); } let hyper_net_stream = body.get_ref().get_ref(); #[cfg(feature = "tls")] + #[inline(always)] fn concrete_stream(stream: &&mut NetworkStream) -> Option { stream.downcast_ref::() .map(|s| NetStream::Https(s.clone())) @@ -91,6 +96,7 @@ impl Data { } #[cfg(not(feature = "tls"))] + #[inline(always)] fn concrete_stream(stream: &&mut NetworkStream) -> Option { stream.downcast_ref::() .map(|s| NetStream::Http(s.clone())) @@ -107,11 +113,10 @@ impl Data { // TODO: Explain this. trace_!("Hyper buffer: [{}..{}] ({} bytes).", pos, cap, cap - pos); - let (start, remaining) = (pos as u64, (cap - pos) as u64); + let mut cursor = Cursor::new(hyper_buf); - cursor.set_position(start); - let inner_data = cursor.take(remaining) - .chain(BufReader::new(net_stream.clone())); + cursor.set_position(pos as u64); + let inner_data = cursor.chain(net_stream); // Create an HTTP reader from the stream. let http_stream = match body { @@ -132,7 +137,11 @@ impl Data { /// buffer contains _all_ of the data in the body of the request. #[inline(always)] pub fn peek(&self) -> &[u8] { - &self.buffer + if self.buffer.len() > PEEK_BYTES { + &self.buffer[..PEEK_BYTES] + } else { + &self.buffer + } } /// Returns true if the `peek` buffer contains all of the data in the body @@ -165,6 +174,7 @@ impl Data { // in the buffer is at `pos` and the buffer has `cap` valid bytes. Thus, the // bytes `vec[pos..cap]` are buffered and unread. The remainder of the data // bytes can be read from `stream`. + #[inline(always)] pub(crate) fn new(mut stream: BodyReader) -> Data { trace_!("Date::new({:?})", stream); let mut peek_buf = vec![0; PEEK_BYTES]; @@ -196,22 +206,13 @@ impl Data { /// This creates a `data` object from a local data source `data`. pub(crate) fn local(mut data: Vec) -> Data { - // Emulate peek buffering. - let (buf, rest) = if data.len() <= PEEK_BYTES { - (data, vec![]) - } else { - let rest = data.split_off(PEEK_BYTES); - (data, rest) - }; - - let stream_len = rest.len() as u64; - let stream = Cursor::new(vec![]).take(0) - .chain(BufReader::new(NetStream::Local(Cursor::new(rest)))); + let empty_stream = Cursor::new(vec![]) + .chain(NetStream::Local(Cursor::new(vec![]))); Data { - buffer: buf, - stream: HttpReader::SizedReader(stream, stream_len), - is_complete: stream_len == 0, + buffer: data, + stream: HttpReader::SizedReader(empty_stream, 0), + is_complete: true, } } } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 754cf213..b27dba0a 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -116,6 +116,7 @@ impl Rocket { } } + #[inline] fn write_response(&self, mut response: Response, mut hyp_res: hyper::FreshResponse) -> io::Result<()> { diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index a38b408c..63f17715 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -11,7 +11,8 @@ pub trait Collider { fn collides_with(&self, other: &T) -> bool; } -pub fn index_match_until(break_c: char, +#[inline(always)] +fn index_match_until(break_c: char, a: &str, b: &str, dir: bool) @@ -39,11 +40,13 @@ pub fn index_match_until(break_c: char, Some((i, j)) } +#[inline(always)] fn do_match_until(break_c: char, a: &str, b: &str, dir: bool) -> bool { index_match_until(break_c, a, b, dir).is_some() } impl<'a> Collider for &'a str { + #[inline(always)] fn collides_with(&self, other: &str) -> bool { let (a, b) = (self, other); do_match_until('<', a, b, true) && do_match_until('>', a, b, false) @@ -72,6 +75,7 @@ impl<'a, 'b> Collider> for URI<'a> { } impl Collider for MediaType { + #[inline(always)] fn collides_with(&self, other: &MediaType) -> bool { let collide = |a, b| a == "*" || b == "*" || a == b; collide(self.top(), other.top()) && collide(self.sub(), other.sub()) @@ -113,10 +117,9 @@ impl<'r> Collider> for Route { self.method == req.method() && self.path.collides_with(req.uri()) && self.path.query().map_or(true, |_| req.uri().query().is_some()) - // FIXME: Avoid calling `format` is `self.format` == None. - && match self.format.as_ref() { - Some(mt_a) => match req.format().as_ref() { - Some(mt_b) => mt_a.collides_with(mt_b), + && match self.format { + Some(ref mt_a) => match req.format() { + Some(ref mt_b) => mt_a.collides_with(mt_b), None => false }, None => true From 30fac3297810b348a4f3e43d01a937783f0baad2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 24 Apr 2017 17:37:18 -0700 Subject: [PATCH 124/297] Upgrade dependencies to Serde 1.0. Closes #272. Resolves #273. --- contrib/Cargo.toml | 10 +++++----- contrib/src/json.rs | 11 ++++++----- contrib/src/msgpack.rs | 10 ++++++---- examples/content_types/Cargo.toml | 6 +++--- examples/handlebars_templates/Cargo.toml | 6 +++--- examples/json/Cargo.toml | 6 +++--- examples/msgpack/Cargo.toml | 4 ++-- examples/todo/Cargo.toml | 6 +++--- lib/src/data/data.rs | 4 ++-- lib/src/data/data_stream.rs | 2 +- 10 files changed, 34 insertions(+), 31 deletions(-) diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 73cafa1c..426347b0 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -29,12 +29,12 @@ log = "^0.3" uuid = { version = "^0.4", optional = true } # Serialization and templating dependencies. -serde = { version = "^0.9", optional = true } -serde_json = { version = "^0.9.3", optional = true } -rmp-serde = { version = "^0.12", optional = true } +serde = { version = "1.0", optional = true } +serde_json = { version = "1.0", optional = true } +rmp-serde = { version = "^0.13", optional = true } # Templating dependencies only. -handlebars = { version = "^0.25", optional = true, features = ["serde_type"] } +handlebars = { version = "^0.26.1", optional = true } glob = { version = "^0.2", optional = true } lazy_static = { version = "^0.2", optional = true } -tera = { version = "^0.8", optional = true } +tera = { version = "^0.10", optional = true } diff --git a/contrib/src/json.rs b/contrib/src/json.rs index 3c7b412a..75d1115d 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -8,7 +8,8 @@ use rocket::data::{self, Data, FromData}; use rocket::response::{self, Responder, content}; use rocket::http::Status; -use serde::{Serialize, Deserialize}; +use serde::Serialize; +use serde::de::DeserializeOwned; use serde_json; @@ -22,9 +23,9 @@ pub use serde_json::error::Error as SerdeError; /// /// If you're receiving JSON data, simply add a `data` parameter to your route /// arguments and ensure the type of the parameter is a `JSON`, where `T` is -/// some type you'd like to parse from JSON. `T` must implement `Deserialize` -/// from [Serde](https://github.com/serde-rs/json). The data is parsed from the -/// HTTP request body. +/// some type you'd like to parse from JSON. `T` must implement `Deserialize` or +/// `DeserializeOwned` from [Serde](https://github.com/serde-rs/json). The data +/// is parsed from the HTTP request body. /// /// ```rust,ignore /// #[post("/users/", format = "application/json", data = "")] @@ -87,7 +88,7 @@ impl JSON { /// Default limit for JSON is 1MB. const LIMIT: u64 = 1 << 20; -impl FromData for JSON { +impl FromData for JSON { type Error = SerdeError; fn from_data(request: &Request, data: Data) -> data::Outcome { diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index f8a47d1d..f95da24e 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -10,7 +10,8 @@ use rocket::data::{self, Data, FromData}; use rocket::response::{self, Responder, Response}; use rocket::http::{ContentType, Status}; -use serde::{Serialize, Deserialize}; +use serde::Serialize; +use serde::de::DeserializeOwned; pub use self::rmp_serde::decode::Error as MsgPackError; @@ -22,8 +23,9 @@ pub use self::rmp_serde::decode::Error as MsgPackError; /// If you're receiving MessagePack data, simply add a `data` parameter to your /// route arguments and ensure the type of the parameter is a `MsgPack`, /// where `T` is some type you'd like to parse from MessagePack. `T` must -/// implement `Deserialize` from [Serde](https://github.com/serde-rs/serde). The -/// data is parsed from the HTTP request body. +/// implement `Deserialize` or `DeserializeOwned` from +/// [Serde](https://github.com/serde-rs/serde). The data is parsed from the HTTP +/// request body. /// /// ```rust,ignore /// #[post("/users/", format = "application/msgpack", data = "")] @@ -100,7 +102,7 @@ fn is_msgpack_content_type(ct: &ContentType) -> bool { && (ct.sub() == "msgpack" || ct.sub() == "x-msgpack") } -impl FromData for MsgPack { +impl FromData for MsgPack { type Error = MsgPackError; fn from_data(request: &Request, data: Data) -> data::Outcome { diff --git a/examples/content_types/Cargo.toml b/examples/content_types/Cargo.toml index d999bedd..10d40c78 100644 --- a/examples/content_types/Cargo.toml +++ b/examples/content_types/Cargo.toml @@ -6,6 +6,6 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -serde = "0.9" -serde_json = "0.9" -serde_derive = "0.9" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" diff --git a/examples/handlebars_templates/Cargo.toml b/examples/handlebars_templates/Cargo.toml index 5384c9e4..136e9e61 100644 --- a/examples/handlebars_templates/Cargo.toml +++ b/examples/handlebars_templates/Cargo.toml @@ -6,9 +6,9 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -serde = "0.9" -serde_derive = "0.9" -serde_json = "0.9" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" [dependencies.rocket_contrib] path = "../../contrib" diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml index 5e96c61a..aa2cf6bf 100644 --- a/examples/json/Cargo.toml +++ b/examples/json/Cargo.toml @@ -6,9 +6,9 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -serde = "0.9" -serde_json = "0.9" -serde_derive = "0.9" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" [dependencies.rocket_contrib] path = "../../contrib" diff --git a/examples/msgpack/Cargo.toml b/examples/msgpack/Cargo.toml index e2c904dd..9e329ca4 100644 --- a/examples/msgpack/Cargo.toml +++ b/examples/msgpack/Cargo.toml @@ -6,8 +6,8 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -serde = "0.9" -serde_derive = "0.9" +serde = "1.0" +serde_derive = "1.0" [dependencies.rocket_contrib] path = "../../contrib" diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index 2aa64a7c..28d3cf5c 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -6,9 +6,9 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -serde = "0.9" -serde_json = "0.9" -serde_derive = "0.9" +serde = "1.0" +serde_json = "1.0" +serde_derive = "1.0" r2d2 = "0.7" diesel = { version = "0.12", features = ["sqlite"] } diesel_codegen = { version = "0.12", features = ["sqlite"] } diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 5061686c..24af63d9 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -1,4 +1,4 @@ -use std::io::{self, Read, Write, Cursor, BufReader, Chain, Take}; +use std::io::{self, Read, Write, Cursor, Chain}; use std::path::Path; use std::fs::File; use std::time::Duration; @@ -205,7 +205,7 @@ impl Data { } /// This creates a `data` object from a local data source `data`. - pub(crate) fn local(mut data: Vec) -> Data { + pub(crate) fn local(data: Vec) -> Data { let empty_stream = Cursor::new(vec![]) .chain(NetStream::Local(Cursor::new(vec![]))); diff --git a/lib/src/data/data_stream.rs b/lib/src/data/data_stream.rs index 58432f14..6e6e8731 100644 --- a/lib/src/data/data_stream.rs +++ b/lib/src/data/data_stream.rs @@ -1,4 +1,4 @@ -use std::io::{self, BufRead, Read, Cursor, BufReader, Chain}; +use std::io::{self, Read, Cursor, Chain}; use super::data::BodyReader; From 13061e9062c3eb8c20e35e2e53c67c319a148f68 Mon Sep 17 00:00:00 2001 From: Lance Carlson Date: Wed, 3 May 2017 12:03:25 -0400 Subject: [PATCH 125/297] Update uuid dependency to 0.5. --- contrib/Cargo.toml | 2 +- examples/uuid/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index 426347b0..ed29af91 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -26,7 +26,7 @@ rocket = { version = "0.2.6", path = "../lib/" } log = "^0.3" # UUID dependencies. -uuid = { version = "^0.4", optional = true } +uuid = { version = "^0.5", optional = true } # Serialization and templating dependencies. serde = { version = "1.0", optional = true } diff --git a/examples/uuid/Cargo.toml b/examples/uuid/Cargo.toml index 28841e1e..9a557dbc 100644 --- a/examples/uuid/Cargo.toml +++ b/examples/uuid/Cargo.toml @@ -6,7 +6,7 @@ workspace = "../../" [dependencies] rocket = { path = "../../lib" } rocket_codegen = { path = "../../codegen" } -uuid = "^0.4" +uuid = "^0.5" lazy_static = "^0.2" [dependencies.rocket_contrib] From f3f2803b0e8337e4e873bb773ec6873d442afd34 Mon Sep 17 00:00:00 2001 From: alexey zabelin Date: Wed, 3 May 2017 17:38:08 -0400 Subject: [PATCH 126/297] Fix typo in requests guide. --- site/guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/guide/requests.md b/site/guide/requests.md index 57b5dec1..f2423add 100644 --- a/site/guide/requests.md +++ b/site/guide/requests.md @@ -255,7 +255,7 @@ Fields of forms can be easily validated via implementations of the `FromFormValue` trait. For example, if you'd like to verify that some user is over some age in a form, then you might define a new `AdultAge` type, use it as a field in a form structure, and implement `FromFormValue` so that it only -validates integers over that age. If a form is a submitted with a bad age, +validates integers over that age. If a form is submitted with a bad age, Rocket won't call a handler requiring a valid form for that structure. You can use `Option` or `Result` types for fields to catch parse failures. From 82c52c903a57d7f5cff0d6cce1b845848ebd2b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wa=C5=82kuski?= Date: Sat, 29 Apr 2017 20:26:27 +0200 Subject: [PATCH 127/297] Fix typo in requests guide: forgot 'be'. --- site/guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/guide/requests.md b/site/guide/requests.md index f2423add..d61d7767 100644 --- a/site/guide/requests.md +++ b/site/guide/requests.md @@ -155,7 +155,7 @@ fn get_page(path: PathBuf) -> T { ... } The path after `/page/` will be available in the `path` parameter. The `FromSegments` implementation for `PathBuf` ensures that `path` cannot lead to [path traversal attacks](https://www.owasp.org/index.php/Path_Traversal). With -this, a safe and secure static file server can implemented in 4 lines: +this, a safe and secure static file server can be implemented in 4 lines: ```rust #[get("/")] From 18f8a9dd5c51ce3689bd89cccd0a9a9ad71e9d6e Mon Sep 17 00:00:00 2001 From: Ivar Abrahamsen Date: Sun, 30 Apr 2017 22:32:21 +0100 Subject: [PATCH 128/297] Fix 'hit_count' parameter name in state guide. Updated parameter names for 'count' to 'hit_count' as code inside functions expect 'hit_count'. This also mirrors the example project code names. --- site/guide/state.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/guide/state.md b/site/guide/state.md index fff7fcef..72e1324f 100644 --- a/site/guide/state.md +++ b/site/guide/state.md @@ -54,7 +54,7 @@ current `HitCount` in a `count` route as follows: ```rust #[get("/count")] -fn count(count: State) -> String { +fn count(hit_count: State) -> String { let current_count = hit_count.0.load(Ordering::Relaxed); format!("Number of visits: {}", current_count) } @@ -64,7 +64,7 @@ You can retrieve more than one `State` type in a single route as well: ```rust #[get("/state")] -fn state(count: State, config: State) -> T { ... } +fn state(hit_count: State, config: State) -> T { ... } ``` It can also be useful to retrieve managed state from a `FromRequest` @@ -99,7 +99,7 @@ type from previous examples: ```rust #[get("/count")] -fn count(count: State) -> String { +fn count(hit_count: State) -> String { let current_count = hit_count.0.load(Ordering::Relaxed); format!("Number of visits: {}", current_count) } @@ -119,7 +119,7 @@ this application, Rocket emits the following warning: warning: HitCount is not currently being managed by Rocket --> src/main.rs:2:17 | -2 | fn count(count: State) -> String { +2 | fn count(hit_count: State) -> String { | ^^^^^^^^^^^^^^^ | = note: this State request guard will always fail From 84255af4f9b348371ce4b15fc7278a9a5281866c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 5 May 2017 12:40:57 -0700 Subject: [PATCH 129/297] Update base64 dependency to 0.5. --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8cda74a2..ab87de46 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -26,7 +26,7 @@ num_cpus = "1" state = "0.2.1" time = "0.1" memchr = "1" -base64 = "0.4" +base64 = "0.5" smallvec = "0.3.3" pear = "0.0.8" pear_codegen = "0.0.8" From 93ace71a50693dd04da5c191d53d567ceea1524e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 5 May 2017 18:47:16 -0700 Subject: [PATCH 130/297] Ask Travis for a Ubuntu 14.04 box for more memory. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 22adfdd3..98cf7d6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: rust sudo: required # so we get a VM with higher specs +dist: trusty # so we get a VM with higher specs cache: cargo rust: - nightly From 6907fd432c8262171d9b796bbf2167a7b1b7fbf7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 8 May 2017 15:28:46 -0700 Subject: [PATCH 131/297] Update base64 to secure version. --- lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ab87de46..d7c3dc1f 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -26,7 +26,7 @@ num_cpus = "1" state = "0.2.1" time = "0.1" memchr = "1" -base64 = "0.5" +base64 = "0.5.2" smallvec = "0.3.3" pear = "0.0.8" pear_codegen = "0.0.8" From 5f0fbf277ce2362b4e655dbf68f4edf7c7495b7f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 11 May 2017 13:58:01 -0700 Subject: [PATCH 132/297] Ask for OS information on issue template. --- .github/ISSUE_TEMPLATE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4dbcb9f3..ae161595 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -17,14 +17,16 @@ Bug reports _must_ include: 1. The version of Rocket you're using. Ensure it's the latest, if possible. - 2. A brief description of the bug that includes: + 2. The operating system (distribution and version) where the issue occurs. + + 3. A brief description of the bug that includes: * The nature of the bug. * When the bug occurs. * What you expected vs. what actually happened. - 3. How you discovered the bug. Short test cases are especially useful. + 4. How you uncovered the bug. Short, reproducible tests are especially useful. - 4. Ideas, if any, about what Rocket is doing incorrectly. + 5. Ideas, if any, about what Rocket is doing incorrectly. ## Questions From a9d9ef386700ae367b5dc6b8b94ae624330b5f58 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 12 May 2017 14:36:32 -0700 Subject: [PATCH 133/297] Update rustls, cookie, and hyper-rustls dependencies. --- lib/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d7c3dc1f..38b15483 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -30,12 +30,13 @@ base64 = "0.5.2" smallvec = "0.3.3" pear = "0.0.8" pear_codegen = "0.0.8" -rustls = { version = "0.5.8", optional = true } -cookie = { version = "0.7.5", features = ["percent-encode", "secure"] } +rustls = { version = "0.7.0", optional = true } +cookie = { version = "0.8.1", features = ["percent-encode", "secure"] } hyper = { version = "0.10.9", default-features = false } [dependencies.hyper-rustls] git = "https://github.com/SergioBenitez/hyper-rustls" +rev = "94e2bf18" default-features = false features = ["server"] optional = true From 5e345e99d0e8769ea537589777f58e9c904eeaea Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 12 May 2017 14:38:18 -0700 Subject: [PATCH 134/297] Make I/O and parsing TLS file errors distinct. --- lib/src/config/config.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 6b88f056..323ed582 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -382,12 +382,24 @@ impl Config { #[cfg(feature = "tls")] pub fn set_tls(&mut self, certs_path: &str, key_path: &str) -> Result<()> { use hyper_rustls::util as tls; + use hyper_rustls::util::Error::Io; - let err = "nonexistent or invalid file"; + let io_err = "nonexistent or unreadable file"; + let pem_err = "malformed PEM file"; + + // Load the certificates. let certs = tls::load_certs(certs_path) - .map_err(|_| self.bad_type("tls", err, "a readable certificates file"))?; + .map_err(|e| match e { + Io(_) => self.bad_type("tls", io_err, "a valid certificates file"), + _ => self.bad_type("tls", pem_err, "a valid certificates file") + })?; + + // And now the private key. let key = tls::load_private_key(key_path) - .map_err(|_| self.bad_type("tls", err, "a readable private key file"))?; + .map_err(|e| match e { + Io(_) => self.bad_type("tls", io_err, "a valid private key file"), + _ => self.bad_type("tls", pem_err, "a valid private key file") + })?; self.tls = Some(TlsConfig { certs, key }); Ok(()) From 781477fff1d1983897b35dad49c74e925185289c Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 30 Mar 2017 22:44:51 +0200 Subject: [PATCH 135/297] Rename 'session_key' config parameter to 'secret_key'. Resolves #220. --- CHANGELOG.md | 2 +- examples/config/Rocket.toml | 6 ++--- examples/session/Rocket.toml | 4 ++-- lib/src/config/builder.rs | 22 +++++++++--------- lib/src/config/config.rs | 40 ++++++++++++++++----------------- lib/src/config/custom_values.rs | 14 ++++++------ lib/src/config/mod.rs | 38 +++++++++++++++---------------- lib/src/rocket.rs | 4 ++-- 8 files changed, 65 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d3ae1c..bed58aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -216,7 +216,7 @@ In addition to new features, Rocket saw the following smaller improvements: * Clippy issues injected by codegen are resolved. * Handlebars was updated to `0.25`. * The `PartialEq` implementation of `Config` doesn't consider the path or - session key. + secret key. * Hyper dependency updated to `0.10`. * The `Error` type for `JSON as FromData` has been exposed as `SerdeError`. * SVG was added as a known Content-Type. diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index 4002abf6..d55f482f 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -1,4 +1,4 @@ -# Except for the session key, nothing here is necessary; Rocket has sane +# Except for the secret key, none of these are actually needed; Rocket has sane # defaults. We show all of them here explicitly for demonstrative purposes. [global.limits] @@ -20,7 +20,7 @@ port = 80 log = "normal" workers = 8 # don't use this key! generate your own and keep it private! -session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" +secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" [production] address = "0.0.0.0" @@ -28,4 +28,4 @@ port = 80 workers = 12 log = "critical" # don't use this key! generate your own and keep it private! -session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" +secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" diff --git a/examples/session/Rocket.toml b/examples/session/Rocket.toml index d9c2fbaa..42a09793 100644 --- a/examples/session/Rocket.toml +++ b/examples/session/Rocket.toml @@ -1,7 +1,7 @@ [staging] -session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" +secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" address = "localhost" port = 8000 [production] -session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" +secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" diff --git a/lib/src/config/builder.rs b/lib/src/config/builder.rs index 5916a9d1..7ecc68a7 100644 --- a/lib/src/config/builder.rs +++ b/lib/src/config/builder.rs @@ -18,8 +18,8 @@ pub struct ConfigBuilder { pub workers: u16, /// How much information to log. pub log_level: LoggingLevel, - /// The session key. - pub session_key: Option, + /// The secret key. + pub secret_key: Option, /// TLS configuration (path to certificates file, path to private key file). pub tls: Option<(String, String)>, /// Size limits. @@ -66,7 +66,7 @@ impl ConfigBuilder { port: config.port, workers: config.workers, log_level: config.log_level, - session_key: None, + secret_key: None, tls: None, limits: config.limits, extras: config.extras, @@ -150,7 +150,7 @@ impl ConfigBuilder { self } - /// Sets the `session_key` in the configuration being built. + /// Sets the `secret_key` in the configuration being built. /// /// # Example /// @@ -160,11 +160,11 @@ impl ConfigBuilder { /// /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; /// let mut config = Config::build(Environment::Staging) - /// .session_key(key) + /// .secret_key(key) /// .unwrap(); /// ``` - pub fn session_key>(mut self, key: K) -> Self { - self.session_key = Some(key.into()); + pub fn secret_key>(mut self, key: K) -> Self { + self.secret_key = Some(key.into()); self } @@ -271,7 +271,7 @@ impl ConfigBuilder { /// # Errors /// /// If the current working directory cannot be retrieved, returns a `BadCWD` - /// error. If the address or session key fail to parse, returns a `BadType` + /// error. If the address or secret key fail to parse, returns a `BadType` /// error. /// /// # Example @@ -307,8 +307,8 @@ impl ConfigBuilder { config.set_tls(&certs_path, &key_path)?; } - if let Some(key) = self.session_key { - config.set_session_key(key)?; + if let Some(key) = self.secret_key { + config.set_secret_key(key)?; } Ok(config) @@ -319,7 +319,7 @@ impl ConfigBuilder { /// # Panics /// /// Panics if the current working directory cannot be retrieved or if the - /// supplied address or session key fail to parse. + /// supplied address or secret key fail to parse. /// /// # Example /// diff --git a/lib/src/config/config.rs b/lib/src/config/config.rs index 323ed582..ba5c3441 100644 --- a/lib/src/config/config.rs +++ b/lib/src/config/config.rs @@ -40,8 +40,8 @@ pub struct Config { pub workers: u16, /// How much information to log. pub log_level: LoggingLevel, - /// The session key. - pub(crate) session_key: SessionKey, + /// The secret key. + pub(crate) secret_key: SecretKey, /// TLS configuration. pub(crate) tls: Option, /// Streaming read size limits. @@ -131,8 +131,8 @@ impl Config { // Note: This may truncate if num_cpus::get() > u16::max. That's okay. let default_workers = ::std::cmp::max(num_cpus::get(), 2) as u16; - // Use a generated session key by default. - let key = SessionKey::Generated(Key::generate()); + // Use a generated secret key by default. + let key = SecretKey::Generated(Key::generate()); Ok(match env { Development => { @@ -142,7 +142,7 @@ impl Config { port: 8000, workers: default_workers, log_level: LoggingLevel::Normal, - session_key: key, + secret_key: key, tls: None, limits: Limits::default(), extras: HashMap::new(), @@ -156,7 +156,7 @@ impl Config { port: 80, workers: default_workers, log_level: LoggingLevel::Normal, - session_key: key, + secret_key: key, tls: None, limits: Limits::default(), extras: HashMap::new(), @@ -170,7 +170,7 @@ impl Config { port: 80, workers: default_workers, log_level: LoggingLevel::Critical, - session_key: key, + secret_key: key, tls: None, limits: Limits::default(), extras: HashMap::new(), @@ -192,7 +192,7 @@ impl Config { } /// Sets the configuration `val` for the `name` entry. If the `name` is one - /// of "address", "port", "session_key", "log", or "workers" (the "default" + /// of "address", "port", "secret_key", "log", or "workers" (the "default" /// values), the appropriate value in the `self` Config structure is set. /// Otherwise, the value is stored as an `extra`. /// @@ -204,7 +204,7 @@ impl Config { /// * **port**: Integer (16-bit unsigned) /// * **workers**: Integer (16-bit unsigned) /// * **log**: String - /// * **session_key**: String (192-bit base64) + /// * **secret_key**: String (192-bit base64) /// * **tls**: Table (`certs` (path as String), `key` (path as String)) pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> { let (id, ok) = (|val| val, |_| Ok(())); @@ -212,7 +212,7 @@ impl Config { address => (str, set_address, id), port => (u16, set_port, ok), workers => (u16, set_workers, ok), - session_key => (str, set_session_key, id), + secret_key => (str, set_secret_key, id), log => (log_level, set_log_level, ok), tls => (tls_config, set_raw_tls, id), limits => (limits, set_limits, ok) @@ -313,7 +313,7 @@ impl Config { self.workers = workers; } - /// Sets the `session_key` in `self` to `key` which must be a 192-bit base64 + /// Sets the `secret_key` in `self` to `key` which must be a 192-bit base64 /// encoded string. /// /// # Errors @@ -330,14 +330,14 @@ impl Config { /// # fn config_test() -> Result<(), ConfigError> { /// let mut config = Config::new(Environment::Staging)?; /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; - /// assert!(config.set_session_key(key).is_ok()); - /// assert!(config.set_session_key("hello? anyone there?").is_err()); + /// assert!(config.set_secret_key(key).is_ok()); + /// assert!(config.set_secret_key("hello? anyone there?").is_err()); /// # Ok(()) /// # } /// ``` - pub fn set_session_key>(&mut self, key: K) -> Result<()> { + pub fn set_secret_key>(&mut self, key: K) -> Result<()> { let key = key.into(); - let error = self.bad_type("session_key", "string", + let error = self.bad_type("secret_key", "string", "a 256-bit base64 encoded string"); if key.len() != 44 { @@ -349,7 +349,7 @@ impl Config { Err(_) => return Err(error) }; - self.session_key = SessionKey::Provided(Key::from_master(&bytes)); + self.secret_key = SecretKey::Provided(Key::from_master(&bytes)); Ok(()) } @@ -478,10 +478,10 @@ impl Config { self.extras.iter().map(|(k, v)| (k.as_str(), v)) } - /// Retrieves the session key from `self`. + /// Retrieves the secret key from `self`. #[inline] - pub(crate) fn session_key(&self) -> &Key { - self.session_key.inner() + pub(crate) fn secret_key(&self) -> &Key { + self.secret_key.inner() } /// Attempts to retrieve the extra named `name` as a string. @@ -668,7 +668,7 @@ impl fmt::Debug for Config { } } -/// Doesn't consider the session key or config path. +/// Doesn't consider the secret key or config path. impl PartialEq for Config { fn eq(&self, other: &Config) -> bool { self.address == other.address diff --git a/lib/src/config/custom_values.rs b/lib/src/config/custom_values.rs index c534e669..0860de5e 100644 --- a/lib/src/config/custom_values.rs +++ b/lib/src/config/custom_values.rs @@ -7,24 +7,24 @@ use config::{Result, Config, Value, ConfigError}; use http::Key; #[derive(Clone)] -pub enum SessionKey { +pub enum SecretKey { Generated(Key), Provided(Key) } -impl SessionKey { - #[inline(always)] +impl SecretKey { + #[inline] pub fn kind(&self) -> &'static str { match *self { - SessionKey::Generated(_) => "generated", - SessionKey::Provided(_) => "provided", + SecretKey::Generated(_) => "generated", + SecretKey::Provided(_) => "provided", } } - #[inline(always)] + #[inline] pub(crate) fn inner(&self) -> &Key { match *self { - SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key + SecretKey::Generated(ref key) | SecretKey::Provided(ref key) => key } } } diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 866c6c17..6c2dd2aa 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -40,8 +40,8 @@ //! * examples: `12`, `1`, `4` //! * **log**: _[string]_ how much information to log; one of `"normal"`, //! `"debug"`, or `"critical"` -//! * **session_key**: _[string]_ a 256-bit base64 encoded string (44 -//! characters) to use as the session key +//! * **secret_key**: _[string]_ a 256-bit base64 encoded string (44 +//! characters) to use as the secret key //! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="` //! * **tls**: _[table]_ a table with two keys: 1) `certs`: _[string]_ a path //! to a certificate chain in PEM format, and 2) `key`: _[string]_ a path to a @@ -71,7 +71,7 @@ //! port = 8000 //! workers = max(number_of_cpus, 2) //! log = "normal" -//! session_key = [randomly generated at launch] +//! secret_key = [randomly generated at launch] //! limits = { forms = 32768 } //! //! [staging] @@ -79,7 +79,7 @@ //! port = 80 //! workers = max(number_of_cpus, 2) //! log = "normal" -//! session_key = [randomly generated at launch] +//! secret_key = [randomly generated at launch] //! limits = { forms = 32768 } //! //! [production] @@ -87,14 +87,14 @@ //! port = 80 //! workers = max(number_of_cpus, 2) //! log = "critical" -//! session_key = [randomly generated at launch] +//! secret_key = [randomly generated at launch] //! limits = { forms = 32768 } //! ``` //! -//! The `workers` and `session_key` default parameters are computed by Rocket +//! The `workers` and `secret_key` default parameters are computed by Rocket //! automatically; the values above are not valid TOML syntax. When manually //! specifying the number of workers, the value should be an integer: `workers = -//! 10`. When manually specifying the session key, the value should a 256-bit +//! 10`. When manually specifying the secret key, the value should a 256-bit //! base64 encoded string. Such a string can be generated with the `openssl` //! command line tool: `openssl rand -base64 32`. //! @@ -634,7 +634,7 @@ mod test { port = 7810 workers = 21 log = "critical" - session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" + secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" template_dir = "mine" json = true pi = 3.14 @@ -645,7 +645,7 @@ mod test { .port(7810) .workers(21) .log_level(LoggingLevel::Critical) - .session_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=") + .secret_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=") .extra("template_dir", "mine") .extra("json", true) .extra("pi", 3.14); @@ -971,49 +971,49 @@ mod test { } #[test] - fn test_good_session_key() { + fn test_good_secret_key() { // Take the lock so changing the environment doesn't cause races. let _env_lock = ENV_LOCK.lock().unwrap(); env::set_var(CONFIG_ENV, "stage"); check_config!(RocketConfig::parse(r#" [stage] - session_key = "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" + secret_key = "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).session_key( + default_config(Staging).secret_key( "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" ) }); check_config!(RocketConfig::parse(r#" [stage] - session_key = "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" + secret_key = "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).session_key( + default_config(Staging).secret_key( "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" ) }); } #[test] - fn test_bad_session_key() { + fn test_bad_secret_key() { // Take the lock so changing the environment doesn't cause races. let _env_lock = ENV_LOCK.lock().unwrap(); env::remove_var(CONFIG_ENV); assert!(RocketConfig::parse(r#" [dev] - session_key = true + secret_key = true "#.to_string(), TEST_CONFIG_FILENAME).is_err()); assert!(RocketConfig::parse(r#" [dev] - session_key = 1283724897238945234897 + secret_key = 1283724897238945234897 "#.to_string(), TEST_CONFIG_FILENAME).is_err()); assert!(RocketConfig::parse(r#" [dev] - session_key = "abcv" + secret_key = "abcv" "#.to_string(), TEST_CONFIG_FILENAME).is_err()); } @@ -1034,7 +1034,7 @@ mod test { assert!(RocketConfig::parse(r#" [dev] - session_key = "abcv" = other + secret_key = "abcv" = other "#.to_string(), TEST_CONFIG_FILENAME).is_err()); } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index b27dba0a..6bf54446 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -218,7 +218,7 @@ impl Rocket { info!("{}:", request); // Inform the request about all of the precomputed state. - request.set_preset_state(&self.config.session_key(), &self.state); + request.set_preset_state(&self.config.secret_key(), &self.state); // Do a bit of preprocessing before routing; run the attached fairings. self.preprocess_request(request, &data); @@ -393,7 +393,7 @@ impl Rocket { info_!("port: {}", White.paint(&config.port)); info_!("log: {}", White.paint(config.log_level)); info_!("workers: {}", White.paint(config.workers)); - info_!("session key: {}", White.paint(config.session_key.kind())); + info_!("secret key: {}", White.paint(config.secret_key.kind())); info_!("limits: {}", White.paint(&config.limits)); let tls_configured = config.tls.is_some(); From 9a7484f7a8a4abf662fb3efcf60ce20ffc7dbe17 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 13 May 2017 02:00:35 -0700 Subject: [PATCH 136/297] Reclose connection on unread data. Minimum nightly is 1.19. --- lib/build.rs | 2 +- lib/src/data/data.rs | 17 +++++++---------- lib/src/data/data_stream.rs | 30 ++++++++++++++++++++++-------- lib/src/data/net_stream.rs | 17 ----------------- lib/src/http/uri.rs | 4 ++-- lib/src/lib.rs | 1 + lib/src/router/collider.rs | 7 +++---- 7 files changed, 36 insertions(+), 42 deletions(-) diff --git a/lib/build.rs b/lib/build.rs index e159803a..69125818 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -8,7 +8,7 @@ use ansi_term::Color::{Red, Yellow, Blue, White}; use version_check::{is_nightly, is_min_version}; // Specifies the minimum nightly version needed to compile Rocket. -const MIN_VERSION: &'static str = "1.16.0-nightly"; +const MIN_VERSION: &'static str = "1.19.0-nightly"; // Convenience macro for writing to stderr. macro_rules! printerr { diff --git a/lib/src/data/data.rs b/lib/src/data/data.rs index 24af63d9..31b71f8b 100644 --- a/lib/src/data/data.rs +++ b/lib/src/data/data.rs @@ -5,7 +5,7 @@ use std::time::Duration; #[cfg(feature = "tls")] use super::net_stream::HttpsStream; -use super::data_stream::DataStream; +use super::data_stream::{DataStream, kill_stream}; use super::net_stream::NetStream; use ext::ReadExt; @@ -17,7 +17,7 @@ use http::hyper::net::{HttpStream, NetworkStream}; pub type HyperBodyReader<'a, 'b> = self::HttpReader<&'a mut hyper::buffer::BufReader<&'b mut NetworkStream>>; -// |---- from hyper ----| +// |---- from hyper ----| pub type BodyReader = HttpReader>, NetStream>>; /// The number of bytes to read into the "peek" buffer. @@ -217,11 +217,8 @@ impl Data { } } -// impl Drop for Data { -// fn drop(&mut self) { -// // FIXME: Do a read; if > 1024, kill the stream. Need access to the -// // internals of `Chain` to do this efficiently/without crazy baggage. -// // https://github.com/rust-lang/rust/pull/41463 -// let _ = io::copy(&mut self.stream, &mut io::sink()); -// } -// } +impl Drop for Data { + fn drop(&mut self) { + kill_stream(&mut self.stream); + } +} diff --git a/lib/src/data/data_stream.rs b/lib/src/data/data_stream.rs index 6e6e8731..1fbb9337 100644 --- a/lib/src/data/data_stream.rs +++ b/lib/src/data/data_stream.rs @@ -1,6 +1,8 @@ use std::io::{self, Read, Cursor, Chain}; +use std::net::Shutdown; use super::data::BodyReader; +use http::hyper::net::NetworkStream; // It's very unfortunate that we have to wrap `BodyReader` in a `BufReader` // since it already contains another `BufReader`. The issue is that Hyper's @@ -38,11 +40,23 @@ impl Read for DataStream { // } // } -// impl Drop for DataStream { -// fn drop(&mut self) { -// // FIXME: Do a read; if > 1024, kill the stream. Need access to the -// // internals of `Chain` to do this efficiently/without crazy baggage. -// // https://github.com/rust-lang/rust/pull/41463 -// let _ = io::copy(&mut self.0, &mut io::sink()); -// } -// } +pub fn kill_stream(stream: &mut BodyReader) { + // Take <= 1k from the stream. If there might be more data, force close. + const FLUSH_LEN: u64 = 1024; + match io::copy(&mut stream.take(FLUSH_LEN), &mut io::sink()) { + Ok(FLUSH_LEN) | Err(_) => { + warn_!("Data left unread. Force closing network stream."); + let (_, network) = stream.get_mut().get_mut(); + if let Err(e) = network.close(Shutdown::Both) { + error_!("Failed to close network stream: {:?}", e); + } + } + Ok(n) => debug!("flushed {} unread bytes", n) + } +} + +impl Drop for DataStream { + fn drop(&mut self) { + kill_stream(&mut self.0.get_mut().1); + } +} diff --git a/lib/src/data/net_stream.rs b/lib/src/data/net_stream.rs index 340ed792..a53b037d 100644 --- a/lib/src/data/net_stream.rs +++ b/lib/src/data/net_stream.rs @@ -91,20 +91,3 @@ impl NetworkStream for NetStream { } } } - -// impl Drop for NetStream { -// fn drop(&mut self) { -// // Take <= 1k from the stream. If there might be more data, force close. -// trace_!("Dropping the network stream..."); -// // const FLUSH_LEN: u64 = 1024; -// // match io::copy(&mut self.take(FLUSH_LEN), &mut io::sink()) { -// // Ok(FLUSH_LEN) | Err(_) => { -// // warn_!("Data left unread. Force closing network stream."); -// // if let Err(e) = self.close(Shutdown::Both) { -// // error_!("Failed to close network stream: {:?}", e); -// // } -// // } -// // Ok(n) => debug!("flushed {} unread bytes", n) -// // } -// } -// } diff --git a/lib/src/http/uri.rs b/lib/src/http/uri.rs index d57557ca..20b990ec 100644 --- a/lib/src/http/uri.rs +++ b/lib/src/http/uri.rs @@ -339,6 +339,7 @@ pub struct Segments<'a>(pub &'a str); impl<'a> Iterator for Segments<'a> { type Item = &'a str; + #[inline] fn next(&mut self) -> Option { // Find the start of the next segment (first that's not '/'). let i = match self.0.find(|c| c != '/') { @@ -348,8 +349,7 @@ impl<'a> Iterator for Segments<'a> { // Get the index of the first character that _is_ a '/' after start. // j = index of first character after i (hence the i +) that's not a '/' - let rest = &self.0[i..]; - let j = rest.find('/').map_or(self.0.len(), |j| i + j); + let j = self.0[i..].find('/').map_or(self.0.len(), |j| i + j); // Save the result, update the iterator, and return! let result = Some(&self.0[i..j]); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 9221c04e..6c9bd4b5 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -8,6 +8,7 @@ #![feature(plugin)] #![feature(never_type)] #![feature(concat_idents)] +#![feature(more_io_inner_methods)] #![plugin(pear_codegen)] diff --git a/lib/src/router/collider.rs b/lib/src/router/collider.rs index 63f17715..5e4ed1f2 100644 --- a/lib/src/router/collider.rs +++ b/lib/src/router/collider.rs @@ -13,10 +13,9 @@ pub trait Collider { #[inline(always)] fn index_match_until(break_c: char, - a: &str, - b: &str, - dir: bool) - -> Option<(isize, isize)> { + a: &str, + b: &str, + dir: bool) -> Option<(isize, isize)> { let (a_len, b_len) = (a.len() as isize, b.len() as isize); let (mut i, mut j, delta) = if dir { (0, 0, 1) From 9c9740f9661354f8a240c8979a7890517bec7994 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 14 May 2017 21:46:01 -0700 Subject: [PATCH 137/297] Fairings v2. --- examples/fairings/src/main.rs | 70 +++++- examples/fairings/src/tests.rs | 26 ++- lib/src/fairing/ad_hoc.rs | 136 ++++++++++++ lib/src/fairing/fairings.rs | 89 ++++++++ lib/src/fairing/info_kind.rs | 89 ++++++++ lib/src/fairing/mod.rs | 386 ++++++++++++++++++++------------- lib/src/lib.rs | 3 +- lib/src/rocket.rs | 22 +- 8 files changed, 640 insertions(+), 181 deletions(-) create mode 100644 lib/src/fairing/ad_hoc.rs create mode 100644 lib/src/fairing/fairings.rs create mode 100644 lib/src/fairing/info_kind.rs diff --git a/examples/fairings/src/main.rs b/examples/fairings/src/main.rs index f28ebbcb..579e8720 100644 --- a/examples/fairings/src/main.rs +++ b/examples/fairings/src/main.rs @@ -4,12 +4,53 @@ extern crate rocket; use std::io::Cursor; +use std::sync::atomic::{AtomicUsize, Ordering}; -use rocket::Fairing; -use rocket::http::Method; +use rocket::{Request, Data, Response}; +use rocket::fairing::{AdHoc, Fairing, Info, Kind}; +use rocket::http::{Method, ContentType, Status}; #[cfg(test)] mod tests; +#[derive(Default)] +struct Counter { + get: AtomicUsize, + post: AtomicUsize, +} + +impl Fairing for Counter { + fn info(&self) -> Info { + Info { + name: "GET/POST Counter", + kind: Kind::Request | Kind::Response + } + } + + fn on_request(&self, request: &mut Request, _: &Data) { + if request.method() == Method::Get { + self.get.fetch_add(1, Ordering::Relaxed); + } else if request.method() == Method::Post { + self.post.fetch_add(1, Ordering::Relaxed); + } + } + + fn on_response(&self, request: &Request, response: &mut Response) { + if response.status() != Status::NotFound { + return + } + + if request.method() == Method::Get && request.uri().path() == "/counts" { + let get_count = self.get.load(Ordering::Relaxed); + let post_count = self.post.load(Ordering::Relaxed); + + let body = format!("Get: {}\nPost: {}", get_count, post_count); + response.set_status(Status::Ok); + response.set_header(ContentType::Plain); + response.set_sized_body(Cursor::new(body)); + } + } +} + #[put("/")] fn hello() -> &'static str { "Hello, world!" @@ -18,19 +59,24 @@ fn hello() -> &'static str { fn rocket() -> rocket::Rocket { rocket::ignite() .mount("/", routes![hello]) - .attach(Fairing::Launch(Box::new(|rocket| { + .attach(Counter::default()) + .attach(AdHoc::on_launch(|rocket| { println!("Rocket is about to launch! Exciting! Here we go..."); Ok(rocket) - }))) - .attach(Fairing::Request(Box::new(|req, _| { + })) + .attach(AdHoc::on_request(|req, _| { println!(" => Incoming request: {}", req); - println!(" => Changing method to `PUT`."); - req.set_method(Method::Put); - }))) - .attach(Fairing::Response(Box::new(|_, res| { - println!(" => Rewriting response body."); - res.set_sized_body(Cursor::new("Hello, fairings!")); - }))) + if req.uri().path() == "/" { + println!(" => Changing method to `PUT`."); + req.set_method(Method::Put); + } + })) + .attach(AdHoc::on_response(|req, res| { + if req.uri().path() == "/" { + println!(" => Rewriting response body."); + res.set_sized_body(Cursor::new("Hello, fairings!")); + } + })) } fn main() { diff --git a/examples/fairings/src/tests.rs b/examples/fairings/src/tests.rs index 197cefd2..ecba4a6a 100644 --- a/examples/fairings/src/tests.rs +++ b/examples/fairings/src/tests.rs @@ -3,9 +3,33 @@ use rocket::testing::MockRequest; use rocket::http::Method::*; #[test] -fn fairings() { +fn rewrite_get_put() { let rocket = rocket(); let mut req = MockRequest::new(Get, "/"); let mut response = req.dispatch_with(&rocket); assert_eq!(response.body_string(), Some("Hello, fairings!".into())); } +#[test] +fn counts() { + let rocket = rocket(); + + // Issue 1 GET request. + let mut req = MockRequest::new(Get, "/"); + req.dispatch_with(&rocket); + + // Check the GET count, taking into account _this_ GET request. + let mut req = MockRequest::new(Get, "/counts"); + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.body_string(), Some("Get: 2\nPost: 0".into())); + + // Issue 1 more GET request and a POST. + let mut req = MockRequest::new(Get, "/"); + req.dispatch_with(&rocket); + let mut req = MockRequest::new(Post, "/"); + req.dispatch_with(&rocket); + + // Check the counts. + let mut req = MockRequest::new(Get, "/counts"); + let mut response = req.dispatch_with(&rocket); + assert_eq!(response.body_string(), Some("Get: 4\nPost: 1".into())); +} diff --git a/lib/src/fairing/ad_hoc.rs b/lib/src/fairing/ad_hoc.rs new file mode 100644 index 00000000..3ba630f6 --- /dev/null +++ b/lib/src/fairing/ad_hoc.rs @@ -0,0 +1,136 @@ +use {Rocket, Request, Response, Data}; +use fairing::{Fairing, Kind, Info}; + +/// A ad-hoc fairing that can be created from a function or closure. +/// +/// This enum can be used to create a fairing from a simple function or clusure +/// without creating a new structure or implementing `Fairing` directly. +/// +/// # Usage +/// +/// Use the [`on_launch`](#method.on_launch), +/// [`on_request`](#method.on_request), or [`on_response`](#method.on_response) +/// constructors to create an `AdHoc` structure from a function or closure. +/// Then, simply attach the structure to the `Rocket` instance. +/// +/// # Example +/// +/// The following snippet creates a `Rocket` instance with two ad-hoc fairings. +/// The first, a launch fairing, simply prints a message indicating that the +/// application is about to the launch. The second, a request fairing, rewrites +/// the method of all requests to be `PUT`. +/// +/// ```rust +/// use rocket::fairing::AdHoc; +/// use rocket::http::Method; +/// +/// rocket::ignite() +/// .attach(AdHoc::on_launch(|rocket| { +/// println!("Rocket is about to launch! Exciting! Here we go..."); +/// Ok(rocket) +/// })) +/// .attach(AdHoc::on_request(|req, _| { +/// req.set_method(Method::Put); +/// })); +/// ``` +pub enum AdHoc { + /// An ad-hoc **launch** fairing. Called just before Rocket launches. + #[doc(hidden)] + Launch(Box Result + Send + Sync + 'static>), + /// An ad-hoc **request** fairing. Called when a request is received. + #[doc(hidden)] + Request(Box), + /// An ad-hoc **response** fairing. Called when a response is ready to be + /// sent to a client. + #[doc(hidden)] + Response(Box) +} + +impl AdHoc { + /// Constructs an `AdHoc` launch fairing. The function `f` will be called by + /// Rocket just prior to launching. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::AdHoc; + /// + /// // The no-op launch fairing. + /// let fairing = AdHoc::on_launch(|rocket| Ok(rocket)); + /// ``` + pub fn on_launch(f: F) -> AdHoc + where F: Fn(Rocket) -> Result + Send + Sync + 'static + { + AdHoc::Launch(Box::new(f)) + } + + /// Constructs an `AdHoc` request fairing. The function `f` will be called + /// by Rocket when a new request is received. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::AdHoc; + /// + /// // The no-op request fairing. + /// let fairing = AdHoc::on_request(|req, data| { + /// // do something with the request and data... + /// # let (_, _) = (req, data); + /// }); + /// ``` + pub fn on_request(f: F) -> AdHoc + where F: Fn(&mut Request, &Data) + Send + Sync + 'static + { + AdHoc::Request(Box::new(f)) + } + + /// Constructs an `AdHoc` response fairing. The function `f` will be called + /// by Rocket when a response is ready to be sent. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::AdHoc; + /// + /// // The no-op response fairing. + /// let fairing = AdHoc::on_response(|req, resp| { + /// // do something with the request and pending response... + /// # let (_, _) = (req, resp); + /// }); + /// ``` + pub fn on_response(f: F) -> AdHoc + where F: Fn(&Request, &mut Response) + Send + Sync + 'static + { + AdHoc::Response(Box::new(f)) + } +} + +impl Fairing for AdHoc { + fn info(&self) -> Info { + use self::AdHoc::*; + match *self { + Launch(_) => Info { name: "AdHoc::Launch", kind: Kind::Launch }, + Request(_) => Info { name: "AdHoc::Request", kind: Kind::Request }, + Response(_) => Info { name: "AdHoc::Response", kind: Kind::Response } + } + } + + fn on_launch(&self, rocket: Rocket) -> Result { + match *self { + AdHoc::Launch(ref launch_fn) => launch_fn(rocket), + _ => Ok(rocket) + } + } + + fn on_request(&self, request: &mut Request, data: &Data) { + if let AdHoc::Request(ref callback) = *self { + callback(request, data) + } + } + + fn on_response(&self, request: &Request, response: &mut Response) { + if let AdHoc::Response(ref callback) = *self { + callback(request, response) + } + } +} diff --git a/lib/src/fairing/fairings.rs b/lib/src/fairing/fairings.rs new file mode 100644 index 00000000..4129d444 --- /dev/null +++ b/lib/src/fairing/fairings.rs @@ -0,0 +1,89 @@ +use {Rocket, Request, Response, Data}; +use fairing::{Fairing, Kind}; + +#[derive(Default)] +pub struct Fairings { + all_fairings: Vec>, + launch: Vec<&'static Fairing>, + request: Vec<&'static Fairing>, + response: Vec<&'static Fairing>, +} + +impl Fairings { + #[inline] + pub fn new() -> Fairings { + Fairings::default() + } + + #[inline] + pub fn attach(&mut self, fairing: Box) { + // Get the kind information. + let kind = fairing.info().kind; + + // The `Fairings` structure separates `all_fairings` into kind groups so + // we don't have to search through all fairings and do a comparison at + // runtime. We need references since a single structure can be multiple + // kinds. The lifetime of that reference is really the lifetime of the + // `Box` for referred fairing, but that lifetime is dynamic; there's no + // way to express it. So we cheat and say that the lifetime is + // `'static` and cast it here. For this to be safe, the following must + // be preserved: + // + // 1) The references can never be exposed with a `'static` lifetime. + // 2) The `Box` must live for the lifetime of the reference. + // + // We maintain these invariants by not exposing the references and never + // deallocating `Box` structures. As such, the references will + // always be valid. Note: `ptr` doesn't point into the `Vec`, so + // reallocations there are irrelvant. Instead, it points into the heap. + let ptr: &'static Fairing = unsafe { ::std::mem::transmute(&*fairing) }; + + self.all_fairings.push(fairing); + if kind.is(Kind::Launch) { self.launch.push(ptr); } + if kind.is(Kind::Request) { self.request.push(ptr); } + if kind.is(Kind::Response) { self.response.push(ptr); } + } + + #[inline(always)] + pub fn handle_launch(&mut self, mut rocket: Rocket) -> Option { + let mut success = Some(()); + for f in &self.launch { + rocket = f.on_launch(rocket).unwrap_or_else(|r| { success = None; r }); + } + + success.map(|_| rocket) + } + + #[inline(always)] + pub fn handle_request(&self, req: &mut Request, data: &Data) { + for fairing in &self.request { + fairing.on_request(req, data); + } + } + + #[inline(always)] + pub fn handle_response(&self, request: &Request, response: &mut Response) { + for fairing in &self.response { + fairing.on_response(request, response); + } + } + + pub fn pretty_print_counts(&self) { + use term_painter::ToStyle; + use term_painter::Color::{White, Magenta}; + + if self.all_fairings.len() > 0 { + info!("📡 {}:", Magenta.paint("Fairings")); + } + + fn info_if_nonempty(kind: &str, fairings: &[&Fairing]) { + let names: Vec<&str> = fairings.iter().map(|f| f.info().name).collect(); + info_!("{} {}: {}", White.paint(fairings.len()), kind, + White.paint(names.join(", "))); + } + + info_if_nonempty("launch", &self.launch); + info_if_nonempty("request", &self.request); + info_if_nonempty("response", &self.response); + } +} diff --git a/lib/src/fairing/info_kind.rs b/lib/src/fairing/info_kind.rs new file mode 100644 index 00000000..ac0091ec --- /dev/null +++ b/lib/src/fairing/info_kind.rs @@ -0,0 +1,89 @@ +use std::ops::BitOr; + +/// Information about a [`Fairing`](/rocket/fairing/trait.Fairing.html). +/// +/// The `name` field is an arbitrary name for a fairing. The `kind` field is a +/// is an `or`d set of [`Kind`](/rocket/fairing/struct.Kind.html) structures. +/// Rocket uses the values set in `Kind` to determine which callbacks from a +/// given `Fairing` implementation to actually call. +/// +/// # Example +/// +/// A simple `Info` structure that can be used for a `Fairing` that implements +/// all three callbacks: +/// +/// ``` +/// use rocket::fairing::{Info, Kind}; +/// +/// # let _unused_info = +/// Info { +/// name: "Example Fairing", +/// kind: Kind::Launch | Kind::Request | Kind::Response +/// } +/// # ; +/// ``` +pub struct Info { + /// The name of the fairing. + pub name: &'static str, + /// A set representing the callbacks the fairing wishes to receive. + pub kind: Kind +} + +/// A bitset representing the kinds of callbacks a +/// [`Fairing`](/rocket/fairing/trait.Fairing.html) wishes to receive. +/// +/// A fairing can request any combination of any of the following kinds of +/// callbacks: +/// +/// * Launch +/// * Request +/// * Response +/// +/// Two `Kind` structures can be `or`d together to represent a combination. For +/// instance, to represent a fairing that is both a launch and request fairing, +/// use `Kind::Launch | Kind::Request`. Similarly, to represent a fairing that +/// is all three kinds, use `Kind::Launch | Kind::Request | Kind::Response`. +#[derive(Debug, Clone, Copy)] +pub struct Kind(usize); + +#[allow(non_upper_case_globals)] +impl Kind { + /// `Kind` flag representing a request for a 'launch' callback. + pub const Launch: Kind = Kind(0b001); + /// `Kind` flag representing a request for a 'request' callback. + pub const Request: Kind = Kind(0b010); + /// `Kind` flag representing a request for a 'response' callback. + pub const Response: Kind = Kind(0b100); + + /// Returns `true` if `self` is a superset of `other`. In other words, + /// returns `true` if all of the kinds in `other` are also in `self`. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::Kind; + /// + /// let launch_and_req = Kind::Launch | Kind::Request; + /// assert!(launch_and_req.is(Kind::Launch | Kind::Request)); + /// + /// assert!(launch_and_req.is(Kind::Launch)); + /// assert!(launch_and_req.is(Kind::Request)); + /// + /// assert!(!launch_and_req.is(Kind::Response)); + /// assert!(!launch_and_req.is(Kind::Launch | Kind::Response)); + /// assert!(!launch_and_req.is(Kind::Launch | Kind::Request | Kind::Response)); + /// ``` + #[inline] + pub fn is(self, other: Kind) -> bool { + (other.0 & self.0) == other.0 + } +} + +impl BitOr for Kind { + type Output = Self; + + #[inline(always)] + fn bitor(self, rhs: Self) -> Self { + Kind(self.0 | rhs.0) + } +} diff --git a/lib/src/fairing/mod.rs b/lib/src/fairing/mod.rs index dd176d49..f2e2f08e 100644 --- a/lib/src/fairing/mod.rs +++ b/lib/src/fairing/mod.rs @@ -2,34 +2,52 @@ //! //! Fairings allow for structured interposition at various points in the //! application lifetime. Fairings can be seen as a restricted form of -//! "middleware". A fairing is simply a function with a particular signature -//! that Rocket will run at a requested point in a program. You can use fairings -//! to rewrite or record information about requests and responses, or to perform -//! an action once a Rocket application has launched. +//! "middleware". A fairing is an arbitrary structure with methods representing +//! callbacks that Rocket will run at requested points in a program. You can use +//! fairings to rewrite or record information about requests and responses, or +//! to perform an action once a Rocket application has launched. //! //! ## Attaching //! //! You must inform Rocket about fairings that you wish to be active by calling //! the [`attach`](/rocket/struct.Rocket.html#method.attach) method on the //! [`Rocket`](/rocket/struct.Rocket.html) instance and passing in the -//! appropriate [`Fairing`](/rocket/fairing/enum.Fairing.html). For instance, to -//! attach `Request` and `Response` fairings named `req_fairing` and -//! `res_fairing` to a new Rocket instance, you might write: +//! appropriate [`Fairing`](/rocket/fairing/trait.Fairing.html). For instance, +//! to attach fairings named `req_fairing` and `res_fairing` to a new Rocket +//! instance, you might write: //! //! ```rust -//! # use rocket::Fairing; -//! # let req_fairing = Fairing::Request(Box::new(|_, _| ())); -//! # let res_fairing = Fairing::Response(Box::new(|_, _| ())); -//! # #[allow(unused_variables)] +//! # use rocket::fairing::AdHoc; +//! # let req_fairing = AdHoc::on_request(|_, _| ()); +//! # let res_fairing = AdHoc::on_response(|_, _| ()); //! let rocket = rocket::ignite() -//! .attach(vec![req_fairing, res_fairing]); +//! .attach(req_fairing) +//! .attach(res_fairing); //! ``` //! //! Once a fairing is attached, Rocket will execute it at the appropiate time, -//! which varies depending on the fairing type. - +//! which varies depending on the fairing type. See the +//! [`Fairing`](/rocket/fairing/trait.Fairing.html) trait documentation for more +//! information on the dispatching of fairing methods. +//! +//! ## Ordering +//! +//! `Fairing`s are executed in the same order in which they are attached: the +//! first attached fairing has its callbacks executed before all others. Because +//! fairing callbacks may not be commutative, it is important to communicate to +//! the user every consequence of a fairing. Furthermore, a `Fairing` should +//! take care to act locally so that the actions of other `Fairings` are not +//! jeopardized. use {Rocket, Request, Response, Data}; +mod fairings; +mod ad_hoc; +mod info_kind; + +pub(crate) use self::fairings::Fairings; +pub use self::ad_hoc::AdHoc; +pub use self::info_kind::{Info, Kind}; + // We might imagine that a request fairing returns an `Outcome`. If it returns // `Success`, we don't do any routing and use that response directly. Same if it // returns `Failure`. We only route if it returns `Forward`. I've chosen not to @@ -42,159 +60,221 @@ use {Rocket, Request, Response, Data}; // appropriate response. This allows the users to handle `OPTIONS` requests // when they'd like but default to the fairing when they don't want to. -/// The type of a **launch** fairing callback. +/// Trait implemented by fairings: Rocket's structured middleware. /// -/// The `Rocket` parameter is the `Rocket` instance being built. The launch -/// fairing can modify the `Rocket` instance arbitrarily. +/// ## Fairing Information /// -/// TODO: Document fully with examples before 0.3. -pub type LaunchFn = Box Result + Send + Sync + 'static>; -/// The type of a **request** fairing callback. +/// Every `Fairing` must implement the +/// [`info`](/rocket/fairing/trait.Fairing.html#tymethod.info) method, which +/// returns an [`Info`](http://localhost:8000/rocket/fairing/struct.Info.html) +/// structure. This structure is used by Rocket to: /// -/// The `&mut Request` parameter is the incoming request, and the `&Data` -/// parameter is the incoming data in the request. +/// 1. Assign a name to the `Fairing`. /// -/// TODO: Document fully with examples before 0.3. -pub type RequestFn = Box; -/// The type of a **response** fairing callback. +/// This is the `name` field, which can be any arbitrary string. Name your +/// fairing something illustrative. The name will be logged during the +/// application's launch procedures. /// -/// The `&Request` parameter is the request that was routed, and the `&mut -/// Response` parameter is the result response. +/// 2. Determine which callbacks to actually issue on the `Fairing`. /// -/// TODO: Document fully with examples before 0.3. -pub type ResponseFn = Box; - -/// An enum representing the three fairing types: launch, request, and response. +/// This is the `kind` field of type +/// [`Kind`](/rocket/fairing/struct.Kind.html). This field is a bitset that +/// represents the kinds of callbacks the fairing wishes to receive. Rocket +/// will only invoke the callbacks that are flagged in this set. `Kind` +/// structures can be `or`d together to represent any combination of kinds +/// of callbacks. For instance, to request launch and response callbacks, +/// return a `kind` field with the value `Kind::Launch | Kind::Response`. /// -/// ## Fairing Types +/// See the [top-level documentation](/rocket/fairing/) for more general +/// information. /// -/// The three types of fairings, launch, request, and response, operate as -/// follows: +/// ## Fairing Callbacks /// -/// * *Launch Fairings* +/// There are three kinds of fairing callbacks: launch, request, and response. +/// As mentioned above, a fairing can request any combination of these callbacks +/// through the `kind` field of the `Info` structure returned from the `info` +/// method. Rocket will only invoke the callbacks set in the `kind` field. /// -/// An attached launch fairing will be called immediately before the Rocket -/// application has launched. At this point, Rocket has opened a socket for -/// listening but has not yet begun accepting connections. A launch fairing -/// can arbitrarily modify the `Rocket` instance being launched. It returns -/// `Ok` if it would like launching to proceed nominally and `Err` -/// otherwise. If a launch fairing returns `Err`, launch is aborted. The -/// [`LaunchFn`](/rocket/fairing/type.LaunchFn.html) documentation contains -/// further information and tips on the function signature. +/// The three callback kinds are as follows: /// -/// * *Request Fairings* +/// * **Launch (`on_launch`)** /// -/// An attached request fairing is called when a request is received. At -/// this point, Rocket has parsed the incoming HTTP into a -/// [Request](/rocket/struct.Request.html) and -/// [Data](/rocket/struct.Data.html) object but has not routed the request. -/// A request fairing can modify the request at will and -/// [peek](/rocket/struct.Data.html#method.peek) into the incoming data. It -/// may not, however, abort or respond directly to the request; these issues -/// are better handled via [request +/// A launch callback, represented by the +/// [`on_launch`](/rocket/fairing/trait.Fairing.html#method.on_launch) +/// method, is called immediately before the Rocket application has +/// launched. At this point, Rocket has opened a socket for listening but +/// has not yet begun accepting connections. A launch callback can +/// arbitrarily modify the `Rocket` instance being launched. It returns `Ok` +/// if it would like launching to proceed nominally and `Err` otherwise. If +/// a launch callback returns `Err`, launch is aborted. +/// +/// * **Request (`on_request`)** +/// +/// A request callback, represented by the +/// [`on_request`](/rocket/fairing/trait.Fairing.html#method.on_request) +/// method, is called just after a request is received. At this point, +/// Rocket has parsed the incoming HTTP into +/// [`Request`](/rocket/struct.Request.html) and +/// [`Data`](/rocket/struct.Data.html) structures but has not routed the +/// request. A request callback can modify the request at will and +/// [`peek`](/rocket/struct.Data.html#method.peek) into the incoming data. +/// It may not, however, abort or respond directly to the request; these +/// issues are better handled via [request /// guards](/rocket/request/trait.FromRequest.html) or via response -/// fairings. A modified request is routed as if it was the original -/// request. The [`RequestFn`](/rocket/fairing/type.RequestFn.html) -/// documentation contains further information and tips on the function -/// signature. +/// callbacks. A modified request is routed as if it was the original +/// request. /// -/// * *Response Fairings* +/// * **Response (`on_response`)** /// -/// An attached response fairing is called when a response is ready to be -/// sent to the client. At this point, Rocket has completed all routing, -/// including to error catchers, and has generated the would-be final -/// response. A response fairing can modify the response at will. A response -/// fairing, can, for example, provide a default response when the user -/// fails to handle the request by checking for 404 responses. The -/// [`ResponseFn`](/rocket/fairing/type.ResponseFn.html) documentation -/// contains further information and tips on the function signature. +/// A response callback is called when a response is ready to be sent to the +/// client. At this point, Rocket has completed all routing, including to +/// error catchers, and has generated the would-be final response. A +/// response callback can modify the response at will. For exammple, a +/// response callback can provide a default response when the user fails to +/// handle the request by checking for 404 responses. /// -/// See the [top-level documentation](/rocket/fairing/) for general information. -pub enum Fairing { - /// A launch fairing. Called just before Rocket launches. - Launch(LaunchFn), - /// A request fairing. Called when a request is received. - Request(RequestFn), - /// A response fairing. Called when a response is ready to be sent. - Response(ResponseFn), -} - -#[derive(Default)] -pub(crate) struct Fairings { - pub launch: Vec, - pub request: Vec, - pub response: Vec, -} - -impl Fairings { - #[inline] - pub fn new() -> Fairings { - Fairings::default() - } - - #[inline(always)] - pub fn attach_all(&mut self, fairings: Vec) { - for fairing in fairings { - self.attach(fairing) - } - } - - #[inline] - pub fn attach(&mut self, fairing: Fairing) { - match fairing { - Fairing::Launch(f) => self.launch.push(f), - Fairing::Request(f) => self.request.push(f), - Fairing::Response(f) => self.response.push(f), - } - } - - #[inline(always)] - pub fn handle_launch(&mut self, mut rocket: Rocket) -> Option { - let mut success = Some(()); - let launch_fairings = ::std::mem::replace(&mut self.launch, vec![]); - for fairing in launch_fairings { - rocket = fairing(rocket).unwrap_or_else(|r| { success = None; r }); - } - - success.map(|_| rocket) - } - - #[inline(always)] - pub fn handle_request(&self, req: &mut Request, data: &Data) { - for fairing in &self.request { - fairing(req, data); - } - } - - #[inline(always)] - pub fn handle_response(&self, request: &Request, response: &mut Response) { - for fairing in &self.response { - fairing(request, response); - } - } - - fn num_attached(&self) -> usize { - self.launch.len() + self.request.len() + self.response.len() - } - - pub fn pretty_print_counts(&self) { - use term_painter::ToStyle; - use term_painter::Color::{White, Magenta}; - - if self.num_attached() > 0 { - info!("📡 {}:", Magenta.paint("Fairings")); - } - - if !self.launch.is_empty() { - info_!("{} launch", White.paint(self.launch.len())); - } - - if !self.request.is_empty() { - info_!("{} request", White.paint(self.request.len())); - } - - if !self.response.is_empty() { - info_!("{} response", White.paint(self.response.len())); - } - } +/// # Implementing +/// +/// A `Fairing` implementation has one required method: `info`. A `Fairing` can +/// also implement any of the available callbacks: `on_launch`, `on_request`, +/// and `on_response`. A `Fairing` _must_ set the appropriate callback kind in +/// the `kind` field of the returned `Info` structure from `info` for a callback +/// to actually be issued by Rocket. +/// +/// A `Fairing` must be `Send + Sync + 'static`. This means that the fairing +/// must be sendable across thread boundaries (`Send`), thread-safe (`Sync`), +/// and have no non-`'static` reference (`'static`). Note that these bounds _do +/// not_ prohibit a `Fairing` from having state: the state need simply be +/// thread-safe and statically available or heap allocated. +/// +/// # Example +/// +/// Imagine that we want to record the number of `GET` and `POST` requests that +/// our application has received. While we could do this with [request +/// guards](/rocket/request/trait.FromRequest.html) and [managed +/// state](/rocket/request/struct.State.html), it would require us to annotate +/// every `GET` and `POST` request with custom types, polluting handler +/// signatures. Instead, we can create a simple fairing that does this globally. +/// +/// The `Counter` fairing below records the number of all `GET` and `POST` +/// requests received. It makes these counts available at a special `'/counts'` +/// path. +/// +/// ```rust +/// use std::io::Cursor; +/// use std::sync::atomic::{AtomicUsize, Ordering}; +/// +/// use rocket::{Request, Data, Response}; +/// use rocket::fairing::{Fairing, Info, Kind}; +/// use rocket::http::{Method, ContentType, Status}; +/// +/// #[derive(Default)] +/// struct Counter { +/// get: AtomicUsize, +/// post: AtomicUsize, +/// } +/// +/// impl Fairing for Counter { +/// fn info(&self) -> Info { +/// Info { +/// name: "GET/POST Counter", +/// kind: Kind::Request | Kind::Response +/// } +/// } +/// +/// fn on_request(&self, request: &mut Request, _: &Data) { +/// if request.method() == Method::Get { +/// self.get.fetch_add(1, Ordering::Relaxed); +/// } else if request.method() == Method::Post { +/// self.post.fetch_add(1, Ordering::Relaxed); +/// } +/// } +/// +/// fn on_response(&self, request: &Request, response: &mut Response) { +/// // Don't change a successful user's response, ever. +/// if response.status() != Status::NotFound { +/// return +/// } +/// +/// if request.method() == Method::Get && request.uri().path() == "/counts" { +/// let get_count = self.get.load(Ordering::Relaxed); +/// let post_count = self.post.load(Ordering::Relaxed); +/// +/// let body = format!("Get: {}\nPost: {}", get_count, post_count); +/// response.set_status(Status::Ok); +/// response.set_header(ContentType::Plain); +/// response.set_sized_body(Cursor::new(body)); +/// } +/// } +/// } +/// ``` +pub trait Fairing: Send + Sync + 'static { + /// Returns an [`Info`](/rocket/fairing/struct.Info.html) structure + /// containing the `name` and [`Kind`](/rocket/fairing/struct.Kind.html) of + /// this fairing. The `name` can be any arbitrary string. `Kind` must be an + /// `or`d set of `Kind` variants. + /// + /// This is the only required method of a `Fairing`. All other methods have + /// no-op default implementations. + /// + /// Rocket will only dispatch callbacks to this fairing for the kinds in the + /// `kind` field of the returned `Info` structure. For instance, if + /// `Kind::Launch | Kind::Request` is used, then Rocket will only call the + /// `on_launch` and `on_request` methods of the fairing. Similarly, if + /// `Kind::Response` is used, Rocket will only call the `on_response` method + /// of this fairing. + /// + /// # Example + /// + /// An `info` implementation for `MyFairing`: a fairing named "My Custom + /// Fairing" that is both a launch and response fairing. + /// + /// ```rust + /// use rocket::fairing::{Fairing, Info, Kind}; + /// + /// struct MyFairing; + /// + /// impl Fairing for MyFairing { + /// fn info(&self) -> Info { + /// Info { + /// name: "My Custom Fairing", + /// kind: Kind::Launch | Kind::Response + /// } + /// } + /// } + /// ``` + fn info(&self) -> Info; + + /// The launch callback. Returns `Ok` if launch should proceed and `Err` if + /// launch should be aborted. + /// + /// This method is called just prior to launching an application if + /// `Kind::Launch` is in the `kind` field of the `Info` structure for this + /// fairing. The `rocket` parameter is the `Rocket` instance that was built + /// for this application. + /// + /// The default implementation of this method simply returns `Ok(rocket)`. + fn on_launch(&self, rocket: Rocket) -> Result { Ok(rocket) } + + /// The request callback. + /// + /// This method is called when a new request is received if `Kind::Request` + /// is in the `kind` field of the `Info` structure for this fairing. The + /// `&mut Request` parameter is the incoming request, and the `&Data` + /// parameter is the incoming data in the request. + /// + /// The default implementation of this method does nothing. + #[allow(unused_variables)] + fn on_request(&self, request: &mut Request, data: &Data) { } + + /// The response callback. + /// + /// This method is called when a response is ready to be issued to a client + /// if `Kind::Response` is in the `kind` field of the `Info` structure for + /// this fairing. The `&Request` parameter is the request that was routed, + /// and the `&mut Response` parameter is the resulting response. + /// + /// The default implementation of this method does nothing. + #[allow(unused_variables)] + fn on_response(&self, request: &Request, response: &mut Response) { } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6c9bd4b5..6f894c4c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -126,12 +126,12 @@ pub mod outcome; pub mod config; pub mod data; pub mod handler; -pub mod error; pub mod fairing; mod router; mod rocket; mod codegen; +mod error; mod catcher; mod ext; @@ -141,7 +141,6 @@ mod ext; #[doc(hidden)] pub use codegen::{StaticRouteInfo, StaticCatchInfo}; #[doc(inline)] pub use outcome::Outcome; #[doc(inline)] pub use data::Data; -#[doc(inline)] pub use fairing::Fairing; pub use router::Route; pub use request::{Request, State}; pub use error::{Error, LaunchError}; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 6bf54446..2b5c8104 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -11,7 +11,7 @@ use state::Container; #[cfg(feature = "tls")] use hyper_rustls::TlsServer; use {logger, handler}; -use ext::{ReadExt, IntoCollection}; +use ext::ReadExt; use config::{self, Config, LoggedValue}; use request::{Request, FormItems}; use data::Data; @@ -587,21 +587,18 @@ impl Rocket { self } - /// Attaches zero or more fairings to this instance of Rocket. + /// Attaches a fairing to this instance of Rocket. /// - /// The `fairings` parameter to this function is generic: it may be either - /// a `Vec`, `&[Fairing]`, or simply `Fairing`. In all cases, all - /// supplied fairings are attached. - /// - /// # Examples + /// # Example /// /// ```rust /// # #![feature(plugin)] /// # #![plugin(rocket_codegen)] /// # extern crate rocket; - /// use rocket::{Rocket, Fairing}; + /// use rocket::Rocket; + /// use rocket::fairing::AdHoc; /// - /// fn launch_fairing(rocket: Rocket) -> Result { + /// fn youll_see(rocket: Rocket) -> Result { /// println!("Rocket is about to launch! You just see..."); /// Ok(rocket) /// } @@ -609,15 +606,14 @@ impl Rocket { /// fn main() { /// # if false { // We don't actually want to launch the server in an example. /// rocket::ignite() - /// .attach(Fairing::Launch(Box::new(launch_fairing))) + /// .attach(AdHoc::on_launch(youll_see)) /// .launch(); /// # } /// } /// ``` #[inline] - pub fn attach>(mut self, fairings: C) -> Self { - let fairings = fairings.into_collection::<[Fairing; 1]>().into_vec(); - self.fairings.attach_all(fairings); + pub fn attach(mut self, fairing: F) -> Self { + self.fairings.attach(Box::new(fairing)); self } From 28a1ef09167161da6a83bf25fbf3471ad3d77300 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 17 May 2017 01:39:36 -0700 Subject: [PATCH 138/297] Fairings, v3. Modifying the `Rocket` structure just before launch doesn't make sense for several reasons: 1) those affects can't influence the launch, and 2) they won't be observed in tests. Thus, an `Attach` fairing kind was added that ameliorates these issues. --- examples/fairings/Rocket.toml | 2 + examples/fairings/src/main.rs | 19 ++++++-- examples/fairings/src/tests.rs | 11 +++++ lib/src/fairing/ad_hoc.rs | 76 +++++++++++++++++++++++------ lib/src/fairing/fairings.rs | 45 +++++++++++------ lib/src/fairing/info_kind.rs | 35 ++++++++++--- lib/src/fairing/mod.rs | 89 +++++++++++++++++++++++----------- lib/src/rocket.rs | 44 +++++++++-------- lib/src/testing.rs | 20 +++++--- 9 files changed, 249 insertions(+), 92 deletions(-) create mode 100644 examples/fairings/Rocket.toml diff --git a/examples/fairings/Rocket.toml b/examples/fairings/Rocket.toml new file mode 100644 index 00000000..062cf500 --- /dev/null +++ b/examples/fairings/Rocket.toml @@ -0,0 +1,2 @@ +[global] +token = 123 diff --git a/examples/fairings/src/main.rs b/examples/fairings/src/main.rs index 579e8720..f55c3da6 100644 --- a/examples/fairings/src/main.rs +++ b/examples/fairings/src/main.rs @@ -6,10 +6,12 @@ extern crate rocket; use std::io::Cursor; use std::sync::atomic::{AtomicUsize, Ordering}; -use rocket::{Request, Data, Response}; +use rocket::{Request, State, Data, Response}; use rocket::fairing::{AdHoc, Fairing, Info, Kind}; use rocket::http::{Method, ContentType, Status}; +struct Token(i64); + #[cfg(test)] mod tests; #[derive(Default)] @@ -56,13 +58,22 @@ fn hello() -> &'static str { "Hello, world!" } +#[get("/token")] +fn token(token: State) -> String { + format!("{}", token.0) +} + fn rocket() -> rocket::Rocket { rocket::ignite() - .mount("/", routes![hello]) + .mount("/", routes![hello, token]) .attach(Counter::default()) + .attach(AdHoc::on_attach(|rocket| { + println!("Adding token managed state..."); + let token_val = rocket.config().get_int("token").unwrap_or(-1); + Ok(rocket.manage(Token(token_val))) + })) .attach(AdHoc::on_launch(|rocket| { - println!("Rocket is about to launch! Exciting! Here we go..."); - Ok(rocket) + println!("Rocket is about to launch!"); })) .attach(AdHoc::on_request(|req, _| { println!(" => Incoming request: {}", req); diff --git a/examples/fairings/src/tests.rs b/examples/fairings/src/tests.rs index ecba4a6a..a50e2e4f 100644 --- a/examples/fairings/src/tests.rs +++ b/examples/fairings/src/tests.rs @@ -9,6 +9,7 @@ fn rewrite_get_put() { let mut response = req.dispatch_with(&rocket); assert_eq!(response.body_string(), Some("Hello, fairings!".into())); } + #[test] fn counts() { let rocket = rocket(); @@ -33,3 +34,13 @@ fn counts() { let mut response = req.dispatch_with(&rocket); assert_eq!(response.body_string(), Some("Get: 4\nPost: 1".into())); } + +#[test] +fn token() { + let rocket = rocket(); + + // Ensure the token is '123', which is what we have in `Rocket.toml`. + let mut req = MockRequest::new(Get, "/token"); + let mut res = req.dispatch_with(&rocket); + assert_eq!(res.body_string(), Some("123".into())); +} diff --git a/lib/src/fairing/ad_hoc.rs b/lib/src/fairing/ad_hoc.rs index 3ba630f6..d353ed65 100644 --- a/lib/src/fairing/ad_hoc.rs +++ b/lib/src/fairing/ad_hoc.rs @@ -8,7 +8,7 @@ use fairing::{Fairing, Kind, Info}; /// /// # Usage /// -/// Use the [`on_launch`](#method.on_launch), +/// Use the [`on_attach`](#method.on_attach), [`on_launch`](#method.on_launch), /// [`on_request`](#method.on_request), or [`on_response`](#method.on_response) /// constructors to create an `AdHoc` structure from a function or closure. /// Then, simply attach the structure to the `Rocket` instance. @@ -25,28 +25,47 @@ use fairing::{Fairing, Kind, Info}; /// use rocket::http::Method; /// /// rocket::ignite() -/// .attach(AdHoc::on_launch(|rocket| { +/// .attach(AdHoc::on_launch(|_| { /// println!("Rocket is about to launch! Exciting! Here we go..."); -/// Ok(rocket) /// })) /// .attach(AdHoc::on_request(|req, _| { /// req.set_method(Method::Put); /// })); /// ``` pub enum AdHoc { + /// An ad-hoc **attach** fairing. Called when the fairing is attached. + #[doc(hidden)] + Attach(Box Result + Send + Sync + 'static>), /// An ad-hoc **launch** fairing. Called just before Rocket launches. #[doc(hidden)] - Launch(Box Result + Send + Sync + 'static>), + Launch(Box), /// An ad-hoc **request** fairing. Called when a request is received. #[doc(hidden)] Request(Box), /// An ad-hoc **response** fairing. Called when a response is ready to be /// sent to a client. #[doc(hidden)] - Response(Box) + Response(Box), } impl AdHoc { + /// Constructs an `AdHoc` attach fairing. The function `f` will be called by + /// Rocket when this fairing is attached. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::AdHoc; + /// + /// // The no-op attach fairing. + /// let fairing = AdHoc::on_attach(|rocket| Ok(rocket)); + /// ``` + pub fn on_attach(f: F) -> AdHoc + where F: Fn(Rocket) -> Result + Send + Sync + 'static + { + AdHoc::Attach(Box::new(f)) + } + /// Constructs an `AdHoc` launch fairing. The function `f` will be called by /// Rocket just prior to launching. /// @@ -55,11 +74,13 @@ impl AdHoc { /// ```rust /// use rocket::fairing::AdHoc; /// - /// // The no-op launch fairing. - /// let fairing = AdHoc::on_launch(|rocket| Ok(rocket)); + /// // A fairing that prints a message just before launching. + /// let fairing = AdHoc::on_launch(|rocket| { + /// println!("Launching in T-3..2..1.."); + /// }); /// ``` pub fn on_launch(f: F) -> AdHoc - where F: Fn(Rocket) -> Result + Send + Sync + 'static + where F: Fn(&Rocket) + Send + Sync + 'static { AdHoc::Launch(Box::new(f)) } @@ -109,16 +130,43 @@ impl Fairing for AdHoc { fn info(&self) -> Info { use self::AdHoc::*; match *self { - Launch(_) => Info { name: "AdHoc::Launch", kind: Kind::Launch }, - Request(_) => Info { name: "AdHoc::Request", kind: Kind::Request }, - Response(_) => Info { name: "AdHoc::Response", kind: Kind::Response } + Attach(_) => { + Info { + name: "AdHoc::Attach", + kind: Kind::Attach, + } + } + Launch(_) => { + Info { + name: "AdHoc::Launch", + kind: Kind::Launch, + } + } + Request(_) => { + Info { + name: "AdHoc::Request", + kind: Kind::Request, + } + } + Response(_) => { + Info { + name: "AdHoc::Response", + kind: Kind::Response, + } + } } } - fn on_launch(&self, rocket: Rocket) -> Result { + fn on_attach(&self, rocket: Rocket) -> Result { match *self { - AdHoc::Launch(ref launch_fn) => launch_fn(rocket), - _ => Ok(rocket) + AdHoc::Attach(ref callback) => callback(rocket), + _ => Ok(rocket), + } + } + + fn on_launch(&self, rocket: &Rocket) { + if let AdHoc::Launch(ref callback) = *self { + callback(rocket) } } diff --git a/lib/src/fairing/fairings.rs b/lib/src/fairing/fairings.rs index 4129d444..0afc0891 100644 --- a/lib/src/fairing/fairings.rs +++ b/lib/src/fairing/fairings.rs @@ -4,6 +4,7 @@ use fairing::{Fairing, Kind}; #[derive(Default)] pub struct Fairings { all_fairings: Vec>, + attach_failure: bool, launch: Vec<&'static Fairing>, request: Vec<&'static Fairing>, response: Vec<&'static Fairing>, @@ -15,11 +16,16 @@ impl Fairings { Fairings::default() } - #[inline] - pub fn attach(&mut self, fairing: Box) { + pub fn attach(&mut self, fairing: Box, mut rocket: Rocket) -> Rocket { // Get the kind information. let kind = fairing.info().kind; + // Run the `on_attach` callback if this is an 'attach' fairing. + if kind.is(Kind::Attach) { + rocket = fairing.on_attach(rocket) + .unwrap_or_else(|r| { self.attach_failure = true; r }) + } + // The `Fairings` structure separates `all_fairings` into kind groups so // we don't have to search through all fairings and do a comparison at // runtime. We need references since a single structure can be multiple @@ -36,22 +42,25 @@ impl Fairings { // deallocating `Box` structures. As such, the references will // always be valid. Note: `ptr` doesn't point into the `Vec`, so // reallocations there are irrelvant. Instead, it points into the heap. - let ptr: &'static Fairing = unsafe { ::std::mem::transmute(&*fairing) }; + // + // Also, we don't save attach fairings since we don't need them anymore. + if !kind.is_exactly(Kind::Attach) { + let ptr: &'static Fairing = unsafe { ::std::mem::transmute(&*fairing) }; - self.all_fairings.push(fairing); - if kind.is(Kind::Launch) { self.launch.push(ptr); } - if kind.is(Kind::Request) { self.request.push(ptr); } - if kind.is(Kind::Response) { self.response.push(ptr); } + self.all_fairings.push(fairing); + if kind.is(Kind::Launch) { self.launch.push(ptr); } + if kind.is(Kind::Request) { self.request.push(ptr); } + if kind.is(Kind::Response) { self.response.push(ptr); } + } + + rocket } #[inline(always)] - pub fn handle_launch(&mut self, mut rocket: Rocket) -> Option { - let mut success = Some(()); - for f in &self.launch { - rocket = f.on_launch(rocket).unwrap_or_else(|r| { success = None; r }); + pub fn handle_launch(&self, rocket: &Rocket) { + for fairing in &self.launch { + fairing.on_launch(rocket); } - - success.map(|_| rocket) } #[inline(always)] @@ -68,6 +77,10 @@ impl Fairings { } } + pub fn had_failure(&self) -> bool { + self.attach_failure + } + pub fn pretty_print_counts(&self) { use term_painter::ToStyle; use term_painter::Color::{White, Magenta}; @@ -78,8 +91,10 @@ impl Fairings { fn info_if_nonempty(kind: &str, fairings: &[&Fairing]) { let names: Vec<&str> = fairings.iter().map(|f| f.info().name).collect(); - info_!("{} {}: {}", White.paint(fairings.len()), kind, - White.paint(names.join(", "))); + info_!("{} {}: {}", + White.paint(fairings.len()), + kind, + White.paint(names.join(", "))); } info_if_nonempty("launch", &self.launch); diff --git a/lib/src/fairing/info_kind.rs b/lib/src/fairing/info_kind.rs index ac0091ec..a950ce8b 100644 --- a/lib/src/fairing/info_kind.rs +++ b/lib/src/fairing/info_kind.rs @@ -10,7 +10,7 @@ use std::ops::BitOr; /// # Example /// /// A simple `Info` structure that can be used for a `Fairing` that implements -/// all three callbacks: +/// all four callbacks: /// /// ``` /// use rocket::fairing::{Info, Kind}; @@ -18,7 +18,7 @@ use std::ops::BitOr; /// # let _unused_info = /// Info { /// name: "Example Fairing", -/// kind: Kind::Launch | Kind::Request | Kind::Response +/// kind: Kind::Attach | Kind::Launch | Kind::Request | Kind::Response /// } /// # ; /// ``` @@ -35,6 +35,7 @@ pub struct Info { /// A fairing can request any combination of any of the following kinds of /// callbacks: /// +/// * Attach /// * Launch /// * Request /// * Response @@ -42,18 +43,20 @@ pub struct Info { /// Two `Kind` structures can be `or`d together to represent a combination. For /// instance, to represent a fairing that is both a launch and request fairing, /// use `Kind::Launch | Kind::Request`. Similarly, to represent a fairing that -/// is all three kinds, use `Kind::Launch | Kind::Request | Kind::Response`. +/// is only an attach fairing, use `Kind::Attach`. #[derive(Debug, Clone, Copy)] pub struct Kind(usize); #[allow(non_upper_case_globals)] impl Kind { + /// `Kind` flag representing a request for an 'attach' callback. + pub const Attach: Kind = Kind(0b0001); /// `Kind` flag representing a request for a 'launch' callback. - pub const Launch: Kind = Kind(0b001); + pub const Launch: Kind = Kind(0b0010); /// `Kind` flag representing a request for a 'request' callback. - pub const Request: Kind = Kind(0b010); + pub const Request: Kind = Kind(0b0100); /// `Kind` flag representing a request for a 'response' callback. - pub const Response: Kind = Kind(0b100); + pub const Response: Kind = Kind(0b1000); /// Returns `true` if `self` is a superset of `other`. In other words, /// returns `true` if all of the kinds in `other` are also in `self`. @@ -77,6 +80,26 @@ impl Kind { pub fn is(self, other: Kind) -> bool { (other.0 & self.0) == other.0 } + + /// Returns `true` if `self` is exactly `other`. + /// + /// # Example + /// + /// ```rust + /// use rocket::fairing::Kind; + /// + /// let launch_and_req = Kind::Launch | Kind::Request; + /// assert!(launch_and_req.is_exactly(Kind::Launch | Kind::Request)); + /// + /// assert!(!launch_and_req.is_exactly(Kind::Launch)); + /// assert!(!launch_and_req.is_exactly(Kind::Request)); + /// assert!(!launch_and_req.is_exactly(Kind::Response)); + /// assert!(!launch_and_req.is_exactly(Kind::Launch | Kind::Response)); + /// ``` + #[inline] + pub fn is_exactly(self, other: Kind) -> bool { + self.0 == other.0 + } } impl BitOr for Kind { diff --git a/lib/src/fairing/mod.rs b/lib/src/fairing/mod.rs index f2e2f08e..d96bac12 100644 --- a/lib/src/fairing/mod.rs +++ b/lib/src/fairing/mod.rs @@ -1,4 +1,4 @@ -//! Fairings: structured interposition at launch, request, and response time. +//! Fairings: callbacks at attach, launch, request, and response time. //! //! Fairings allow for structured interposition at various points in the //! application lifetime. Fairings can be seen as a restricted form of @@ -26,7 +26,7 @@ //! ``` //! //! Once a fairing is attached, Rocket will execute it at the appropiate time, -//! which varies depending on the fairing type. See the +//! which varies depending on the fairing implementation. See the //! [`Fairing`](/rocket/fairing/trait.Fairing.html) trait documentation for more //! information on the dispatching of fairing methods. //! @@ -37,7 +37,8 @@ //! fairing callbacks may not be commutative, it is important to communicate to //! the user every consequence of a fairing. Furthermore, a `Fairing` should //! take care to act locally so that the actions of other `Fairings` are not -//! jeopardized. +//! jeopardized. For instance, unless it is made abundantly clear, a fairing +//! should not rewrite every request. use {Rocket, Request, Response, Data}; mod fairings; @@ -90,12 +91,29 @@ pub use self::info_kind::{Info, Kind}; /// /// ## Fairing Callbacks /// -/// There are three kinds of fairing callbacks: launch, request, and response. -/// As mentioned above, a fairing can request any combination of these callbacks -/// through the `kind` field of the `Info` structure returned from the `info` -/// method. Rocket will only invoke the callbacks set in the `kind` field. +/// There are four kinds of fairing callbacks: attach, launch, request, and +/// response. As mentioned above, a fairing can request any combination of these +/// callbacks through the `kind` field of the `Info` structure returned from the +/// `info` method. Rocket will only invoke the callbacks set in the `kind` +/// field. /// -/// The three callback kinds are as follows: +/// The four callback kinds are as follows: +/// +/// * **Attach (`on_attach`)** +/// +/// An attach callback, represented by the +/// [`on_attach`](/rocket/fairing/trait.Fairing.html#method.on_attach) +/// method, is called when a fairing is first attached via the +/// [`attach`](/rocket/struct.Rocket.html#method.attach) method. The state +/// of the `Rocket` instance is, at this point, not finalized, as the user +/// may still add additional information to the `Rocket` instance. As a +/// result, it is unwise to depend on the state of the `Rocket` instance. +/// +/// An attach callback can arbitrarily modify the `Rocket` instance being +/// constructed. It returns `Ok` if it would like launching to proceed +/// nominally and `Err` otherwise. If a launch callback returns `Err`, +/// launch will be aborted. All attach callbacks are executed on `launch`, +/// even if one or more signal a failure. /// /// * **Launch (`on_launch`)** /// @@ -103,10 +121,8 @@ pub use self::info_kind::{Info, Kind}; /// [`on_launch`](/rocket/fairing/trait.Fairing.html#method.on_launch) /// method, is called immediately before the Rocket application has /// launched. At this point, Rocket has opened a socket for listening but -/// has not yet begun accepting connections. A launch callback can -/// arbitrarily modify the `Rocket` instance being launched. It returns `Ok` -/// if it would like launching to proceed nominally and `Err` otherwise. If -/// a launch callback returns `Err`, launch is aborted. +/// has not yet begun accepting connections. A launch callback can inspect +/// the `Rocket` instance being launched. /// /// * **Request (`on_request`)** /// @@ -136,15 +152,15 @@ pub use self::info_kind::{Info, Kind}; /// # Implementing /// /// A `Fairing` implementation has one required method: `info`. A `Fairing` can -/// also implement any of the available callbacks: `on_launch`, `on_request`, -/// and `on_response`. A `Fairing` _must_ set the appropriate callback kind in -/// the `kind` field of the returned `Info` structure from `info` for a callback -/// to actually be issued by Rocket. +/// also implement any of the available callbacks: `on_attach`, `on_launch`, +/// `on_request`, and `on_response`. A `Fairing` _must_ set the appropriate +/// callback kind in the `kind` field of the returned `Info` structure from +/// `info` for a callback to actually be issued by Rocket. /// /// A `Fairing` must be `Send + Sync + 'static`. This means that the fairing /// must be sendable across thread boundaries (`Send`), thread-safe (`Sync`), /// and have no non-`'static` reference (`'static`). Note that these bounds _do -/// not_ prohibit a `Fairing` from having state: the state need simply be +/// not_ prohibit a `Fairing` from holding state: the state need simply be /// thread-safe and statically available or heap allocated. /// /// # Example @@ -154,7 +170,7 @@ pub use self::info_kind::{Info, Kind}; /// guards](/rocket/request/trait.FromRequest.html) and [managed /// state](/rocket/request/struct.State.html), it would require us to annotate /// every `GET` and `POST` request with custom types, polluting handler -/// signatures. Instead, we can create a simple fairing that does this globally. +/// signatures. Instead, we can create a simple fairing that acts globally. /// /// The `Counter` fairing below records the number of all `GET` and `POST` /// requests received. It makes these counts available at a special `'/counts'` @@ -238,23 +254,38 @@ pub trait Fairing: Send + Sync + 'static { /// fn info(&self) -> Info { /// Info { /// name: "My Custom Fairing", - /// kind: Kind::Launch | Kind::Response + /// kind: Kind::Attach | Kind::Launch | Kind::Response /// } /// } /// } /// ``` fn info(&self) -> Info; - /// The launch callback. Returns `Ok` if launch should proceed and `Err` if + /// The attach callback. Returns `Ok` if launch should proceed and `Err` if /// launch should be aborted. /// - /// This method is called just prior to launching an application if - /// `Kind::Launch` is in the `kind` field of the `Info` structure for this - /// fairing. The `rocket` parameter is the `Rocket` instance that was built - /// for this application. + /// This method is called when a fairing is attached if `Kind::Attach` is in + /// the `kind` field of the `Info` structure for this fairing. The `rocket` + /// parameter is the `Rocket` instance that is currently being built for + /// this application. + /// + /// ## Default Implementation /// /// The default implementation of this method simply returns `Ok(rocket)`. - fn on_launch(&self, rocket: Rocket) -> Result { Ok(rocket) } + fn on_attach(&self, rocket: Rocket) -> Result { Ok(rocket) } + + /// The launch callback. + /// + /// This method is called just prior to launching the application if + /// `Kind::Launch` is in the `kind` field of the `Info` structure for this + /// fairing. The `&Rocket` parameter curresponds to the application that + /// will be launched. + /// + /// ## Default Implementation + /// + /// The default implementation of this method does nothing. + #[allow(unused_variables)] + fn on_launch(&self, rocket: &Rocket) {} /// The request callback. /// @@ -263,9 +294,11 @@ pub trait Fairing: Send + Sync + 'static { /// `&mut Request` parameter is the incoming request, and the `&Data` /// parameter is the incoming data in the request. /// + /// ## Default Implementation + /// /// The default implementation of this method does nothing. #[allow(unused_variables)] - fn on_request(&self, request: &mut Request, data: &Data) { } + fn on_request(&self, request: &mut Request, data: &Data) {} /// The response callback. /// @@ -274,7 +307,9 @@ pub trait Fairing: Send + Sync + 'static { /// this fairing. The `&Request` parameter is the request that was routed, /// and the `&mut Response` parameter is the resulting response. /// + /// ## Default Implementation + /// /// The default implementation of this method does nothing. #[allow(unused_variables)] - fn on_response(&self, request: &Request, response: &mut Response) { } + fn on_response(&self, request: &Request, response: &mut Response) {} } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 2b5c8104..6eb378cc 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -284,7 +284,6 @@ impl Rocket { for route in matches { // Retrieve and set the requests parameters. info_!("Matched: {}", route); - // FIXME: Users should not be able to use this. request.set_params(route); // Dispatch the request to the handler. @@ -598,25 +597,37 @@ impl Rocket { /// use rocket::Rocket; /// use rocket::fairing::AdHoc; /// - /// fn youll_see(rocket: Rocket) -> Result { - /// println!("Rocket is about to launch! You just see..."); - /// Ok(rocket) - /// } - /// /// fn main() { /// # if false { // We don't actually want to launch the server in an example. /// rocket::ignite() - /// .attach(AdHoc::on_launch(youll_see)) + /// .attach(AdHoc::on_launch(|_| { + /// println!("Rocket is about to launch! You just see..."); + /// })) /// .launch(); /// # } /// } /// ``` #[inline] pub fn attach(mut self, fairing: F) -> Self { - self.fairings.attach(Box::new(fairing)); + // Attach the fairings, which requires us to move `self`. + let mut fairings = mem::replace(&mut self.fairings, Fairings::new()); + self = fairings.attach(Box::new(fairing), self); + + // Make sure we keep the fairings around! + self.fairings = fairings; self } + pub(crate) fn prelaunch_check(&self) -> Option { + if self.router.has_collisions() { + Some(LaunchError::from(LaunchErrorKind::Collision)) + } else if self.fairings.had_failure() { + Some(LaunchError::from(LaunchErrorKind::FailedFairing)) + } else { + None + } + } + /// Starts the application server and begins listening for and dispatching /// requests to mounted routes and catchers. Unless there is an error, this /// function does not return and blocks until program termination. @@ -637,9 +648,9 @@ impl Rocket { /// rocket::ignite().launch(); /// # } /// ``` - pub fn launch(mut self) -> LaunchError { - if self.router.has_collisions() { - return LaunchError::from(LaunchErrorKind::Collision); + pub fn launch(self) -> LaunchError { + if let Some(error) = self.prelaunch_check() { + return error; } self.fairings.pretty_print_counts(); @@ -657,15 +668,8 @@ impl Rocket { Err(e) => return LaunchError::from(e) }; - // Run all of the launch fairings. - let mut fairings = mem::replace(&mut self.fairings, Fairings::new()); - self = match fairings.handle_launch(self) { - Some(rocket) => rocket, - None => return LaunchError::from(LaunchErrorKind::FailedFairing) - }; - - // Make sure we keep the request/response fairings! - self.fairings = fairings; + // Run the launch fairings. + self.fairings.handle_launch(&self); launch_info!("🚀 {} {}{}", White.paint("Rocket has launched from"), diff --git a/lib/src/testing.rs b/lib/src/testing.rs index 46445aa4..8bd79510 100644 --- a/lib/src/testing.rs +++ b/lib/src/testing.rs @@ -76,7 +76,7 @@ //! ``` use ::{Rocket, Request, Response, Data}; -use http::{Method, Header, Cookie}; +use http::{Method, Status, Header, Cookie}; use std::net::SocketAddr; @@ -201,11 +201,12 @@ impl<'r> MockRequest<'r> { /// Dispatch this request using a given instance of Rocket. /// - /// Returns the body of the response if there was a response. The return - /// value is `None` if any of the following occurs: - /// - /// 1. The returned body was not valid UTF8. - /// 2. The application failed to respond. + /// It is possible that the supplied `rocket` instance contains malformed + /// input such as colliding or invalid routes or failed fairings. When this + /// is the case, the returned `Response` will contain a status of + /// `InternalServerError`, and the body will contain the error that + /// occurred. In all other cases, the returned `Response` will be that of + /// the application. /// /// # Examples /// @@ -234,6 +235,13 @@ impl<'r> MockRequest<'r> { /// # } /// ``` pub fn dispatch_with<'s>(&'s mut self, rocket: &'r Rocket) -> Response<'s> { + if let Some(error) = rocket.prelaunch_check() { + return Response::build() + .status(Status::InternalServerError) + .sized_body(::std::io::Cursor::new(error.to_string())) + .finalize() + } + let data = ::std::mem::replace(&mut self.data, Data::local(vec![])); rocket.dispatch(&mut self.request, data) } From 9b955747e46d671983d72caebf03791b64ddc5a2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 19 May 2017 03:29:08 -0700 Subject: [PATCH 139/297] Remove config global state. Use Responder::respond_to. This commit includes two major changes to core: 1. Configuration state is no longer global. The `config::active()` function has been removed. The active configuration can be retrieved via the `config` method on a `Rocket` instance. 2. The `Responder` trait has changed. `Responder::respond(self)` has been removed in favor of `Responder::respond_to(self, &Request)`. This allows responders to dynamically adjust their response based on the incoming request. Additionally, it includes the following changes to core and codegen: * The `Request::guard` method was added to allow for simple retrivial of request guards. * The `Request::limits` method was added to retrieve configured limits. * The `File` `Responder` implementation now uses a fixed size body instead of a chunked body. * The `Outcome::of(R)` method was removed while `Outcome::from Vec; @@ -71,7 +71,8 @@ pub fn error_decorator(ecx: &mut ExtCtxt, $req_ident: &'_b ::rocket::Request) -> ::rocket::response::Result<'_b> { let user_response = $user_fn_name($fn_arguments); - let response = ::rocket::response::Responder::respond(user_response)?; + let response = ::rocket::response::Responder::respond_to(user_response, + $req_ident)?; let status = ::rocket::http::Status::raw($code); ::rocket::response::Response::build().status(status).merge(response).ok() } diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index b060f300..44aa76f4 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -78,7 +78,7 @@ impl RouteGenerateExt for RouteParams { let mut items = ::rocket::request::FormItems::from($form_string); let obj = match ::rocket::request::FromForm::from_form_items(items.by_ref()) { Ok(v) => v, - Err(_) => return ::rocket::Outcome::Forward(_data) + Err(_) => return ::rocket::Outcome::Forward(__data) }; if !items.exhaust() { @@ -106,7 +106,7 @@ impl RouteGenerateExt for RouteParams { let ty = strip_ty_lifetimes(arg.ty.clone()); Some(quote_stmt!(ecx, let $name: $ty = - match ::rocket::data::FromData::from_data(_req, _data) { + match ::rocket::data::FromData::from_data(__req, __data) { ::rocket::Outcome::Success(d) => d, ::rocket::Outcome::Forward(d) => return ::rocket::Outcome::Forward(d), @@ -120,9 +120,9 @@ impl RouteGenerateExt for RouteParams { fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option { let param = self.query_param.as_ref(); let expr = quote_expr!(ecx, - match _req.uri().query() { + match __req.uri().query() { Some(query) => query, - None => return ::rocket::Outcome::Forward(_data) + None => return ::rocket::Outcome::Forward(__data) } ); @@ -149,13 +149,13 @@ impl RouteGenerateExt for RouteParams { // Note: the `None` case shouldn't happen if a route is matched. let ident = param.ident().prepend(PARAM_PREFIX); let expr = match param { - Param::Single(_) => quote_expr!(ecx, match _req.get_param_str($i) { + Param::Single(_) => quote_expr!(ecx, match __req.get_param_str($i) { Some(s) => <$ty as ::rocket::request::FromParam>::from_param(s), - None => return ::rocket::Outcome::Forward(_data) + None => return ::rocket::Outcome::Forward(__data) }), - Param::Many(_) => quote_expr!(ecx, match _req.get_raw_segments($i) { + Param::Many(_) => quote_expr!(ecx, match __req.get_raw_segments($i) { Some(s) => <$ty as ::rocket::request::FromSegments>::from_segments(s), - None => return ::rocket::Outcome::Forward(_data) + None => return ::rocket::Outcome::Forward(__data) }), }; @@ -166,7 +166,7 @@ impl RouteGenerateExt for RouteParams { Err(e) => { println!(" => Failed to parse '{}': {:?}", stringify!($original_ident), e); - return ::rocket::Outcome::Forward(_data) + return ::rocket::Outcome::Forward(__data) } }; ).expect("declared param parsing statement")); @@ -195,10 +195,10 @@ impl RouteGenerateExt for RouteParams { fn_param_statements.push(quote_stmt!(ecx, #[allow(non_snake_case)] let $ident: $ty = match - ::rocket::request::FromRequest::from_request(_req) { + ::rocket::request::FromRequest::from_request(__req) { ::rocket::outcome::Outcome::Success(v) => v, ::rocket::outcome::Outcome::Forward(_) => - return ::rocket::Outcome::forward(_data), + return ::rocket::Outcome::forward(__data), ::rocket::outcome::Outcome::Failure((code, _)) => { return ::rocket::Outcome::Failure(code) }, @@ -254,13 +254,13 @@ fn generic_route_decorator(known_method: Option>, // Allow the `unreachable_code` lint for those FromParam impls that have // an `Error` associated type of !. #[allow(unreachable_code)] - fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) + fn $route_fn_name<'_b>(__req: &'_b ::rocket::Request, __data: ::rocket::Data) -> ::rocket::handler::Outcome<'_b> { $param_statements $query_statement $data_statement let responder = $user_fn_name($fn_arguments); - ::rocket::handler::Outcome::of(responder) + ::rocket::handler::Outcome::from(__req, responder) } ).unwrap()); diff --git a/codegen/src/lints/utils.rs b/codegen/src/lints/utils.rs index baa3cee6..50a5175f 100644 --- a/codegen/src/lints/utils.rs +++ b/codegen/src/lints/utils.rs @@ -174,11 +174,11 @@ pub fn msg_and_help<'a, T: LintContext<'a>>(cx: &T, note: &str, help_sp: Option, help: &str) { - let mut b = cx.struct_span_lint(lint, msg_sp, msg); - b.note(note); + // Be conservative. If we don't know the receiver, don't emit the warning. if let Some(span) = help_sp { - b.span_help(span, help); + cx.struct_span_lint(lint, msg_sp, msg) + .note(note) + .span_help(span, help) + .emit() } - - b.emit(); } diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index ed29af91..4b3d0601 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -18,8 +18,7 @@ tera_templates = ["tera", "templates"] handlebars_templates = ["handlebars", "templates"] # Internal use only. -templates = ["serde", "serde_json", "lazy_static_macro", "glob"] -lazy_static_macro = ["lazy_static"] +templates = ["serde", "serde_json", "glob"] [dependencies] rocket = { version = "0.2.6", path = "../lib/" } @@ -36,5 +35,4 @@ rmp-serde = { version = "^0.13", optional = true } # Templating dependencies only. handlebars = { version = "^0.26.1", optional = true } glob = { version = "^0.2", optional = true } -lazy_static = { version = "^0.2", optional = true } tera = { version = "^0.10", optional = true } diff --git a/contrib/src/json.rs b/contrib/src/json.rs index 75d1115d..f0338e7e 100644 --- a/contrib/src/json.rs +++ b/contrib/src/json.rs @@ -1,7 +1,6 @@ use std::ops::{Deref, DerefMut}; use std::io::Read; -use rocket::config; use rocket::outcome::{Outcome, IntoOutcome}; use rocket::request::Request; use rocket::data::{self, Data, FromData}; @@ -80,6 +79,7 @@ impl JSON { /// let my_json = JSON(string); /// assert_eq!(my_json.into_inner(), "Hello".to_string()); /// ``` + #[inline(always)] pub fn into_inner(self) -> T { self.0 } @@ -97,10 +97,7 @@ impl FromData for JSON { return Outcome::Forward(data); } - let size_limit = config::active() - .and_then(|c| c.limits.get("json")) - .unwrap_or(LIMIT); - + let size_limit = request.limits().get("json").unwrap_or(LIMIT); serde_json::from_reader(data.open().take(size_limit)) .map(|val| JSON(val)) .map_err(|e| { error_!("Couldn't parse JSON body: {:?}", e); e }) @@ -112,9 +109,9 @@ impl FromData for JSON { /// JSON and a fixed-size body with the serialized value. If serialization /// fails, an `Err` of `Status::InternalServerError` is returned. impl Responder<'static> for JSON { - fn respond(self) -> response::Result<'static> { + fn respond_to(self, req: &Request) -> response::Result<'static> { serde_json::to_string(&self.0).map(|string| { - content::JSON(string).respond().unwrap() + content::JSON(string).respond_to(req).unwrap() }).map_err(|e| { error_!("JSON failed to serialize: {:?}", e); Status::InternalServerError @@ -125,12 +122,14 @@ impl Responder<'static> for JSON { impl Deref for JSON { type Target = T; + #[inline(always)] fn deref<'a>(&'a self) -> &'a T { &self.0 } } impl DerefMut for JSON { + #[inline(always)] fn deref_mut<'a>(&'a mut self) -> &'a mut T { &mut self.0 } diff --git a/contrib/src/lib.rs b/contrib/src/lib.rs index a90c4dbc..795a90e4 100644 --- a/contrib/src/lib.rs +++ b/contrib/src/lib.rs @@ -1,4 +1,7 @@ #![feature(drop_types_in_const, macro_reexport)] +#![cfg_attr(feature = "templates", feature(conservative_impl_trait))] +#![cfg_attr(feature = "templates", feature(associated_consts))] +#![cfg_attr(feature = "templates", feature(struct_field_attributes))] //! This crate contains officially sanctioned contributor libraries that provide //! functionality commonly used by Rocket applications. @@ -37,10 +40,6 @@ #[macro_use] extern crate log; #[macro_use] extern crate rocket; -#[cfg_attr(feature = "lazy_static_macro", macro_use)] -#[cfg(feature = "lazy_static_macro")] -extern crate lazy_static; - #[cfg(feature = "serde")] extern crate serde; diff --git a/contrib/src/msgpack.rs b/contrib/src/msgpack.rs index f95da24e..3bd7a76e 100644 --- a/contrib/src/msgpack.rs +++ b/contrib/src/msgpack.rs @@ -3,7 +3,6 @@ extern crate rmp_serde; use std::ops::{Deref, DerefMut}; use std::io::{Cursor, Read}; -use rocket::config; use rocket::outcome::{Outcome, IntoOutcome}; use rocket::request::Request; use rocket::data::{self, Data, FromData}; @@ -111,11 +110,8 @@ impl FromData for MsgPack { return Outcome::Forward(data); } - let size_limit = config::active() - .and_then(|c| c.limits.get("msgpack")) - .unwrap_or(LIMIT); - let mut buf = Vec::new(); + let size_limit = request.limits().get("msgpack").unwrap_or(LIMIT); if let Err(e) = data.open().take(size_limit).read_to_end(&mut buf) { let e = MsgPackError::InvalidDataRead(e); error_!("Couldn't read request data: {:?}", e); @@ -132,7 +128,7 @@ impl FromData for MsgPack { /// Content-Type `MsgPack` and a fixed-size body with the serialization. If /// serialization fails, an `Err` of `Status::InternalServerError` is returned. impl Responder<'static> for MsgPack { - fn respond(self) -> response::Result<'static> { + fn respond_to(self, _: &Request) -> response::Result<'static> { rmp_serde::to_vec(&self.0).map_err(|e| { error_!("MsgPack failed to serialize: {:?}", e); Status::InternalServerError diff --git a/contrib/src/templates/context.rs b/contrib/src/templates/context.rs new file mode 100644 index 00000000..39cfa090 --- /dev/null +++ b/contrib/src/templates/context.rs @@ -0,0 +1,131 @@ +use std::path::{Path, PathBuf}; +use std::collections::HashMap; + +use super::{Engines, TemplateInfo}; +use super::glob; + +use rocket::http::ContentType; + +pub struct Context { + /// The root of the template directory. + pub root: PathBuf, + /// Mapping from template name to its information. + pub templates: HashMap, + /// Mapping from template name to its information. + pub engines: Engines +} + +impl Context { + pub fn initialize(root: PathBuf) -> Option { + let mut templates: HashMap = HashMap::new(); + for ext in Engines::ENABLED_EXTENSIONS { + let mut glob_path = root.join("**").join("*"); + glob_path.set_extension(ext); + let glob_path = glob_path.to_str().expect("valid glob path string"); + + for path in glob(glob_path).unwrap().filter_map(Result::ok) { + let (name, data_type_str) = split_path(&root, &path); + if let Some(info) = templates.get(&*name) { + warn_!("Template name '{}' does not have a unique path.", name); + info_!("Existing path: {:?}", info.path); + info_!("Additional path: {:?}", path); + warn_!("Using existing path for template '{}'.", name); + continue; + } + + let data_type = data_type_str.as_ref() + .map(|ext| ContentType::from_extension(ext)) + .unwrap_or(ContentType::HTML); + + templates.insert(name, TemplateInfo { + path: path.to_path_buf(), + extension: ext.to_string(), + data_type: data_type, + }); + } + } + + Engines::init(&templates).map(|engines| { + Context { root, templates, engines } + }) + } +} + +/// Removes the file path's extension or does nothing if there is none. +fn remove_extension>(path: P) -> PathBuf { + let path = path.as_ref(); + let stem = match path.file_stem() { + Some(stem) => stem, + None => return path.to_path_buf() + }; + + match path.parent() { + Some(parent) => parent.join(stem), + None => PathBuf::from(stem) + } +} + +/// Splits a path into a name that may be used to identify the template, and the +/// template's data type, if any. +fn split_path(root: &Path, path: &Path) -> (String, Option) { + let rel_path = path.strip_prefix(root).unwrap().to_path_buf(); + let path_no_ext = remove_extension(&rel_path); + let data_type = path_no_ext.extension(); + let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned(); + + // Ensure template name consistency on Windows systems + if cfg!(windows) { + name = name.replace("\\", "/"); + } + + (name, data_type.map(|d| d.to_string_lossy().into_owned())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn template_path_index_html() { + for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] { + for filename in &["index.html.hbs", "index.html.tera"] { + let path = Path::new(root).join(filename); + let (name, data_type) = split_path(Path::new(root), &path); + + assert_eq!(name, "index"); + assert_eq!(data_type, Some("html".into())); + } + } + } + + #[test] + fn template_path_subdir_index_html() { + for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] { + for sub in &["a/", "a/b/", "a/b/c/", "a/b/c/d/"] { + for filename in &["index.html.hbs", "index.html.tera"] { + let path = Path::new(root).join(sub).join(filename); + let (name, data_type) = split_path(Path::new(root), &path); + + let expected_name = format!("{}index", sub); + assert_eq!(name, expected_name.as_str()); + assert_eq!(data_type, Some("html".into())); + } + } + } + } + + #[test] + fn template_path_doc_examples() { + fn name_for(path: &str) -> String { + split_path(Path::new("templates/"), &Path::new("templates/").join(path)).0 + } + + assert_eq!(name_for("index.html.hbs"), "index"); + assert_eq!(name_for("index.tera"), "index"); + assert_eq!(name_for("index.hbs"), "index"); + assert_eq!(name_for("dir/index.hbs"), "dir/index"); + assert_eq!(name_for("dir/index.html.tera"), "dir/index"); + assert_eq!(name_for("index.template.html.hbs"), "index.template"); + assert_eq!(name_for("subdir/index.template.html.hbs"), "subdir/index.template"); + } +} diff --git a/contrib/src/templates/engine.rs b/contrib/src/templates/engine.rs new file mode 100644 index 00000000..0136c4b1 --- /dev/null +++ b/contrib/src/templates/engine.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; + +use super::serde::Serialize; +use super::TemplateInfo; + +#[cfg(feature = "tera_templates")] use super::tera_templates::Tera; +#[cfg(feature = "handlebars_templates")] use super::handlebars_templates::Handlebars; + +pub trait Engine: Send + Sync + 'static { + const EXT: &'static str; + + fn init(templates: &[(&str, &TemplateInfo)]) -> Option where Self: Sized; + fn render(&self, name: &str, context: C) -> Option; +} + +pub struct Engines { + #[cfg(feature = "tera_templates")] + tera: Tera, + #[cfg(feature = "handlebars_templates")] + handlebars: Handlebars, +} + +impl Engines { + pub const ENABLED_EXTENSIONS: &'static [&'static str] = &[ + #[cfg(feature = "tera_templates")] Tera::EXT, + #[cfg(feature = "handlebars_templates")] Handlebars::EXT, + ]; + + pub fn init(templates: &HashMap) -> Option { + fn inner(templates: &HashMap) -> Option { + let named_templates = templates.iter() + .filter(|&(_, i)| i.extension == E::EXT) + .map(|(k, i)| (k.as_str(), i)) + .collect::>(); + + E::init(&*named_templates) + } + + Some(Engines { + #[cfg(feature = "tera_templates")] + tera: match inner::(templates) { + Some(tera) => tera, + None => return None + }, + #[cfg(feature = "handlebars_templates")] + handlebars: match inner::(templates) { + Some(hb) => hb, + None => return None + }, + }) + } + + pub fn render(&self, name: &str, info: &TemplateInfo, c: C) -> Option + where C: Serialize + { + #[cfg(feature = "tera_templates")] + { + if info.extension == Tera::EXT { + return Engine::render(&self.tera, name, c); + } + } + + #[cfg(feature = "handlebars_templates")] + { + if info.extension == Handlebars::EXT { + return Engine::render(&self.handlebars, name, c); + } + } + + None + } +} diff --git a/contrib/src/templates/handlebars_templates.rs b/contrib/src/templates/handlebars_templates.rs index 85dcdec8..6e2d6e70 100644 --- a/contrib/src/templates/handlebars_templates.rs +++ b/contrib/src/templates/handlebars_templates.rs @@ -1,57 +1,40 @@ extern crate handlebars; use super::serde::Serialize; -use super::TemplateInfo; +use super::{Engine, TemplateInfo}; -use self::handlebars::Handlebars; +pub use self::handlebars::Handlebars; -static mut HANDLEBARS: Option = None; +impl Engine for Handlebars { + const EXT: &'static str = "hbs"; -pub const EXT: &'static str = "hbs"; - -// This function must be called a SINGLE TIME from A SINGLE THREAD for safety to -// hold here and in `render`. -pub unsafe fn register(templates: &[(&str, &TemplateInfo)]) -> bool { - if HANDLEBARS.is_some() { - error_!("Internal error: reregistering handlebars!"); - return false; - } - - let mut hb = Handlebars::new(); - let mut success = true; - for &(name, info) in templates { - let path = &info.full_path; - if let Err(e) = hb.register_template_file(name, path) { - error_!("Handlebars template '{}' failed registry: {:?}", name, e); - success = false; + fn init(templates: &[(&str, &TemplateInfo)]) -> Option { + let mut hb = Handlebars::new(); + for &(name, info) in templates { + let path = &info.path; + if let Err(e) = hb.register_template_file(name, path) { + error!("Error in Handlebars template '{}'.", name); + info_!("{}", e); + info_!("Template path: '{}'.", path.to_string_lossy()); + return None; + } } + + Some(hb) } - HANDLEBARS = Some(hb); - success -} - -pub fn render(name: &str, _info: &TemplateInfo, context: &T) -> Option - where T: Serialize -{ - let hb = match unsafe { HANDLEBARS.as_ref() } { - Some(hb) => hb, - None => { - error_!("Internal error: `render` called before handlebars init."); + fn render(&self, name: &str, context: C) -> Option { + if self.get_template(name).is_none() { + error_!("Handlebars template '{}' does not exist.", name); return None; } - }; - if hb.get_template(name).is_none() { - error_!("Handlebars template '{}' does not exist.", name); - return None; - } - - match hb.render(name, context) { - Ok(string) => Some(string), - Err(e) => { - error_!("Error rendering Handlebars template '{}': {}", name, e); - None + match self.render(name, &context) { + Ok(string) => Some(string), + Err(e) => { + error_!("Error rendering Handlebars template '{}': {}", name, e); + None + } } } } diff --git a/contrib/src/templates/macros.rs b/contrib/src/templates/macros.rs deleted file mode 100644 index d4649d73..00000000 --- a/contrib/src/templates/macros.rs +++ /dev/null @@ -1,53 +0,0 @@ -/// Returns a hashset with the extensions of all of the enabled template -/// engines from the set of template engined passed in. -macro_rules! engine_set { - ($($feature:expr => $engine:ident),+,) => ({ - type RegisterFn = for<'a, 'b> unsafe fn(&'a [(&'b str, &TemplateInfo)]) -> bool; - - let mut set = Vec::new(); - $( - #[cfg(feature = $feature)] - fn $engine(set: &mut Vec<(&'static str, RegisterFn)>) { - set.push(($engine::EXT, $engine::register)); - } - - #[cfg(not(feature = $feature))] - fn $engine(_: &mut Vec<(&'static str, RegisterFn)>) { } - - $engine(&mut set); - )+ - - set - }); -} - -/// Renders the template named `name` with the given template info `info` and -/// context `ctxt` using one of the templates in the template set passed in. It -/// does this by checking if the template's extension matches the engine's -/// extension, and if so, calls the engine's `render` method. All of this only -/// happens for engine's that have been enabled as features by the user. -macro_rules! render_set { - ($name:expr, $info:expr, $ctxt:expr, $($feature:expr => $engine:ident),+,) => ({ - $( - #[cfg(feature = $feature)] - fn $engine(name: &str, info: &TemplateInfo, c: &T) - -> Option