diff --git a/contrib/codegen/src/database.rs b/contrib/codegen/src/database.rs index 12bb068e..4c4a6725 100644 --- a/contrib/codegen/src/database.rs +++ b/contrib/codegen/src/database.rs @@ -71,13 +71,13 @@ pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result ::rocket_contrib::databases); let request = quote!(::rocket::request); - let generated_types = quote_spanned! { span => + let request_guard_type = quote_spanned! { span => /// The request guard type. #vis struct #guard_type(#databases::Connection); }; Ok(quote! { - #generated_types + #request_guard_type impl #guard_type { /// Returns a fairing that initializes the associated database @@ -110,8 +110,8 @@ pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result #request::FromRequest<'a, 'r> for #guard_type { type Error = (); - async fn from_request(request: &'a #request::Request<'r>) -> #request::Outcome { - <#databases::Connection>::from_request(request).await.map(Self) + async fn from_request(req: &'a #request::Request<'r>) -> #request::Outcome { + <#databases::Connection>::from_request(req).await.map(Self) } } }.into()) diff --git a/contrib/codegen/tests/ui-fail-nightly/database-syntax.stderr b/contrib/codegen/tests/ui-fail-nightly/database-syntax.stderr index d4283c55..a7e7d5f6 100644 --- a/contrib/codegen/tests/ui-fail-nightly/database-syntax.stderr +++ b/contrib/codegen/tests/ui-fail-nightly/database-syntax.stderr @@ -1,4 +1,4 @@ -error: unexpected end of input, expected literal +error: unexpected end of input, expected string literal --> $DIR/database-syntax.rs:6:1 | 6 | #[database] diff --git a/contrib/codegen/tests/ui-fail-stable/database-syntax.stderr b/contrib/codegen/tests/ui-fail-stable/database-syntax.stderr index 740dda25..ae39a3ad 100644 --- a/contrib/codegen/tests/ui-fail-stable/database-syntax.stderr +++ b/contrib/codegen/tests/ui-fail-stable/database-syntax.stderr @@ -1,4 +1,4 @@ -error: unexpected end of input, expected literal +error: unexpected end of input, expected string literal --> $DIR/database-syntax.rs:6:1 | 6 | #[database] diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index 60797a2c..c7cfabff 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -14,7 +14,10 @@ edition = "2018" [features] # Internal use only. templates = ["serde", "serde_json", "glob", "notify"] -databases = ["r2d2", "tokio/blocking", "tokio/rt-threaded", "rocket_contrib_codegen/database_attribute"] +databases = [ + "serde", "r2d2", "tokio/blocking", "tokio/rt-threaded", + "rocket_contrib_codegen/database_attribute" +] # User-facing features. default = ["json", "serve"] diff --git a/contrib/lib/src/databases.rs b/contrib/lib/src/databases.rs index c416ef7b..b5d771b3 100644 --- a/contrib/lib/src/databases.rs +++ b/contrib/lib/src/databases.rs @@ -40,7 +40,9 @@ //! See [Provided](#provided) for a list of supported database and their //! associated feature name. //! -//! In `Rocket.toml` or the equivalent via environment variables: +//! In whichever configuration source you choose, configure a `databases` +//! dictionary with an internal dictionary for each database, here `sqlite_logs` +//! in a TOML source: //! //! ```toml //! [global.databases] @@ -97,8 +99,9 @@ //! //! ## Configuration //! -//! Databases can be configured via various mechanisms: `Rocket.toml`, -//! procedurally via `rocket::custom()`, or via environment variables. +//! Databases can be configured as any other values. Using the default +//! configuration provider, either via `Rocket.toml` or environment variables. +//! You can also use a custom provider. //! //! ### `Rocket.toml` //! @@ -138,31 +141,23 @@ //! The example below does just this: //! //! ```rust -//! #[macro_use] extern crate rocket; +//! # #[cfg(feature = "diesel_sqlite_pool")] { +//! use rocket::figment::{value::{Map, Value}, util::map}; //! -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! use std::collections::HashMap; -//! use rocket::config::{Config, Environment, Value}; +//! #[rocket::launch] +//! fn rocket() -> _ { +//! let db: Map<_, Value> = map! { +//! "url" => "db.sqlite".into(), +//! "pool_size" => 10.into() +//! }; //! -//! #[launch] -//! fn rocket() -> rocket::Rocket { -//! let mut database_config = HashMap::new(); -//! let mut databases = HashMap::new(); +//! let figment = rocket::Config::figment() +//! .merge(("databases", map!["my_db" => db])); //! -//! // This is the same as the following TOML: -//! // my_db = { url = "database.sqlite" } -//! database_config.insert("url", Value::from("database.sqlite")); -//! databases.insert("my_db", Value::from(database_config)); -//! -//! let config = Config::build(Environment::Development) -//! .extra("databases", databases) -//! .finalize() -//! .unwrap(); -//! -//! rocket::custom(config) +//! rocket::custom(figment) //! } -//! # } fn main() {} +//! # rocket(); +//! # } //! ``` //! //! ### Environment Variables @@ -246,33 +241,22 @@ //! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket_contrib; //! # -//! # #[cfg(feature = "diesel_sqlite_pool")] -//! # mod test { -//! # use std::collections::HashMap; -//! # use rocket::config::{Config, Environment, Value}; -//! # +//! # #[cfg(feature = "diesel_sqlite_pool")] { +//! # use rocket::figment::{value::{Map, Value}, util::map}; //! use rocket_contrib::databases::diesel; //! //! #[database("my_db")] //! struct MyDatabase(diesel::SqliteConnection); //! //! #[launch] -//! fn rocket() -> rocket::Rocket { -//! # let mut db_config = HashMap::new(); -//! # let mut databases = HashMap::new(); -//! # -//! # db_config.insert("url", Value::from("database.sqlite")); -//! # db_config.insert("pool_size", Value::from(10)); -//! # databases.insert("my_db", Value::from(db_config)); -//! # -//! # let config = Config::build(Environment::Development) -//! # .extra("databases", databases) -//! # .finalize() -//! # .unwrap(); -//! # -//! rocket::custom(config).attach(MyDatabase::fairing()) +//! fn rocket() -> _ { +//! # let db: Map<_, Value> = map![ +//! # "url" => "db.sqlite".into(), "pool_size" => 10.into() +//! # ]; +//! # let figment = rocket::Config::figment().merge(("databases", map!["my_db" => db])); +//! rocket::custom(figment).attach(MyDatabase::fairing()) //! } -//! # } fn main() {} +//! # } //! ``` //! //! ## Handlers @@ -376,22 +360,23 @@ pub extern crate r2d2; -#[cfg(any(feature = "diesel_sqlite_pool", - feature = "diesel_postgres_pool", - feature = "diesel_mysql_pool"))] +#[cfg(any( + feature = "diesel_sqlite_pool", + feature = "diesel_postgres_pool", + feature = "diesel_mysql_pool" +))] pub extern crate diesel; -use std::fmt::{self, Display, Formatter}; use std::marker::PhantomData; use std::sync::Arc; -use rocket::config::{self, Value}; use rocket::fairing::{AdHoc, Fairing}; use rocket::request::{Request, Outcome, FromRequest}; use rocket::outcome::IntoOutcome; use rocket::http::Status; use rocket::tokio::sync::{OwnedSemaphorePermit, Semaphore, Mutex}; +use rocket::tokio::time::timeout; use self::r2d2::ManageConnection; @@ -425,7 +410,7 @@ use self::r2d2::ManageConnection; /// [`database_config`]`("my_database", &config)`: /// /// ```rust,ignore -/// DatabaseConfig { +/// Config { /// url: "dummy_db.sqlite", /// pool_size: 10, /// extras: { @@ -434,16 +419,51 @@ use self::r2d2::ManageConnection; /// }, /// } /// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct DatabaseConfig<'a> { - /// The connection URL specified in the Rocket configuration. - pub url: &'a str, - /// The size of the pool to be initialized. Defaults to the number of - /// Rocket workers. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Config { + /// Connection URL specified in the Rocket configuration. + pub url: String, + /// Initial pool size. Defaults to the number of Rocket workers. pub pool_size: u32, - /// Any extra options that are included in the configuration, **excluding** - /// the url and pool_size. - pub extras: rocket::config::Map, + /// How long to wait, in seconds, for a new connection before timing out. + /// Defaults to `5`. + // FIXME: Use `time`. + pub timeout: u8, +} + +use serde::{Serialize, Deserialize}; +use rocket::figment::{Figment, Error, providers::Serialized}; + +impl Config { + /// Retrieves the database configuration for the database named `name`. + /// + /// This function is primarily used by the code generated by the `#[database]` + /// attribute. + /// + /// # Example + /// + /// Consider the following configuration: + /// + /// ```toml + /// [global.databases] + /// my_db = { url = "db/db.sqlite", pool_size = 25 } + /// my_other_db = { url = "mysql://root:root@localhost/database" } + /// ``` + /// + /// The following example uses `database_config` to retrieve the configurations + /// for the `my_db` and `my_other_db` databases: + /// + /// ```rust + /// + /// ``` + pub fn from(cargo: &rocket::Cargo, db: &str) -> Result { + let db_key = format!("databases.{}", db); + let key = |name: &str| format!("{}.{}", db_key, name); + Figment::from(cargo.figment()) + .merge(Serialized::default(&key("pool_size"), cargo.config().workers)) + .merge(Serialized::default(&key("timeout"), 5)) + .extract_inner::(&db_key) + } } /// A wrapper around `r2d2::Error`s or a custom database error type. @@ -458,143 +478,6 @@ pub enum DbError { PoolError(r2d2::Error), } -/// Error returned on invalid database configurations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConfigError { - /// The `databases` configuration key is missing or is empty. - MissingTable, - /// The requested database configuration key is missing from the active - /// configuration. - MissingKey, - /// The configuration associated with the key isn't a - /// [`Table`](rocket::config::Table). - MalformedConfiguration, - /// The required `url` key is missing. - MissingUrl, - /// The value for `url` isn't a string. - MalformedUrl, - /// The `pool_size` exceeds `u32::max_value()` or is negative. - InvalidPoolSize(i64), -} - -/// Retrieves the database configuration for the database named `name`. -/// -/// This function is primarily used by the code generated by the `#[database]` -/// attribute. -/// -/// # Example -/// -/// Consider the following configuration: -/// -/// ```toml -/// [global.databases] -/// my_db = { url = "db/db.sqlite", pool_size = 25 } -/// my_other_db = { url = "mysql://root:root@localhost/database" } -/// ``` -/// -/// The following example uses `database_config` to retrieve the configurations -/// for the `my_db` and `my_other_db` databases: -/// -/// ```rust -/// # extern crate rocket; -/// # extern crate rocket_contrib; -/// # -/// # use std::{collections::BTreeMap, mem::drop}; -/// # use rocket::{fairing::AdHoc, config::{Config, Environment, Value}}; -/// use rocket_contrib::databases::{database_config, ConfigError}; -/// -/// # let mut databases = BTreeMap::new(); -/// # -/// # let mut my_db = BTreeMap::new(); -/// # my_db.insert("url".to_string(), Value::from("db/db.sqlite")); -/// # my_db.insert("pool_size".to_string(), Value::from(25)); -/// # -/// # let mut my_other_db = BTreeMap::new(); -/// # my_other_db.insert("url".to_string(), -/// # Value::from("mysql://root:root@localhost/database")); -/// # -/// # databases.insert("my_db".to_string(), Value::from(my_db)); -/// # databases.insert("my_other_db".to_string(), Value::from(my_other_db)); -/// # -/// # let config = Config::build(Environment::Development) -/// # .extra("databases", databases) -/// # .expect("custom config okay"); -/// # -/// # rocket::custom(config).attach(AdHoc::on_attach("Testing", |mut rocket| async { -/// # { -/// let rocket_config = rocket.config().await; -/// let config = database_config("my_db", rocket_config).unwrap(); -/// assert_eq!(config.url, "db/db.sqlite"); -/// assert_eq!(config.pool_size, 25); -/// -/// let other_config = database_config("my_other_db", rocket_config).unwrap(); -/// assert_eq!(other_config.url, "mysql://root:root@localhost/database"); -/// -/// let error = database_config("invalid_db", rocket_config).unwrap_err(); -/// assert_eq!(error, ConfigError::MissingKey); -/// # } -/// # -/// # Ok(rocket) -/// # })); -/// ``` -pub fn database_config<'a>( - name: &str, - from: &'a config::Config -) -> Result, ConfigError> { - // Find the first `databases` config that's a table with a key of 'name' - // equal to `name`. - let connection_config = from.get_table("databases") - .map_err(|_| ConfigError::MissingTable)? - .get(name) - .ok_or(ConfigError::MissingKey)? - .as_table() - .ok_or(ConfigError::MalformedConfiguration)?; - - let maybe_url = connection_config.get("url") - .ok_or(ConfigError::MissingUrl)?; - - let url = maybe_url.as_str().ok_or(ConfigError::MalformedUrl)?; - - let pool_size = connection_config.get("pool_size") - .and_then(Value::as_integer) - .unwrap_or(from.workers as i64); - - if pool_size < 1 || pool_size > u32::max_value() as i64 { - return Err(ConfigError::InvalidPoolSize(pool_size)); - } - - let mut extras = connection_config.clone(); - extras.remove("url"); - extras.remove("pool_size"); - - Ok(DatabaseConfig { url, pool_size: pool_size as u32, extras: extras }) -} - -impl<'a> Display for ConfigError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - ConfigError::MissingTable => { - write!(f, "A table named `databases` was not found for this configuration") - }, - ConfigError::MissingKey => { - write!(f, "An entry in the `databases` table was not found for this key") - }, - ConfigError::MalformedConfiguration => { - write!(f, "The configuration for this database is malformed") - } - ConfigError::MissingUrl => { - write!(f, "The connection URL is missing for this database") - }, - ConfigError::MalformedUrl => { - write!(f, "The specified connection URL is malformed") - }, - ConfigError::InvalidPoolSize(invalid_size) => { - write!(f, "'{}' is not a valid value for `pool_size`", invalid_size) - }, - } - } -} - /// Trait implemented by `r2d2`-based database adapters. /// /// # Provided Implementations @@ -629,7 +512,7 @@ impl<'a> Display for ConfigError { /// `Poolable` for `foo::Connection`: /// /// ```rust -/// use rocket_contrib::databases::{r2d2, DbError, DatabaseConfig, Poolable}; +/// use rocket_contrib::databases::{r2d2, DbError, Config, Poolable}; /// # mod foo { /// # use std::fmt; /// # use rocket_contrib::databases::r2d2; @@ -661,8 +544,8 @@ impl<'a> Display for ConfigError { /// type Manager = foo::ConnectionManager; /// type Error = DbError; /// -/// fn pool(config: DatabaseConfig) -> Result, Self::Error> { -/// let manager = foo::ConnectionManager::new(config.url) +/// fn pool(config: &Config) -> Result, Self::Error> { +/// let manager = foo::ConnectionManager::new(&config.url) /// .map_err(DbError::Custom)?; /// /// r2d2::Pool::builder() @@ -692,7 +575,7 @@ pub trait Poolable: Send + Sized + 'static { /// Creates an `r2d2` connection pool for `Manager::Connection`, returning /// the pool on success. - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error>; + fn pool(config: &Config) -> Result, Self::Error>; } #[cfg(feature = "diesel_sqlite_pool")] @@ -700,8 +583,8 @@ impl Poolable for diesel::SqliteConnection { type Manager = diesel::r2d2::ConnectionManager; type Error = r2d2::Error; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let manager = diesel::r2d2::ConnectionManager::new(config.url); + fn pool(config: &Config) -> Result, Self::Error> { + let manager = diesel::r2d2::ConnectionManager::new(&config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } @@ -711,8 +594,8 @@ impl Poolable for diesel::PgConnection { type Manager = diesel::r2d2::ConnectionManager; type Error = r2d2::Error; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let manager = diesel::r2d2::ConnectionManager::new(config.url); + fn pool(config: &Config) -> Result, Self::Error> { + let manager = diesel::r2d2::ConnectionManager::new(&config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } @@ -722,8 +605,8 @@ impl Poolable for diesel::MysqlConnection { type Manager = diesel::r2d2::ConnectionManager; type Error = r2d2::Error; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let manager = diesel::r2d2::ConnectionManager::new(config.url); + fn pool(config: &Config) -> Result, Self::Error> { + let manager = diesel::r2d2::ConnectionManager::new(&config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } @@ -734,7 +617,7 @@ impl Poolable for postgres::Client { type Manager = r2d2_postgres::PostgresConnectionManager; type Error = DbError; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { + fn pool(config: &Config) -> Result, Self::Error> { let manager = r2d2_postgres::PostgresConnectionManager::new( config.url.parse().map_err(DbError::Custom)?, postgres::tls::NoTls, @@ -750,8 +633,8 @@ impl Poolable for mysql::Conn { type Manager = r2d2_mysql::MysqlConnectionManager; type Error = r2d2::Error; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let opts = mysql::OptsBuilder::from_opts(config.url); + fn pool(config: &Config) -> Result, Self::Error> { + let opts = mysql::OptsBuilder::from_opts(&config.url); let manager = r2d2_mysql::MysqlConnectionManager::new(opts); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } @@ -762,9 +645,8 @@ impl Poolable for rusqlite::Connection { type Manager = r2d2_sqlite::SqliteConnectionManager; type Error = r2d2::Error; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let manager = r2d2_sqlite::SqliteConnectionManager::file(config.url); - + fn pool(config: &Config) -> Result, Self::Error> { + let manager = r2d2_sqlite::SqliteConnectionManager::file(&*config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager) } } @@ -774,8 +656,8 @@ impl Poolable for memcache::Client { type Manager = r2d2_memcache::MemcacheConnectionManager; type Error = DbError; - fn pool(config: DatabaseConfig<'_>) -> Result, Self::Error> { - let manager = r2d2_memcache::MemcacheConnectionManager::new(config.url); + fn pool(config: &Config) -> Result, Self::Error> { + let manager = r2d2_memcache::MemcacheConnectionManager::new(&*config.url); r2d2::Pool::builder().max_size(config.pool_size).build(manager).map_err(DbError::PoolError) } } @@ -786,11 +668,23 @@ impl Poolable for memcache::Client { /// types are properly checked. #[doc(hidden)] pub struct ConnectionPool { + config: Config, pool: r2d2::Pool, semaphore: Arc, _marker: PhantomData K>, } +impl Clone for ConnectionPool { + fn clone(&self) -> Self { + ConnectionPool { + config: self.config.clone(), + pool: self.pool.clone(), + semaphore: self.semaphore.clone(), + _marker: PhantomData + } + } +} + /// Unstable internal details of generated code for the #[database] attribute. /// /// This type is implemented here instead of in generated code to ensure all @@ -816,30 +710,31 @@ async fn run_blocking(job: F) -> R } impl ConnectionPool { - pub fn fairing(fairing_name: &'static str, config_name: &'static str) -> impl Fairing { + pub fn fairing(fairing_name: &'static str, db_name: &'static str) -> impl Fairing { AdHoc::on_attach(fairing_name, move |mut rocket| async move { - let config = database_config(config_name, rocket.config().await); - let pool = config.map(|c| (c.pool_size, C::pool(c))); + let config = match Config::from(rocket.inspect().await, db_name) { + Ok(config) => config, + Err(config_error) => { + rocket::error!("database configuration error for '{}'", db_name); + error_!("{}", config_error); + return Err(rocket); + } + }; - match pool { - Ok((size, Ok(pool))) => { + match C::pool(&config) { + Ok(pool) => { + let pool_size = config.pool_size; let managed = ConnectionPool:: { - pool, - semaphore: Arc::new(Semaphore::new(size as usize)), + config, pool, + semaphore: Arc::new(Semaphore::new(pool_size as usize)), _marker: PhantomData, }; + Ok(rocket.manage(managed)) }, - Err(config_error) => { - rocket::logger::error( - &format!("Database configuration failure: '{}'", config_name)); - rocket::logger::error_(&config_error.to_string()); - Err(rocket) - }, - Ok((_, Err(pool_error))) => { - rocket::logger::error( - &format!("Failed to initialize pool for '{}'", config_name)); - rocket::logger::error_(&format!("{:?}", pool_error)); + Err(pool_error) => { + rocket::error!("failed to initialize pool for '{}'", db_name); + error_!("{:?}", pool_error); Err(rocket) }, } @@ -847,28 +742,24 @@ impl ConnectionPool { } async fn get(&self) -> Result, ()> { - // TODO: Make timeout configurable. - let permit = match tokio::time::timeout( - std::time::Duration::from_secs(5), - self.semaphore.clone().acquire_owned() - ).await { + let duration = std::time::Duration::from_secs(self.config.timeout as u64); + let permit = match timeout(duration, self.semaphore.clone().acquire_owned()).await { Ok(p) => p, Err(_) => { - error_!("Failed to get a database connection within the timeout."); + error_!("database connection retrieval timed out"); return Err(()); } }; - // TODO: Make timeout configurable. let pool = self.pool.clone(); - match run_blocking(move || pool.get_timeout(std::time::Duration::from_secs(5))).await { + match run_blocking(move || pool.get_timeout(duration)).await { Ok(c) => Ok(Connection { connection: Arc::new(Mutex::new(Some(c))), permit: Some(permit), _marker: PhantomData, }), Err(e) => { - error_!("Failed to get a database connection: {}", e); + error_!("failed to get a database connection: {}", e); Err(()) } } @@ -878,12 +769,14 @@ impl ConnectionPool { pub async fn get_one(cargo: &rocket::Cargo) -> Option> { match cargo.state::() { Some(pool) => pool.get().await.ok(), - None => { - error_!("Database fairing was not attached for {}", std::any::type_name::()); - None - } + None => None } } + + #[inline] + pub async fn get_pool(cargo: &rocket::Cargo) -> Option { + cargo.state::().map(|pool| pool.clone()) + } } impl Connection { @@ -911,7 +804,8 @@ impl Drop for Connection { if let Some(conn) = connection.take() { drop(conn); } - // NB: Explicitly dropping the permit here so that it's only + + // Explicitly dropping the permit here so that it's only // released after the connection is. drop(permit); }) @@ -934,201 +828,3 @@ impl<'a, 'r, K: 'static, C: Poolable> FromRequest<'a, 'r> for Connection { } } } - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - use rocket::{Config, config::{Environment, Value}}; - use super::{ConfigError::*, database_config}; - - #[test] - fn no_database_entry_in_config_returns_error() { - let config = Config::build(Environment::Development) - .finalize() - .unwrap(); - let database_config_result = database_config("dummy_db", &config); - - assert_eq!(Err(MissingTable), database_config_result); - } - - #[test] - fn no_matching_connection_returns_error() { - // Laboriously setup the config extras - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); - connection_config.insert("pool_size".to_string(), Value::from(10)); - database_extra.insert("dummy_db".to_string(), Value::from(connection_config)); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("real_db", &config); - - assert_eq!(Err(MissingKey), database_config_result); - } - - #[test] - fn incorrectly_structured_config_returns_error() { - let mut database_extra = BTreeMap::new(); - let connection_config = vec!["url", "dummy_db.slqite"]; - database_extra.insert("dummy_db".to_string(), Value::from(connection_config)); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("dummy_db", &config); - - assert_eq!(Err(MalformedConfiguration), database_config_result); - } - - #[test] - fn missing_connection_string_returns_error() { - let mut database_extra = BTreeMap::new(); - let connection_config: BTreeMap = BTreeMap::new(); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("dummy_db", &config); - - assert_eq!(Err(MissingUrl), database_config_result); - } - - #[test] - fn invalid_connection_string_returns_error() { - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from(42)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("dummy_db", &config); - - assert_eq!(Err(MalformedUrl), database_config_result); - } - - #[test] - fn negative_pool_size_returns_error() { - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); - connection_config.insert("pool_size".to_string(), Value::from(-1)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("dummy_db", &config); - - assert_eq!(Err(InvalidPoolSize(-1)), database_config_result); - } - - #[test] - fn pool_size_beyond_u32_max_returns_error() { - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - let over_max = (u32::max_value()) as i64 + 1; - connection_config.insert("url".to_string(), Value::from("dummy_db.sqlite")); - connection_config.insert("pool_size".to_string(), Value::from(over_max)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config_result = database_config("dummy_db", &config); - - // The size of `0` is an overflow wrap-around - assert_eq!(Err(InvalidPoolSize(over_max)), database_config_result); - } - - #[test] - fn happy_path_database_config() { - let url = "dummy_db.sqlite"; - let pool_size = 10; - - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from(url)); - connection_config.insert("pool_size".to_string(), Value::from(pool_size)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config = database_config("dummy_db", &config).unwrap(); - - assert_eq!(url, database_config.url); - assert_eq!(pool_size, database_config.pool_size); - assert_eq!(0, database_config.extras.len()); - } - - #[test] - fn extras_do_not_contain_required_keys() { - let url = "dummy_db.sqlite"; - let pool_size = 10; - - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from(url)); - connection_config.insert("pool_size".to_string(), Value::from(pool_size)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config = database_config("dummy_db", &config).unwrap(); - - assert_eq!(url, database_config.url); - assert_eq!(pool_size, database_config.pool_size); - assert_eq!(false, database_config.extras.contains_key("url")); - assert_eq!(false, database_config.extras.contains_key("pool_size")); - } - - #[test] - fn extra_values_are_placed_in_extras_map() { - let url = "dummy_db.sqlite"; - let pool_size = 10; - let tls_cert = "certs.pem"; - let tls_key = "key.pem"; - - let mut database_extra = BTreeMap::new(); - let mut connection_config = BTreeMap::new(); - connection_config.insert("url".to_string(), Value::from(url)); - connection_config.insert("pool_size".to_string(), Value::from(pool_size)); - connection_config.insert("certs".to_string(), Value::from(tls_cert)); - connection_config.insert("key".to_string(), Value::from(tls_key)); - database_extra.insert("dummy_db", connection_config); - - let config = Config::build(Environment::Development) - .extra("databases", database_extra) - .finalize() - .unwrap(); - - let database_config = database_config("dummy_db", &config).unwrap(); - - assert_eq!(url, database_config.url); - assert_eq!(pool_size, database_config.pool_size); - assert_eq!(true, database_config.extras.contains_key("certs")); - assert_eq!(true, database_config.extras.contains_key("key")); - } -} diff --git a/contrib/lib/src/helmet/helmet.rs b/contrib/lib/src/helmet/helmet.rs index 6eb2837d..71783521 100644 --- a/contrib/lib/src/helmet/helmet.rs +++ b/contrib/lib/src/helmet/helmet.rs @@ -203,7 +203,7 @@ impl Fairing for SpaceHelmet { fn on_launch(&self, cargo: &Cargo) { if cargo.config().tls_enabled() - && !cargo.config().environment.is_dev() + && cargo.figment().profile() != rocket::Config::DEBUG_PROFILE && !self.is_enabled::() { warn_!("Space Helmet: deploying with TLS without enabling HSTS."); diff --git a/contrib/lib/src/templates/fairing.rs b/contrib/lib/src/templates/fairing.rs index 17a98cc1..5969f9b4 100644 --- a/contrib/lib/src/templates/fairing.rs +++ b/contrib/lib/src/templates/fairing.rs @@ -1,7 +1,6 @@ use crate::templates::{DEFAULT_TEMPLATE_DIR, Context, Engines}; use rocket::Rocket; -use rocket::config::ConfigError; use rocket::fairing::{Fairing, Info, Kind}; pub(crate) use self::context::ContextManager; @@ -152,19 +151,31 @@ impl Fairing for TemplateFairing { /// template engines. In debug mode, the `ContextManager::new` method /// initializes a directory watcher for auto-reloading of templates. async fn on_attach(&self, mut rocket: Rocket) -> Result { - let config = rocket.config().await; - let mut template_root = config.root_relative(DEFAULT_TEMPLATE_DIR); - match config.get_str("template_dir") { - Ok(dir) => template_root = config.root_relative(dir), - Err(ConfigError::Missing(_)) => { /* ignore missing */ } + use rocket::figment::{Source, value::magic::RelativePathBuf}; + + let configured_dir = rocket.figment().await + .extract_inner::("template_dir") + .map(|path| path.relative()); + + let path = match configured_dir { + Ok(dir) => dir, + Err(e) if e.missing() => DEFAULT_TEMPLATE_DIR.into(), Err(e) => { - e.pretty_print(); - warn_!("Using default templates directory '{:?}'", template_root); + rocket::config::pretty_print_error(e); + return Err(rocket); } }; - match Context::initialize(template_root) { + let root = Source::from(&*path); + match Context::initialize(path) { Some(mut ctxt) => { + use rocket::{logger::PaintExt, yansi::Paint}; + use crate::templates::Engines; + + info!("{}{}", Paint::emoji("📐 "), Paint::magenta("Templating:")); + info_!("directory: {}", Paint::white(root)); + info_!("engines: {:?}", Paint::white(Engines::ENABLED_EXTENSIONS)); + (self.custom_callback)(&mut ctxt.engines); Ok(rocket.manage(ContextManager::new(ctxt))) } diff --git a/contrib/lib/tests/databases.rs b/contrib/lib/tests/databases.rs index 008e05c4..69b4a2c7 100644 --- a/contrib/lib/tests/databases.rs +++ b/contrib/lib/tests/databases.rs @@ -12,9 +12,8 @@ mod databases_tests { #[cfg(all(feature = "databases", feature = "sqlite_pool"))] #[cfg(test)] mod rusqlite_integration_test { - use rocket::config::{Config, Environment, Value, Map}; - use rocket_contrib::databases::rusqlite; use rocket_contrib::database; + use rocket_contrib::databases::rusqlite; use rusqlite::types::ToSql; @@ -27,18 +26,19 @@ mod rusqlite_integration_test { #[rocket::async_test] async fn test_db() { - let mut test_db: Map = Map::new(); - let mut test_db_opts: Map = Map::new(); - test_db_opts.insert("url".into(), Value::String(":memory:".into())); - test_db.insert("test_db".into(), Value::Table(test_db_opts.clone())); - test_db.insert("test_db_2".into(), Value::Table(test_db_opts)); - let config = Config::build(Environment::Development) - .extra("databases", Value::Table(test_db)) - .finalize() - .unwrap(); + use rocket::figment::{Figment, util::map}; - let mut rocket = rocket::custom(config).attach(SqliteDb::fairing()).attach(SqliteDb2::fairing()); - let conn = SqliteDb::get_one(rocket.inspect().await).await.expect("unable to get connection"); + let options = map!["url" => ":memory:"]; + let config = Figment::from(rocket::Config::default()) + .merge(("databases", map!["test_db" => &options])) + .merge(("databases", map!["test_db_2" => &options])); + + let mut rocket = rocket::custom(config) + .attach(SqliteDb::fairing()) + .attach(SqliteDb2::fairing()); + + let conn = SqliteDb::get_one(rocket.inspect().await).await + .expect("unable to get connection"); // Rusqlite's `transaction()` method takes `&mut self`; this tests that // the &mut method can be called inside the closure passed to `run()`. diff --git a/contrib/lib/tests/templates.rs b/contrib/lib/tests/templates.rs index e92d90bc..772c3491 100644 --- a/contrib/lib/tests/templates.rs +++ b/contrib/lib/tests/templates.rs @@ -6,7 +6,7 @@ mod templates_tests { use std::path::{Path, PathBuf}; use rocket::{Rocket, http::RawStr}; - use rocket::config::{Config, Environment}; + use rocket::config::Config; use rocket_contrib::templates::{Template, Metadata}; #[get("//")] @@ -27,11 +27,8 @@ mod templates_tests { } fn rocket() -> Rocket { - let config = Config::build(Environment::Development) - .extra("template_dir", template_root().to_str().expect("template directory")) - .expect("valid configuration"); - - rocket::custom(config).attach(Template::fairing()) + rocket::custom(Config::figment().merge(("template_dir", template_root()))) + .attach(Template::fairing()) .mount("/", routes![template_check, is_reloading]) } diff --git a/core/codegen/tests/route-ranking.rs b/core/codegen/tests/route-ranking.rs index 68d1038a..9efefbe1 100644 --- a/core/codegen/tests/route-ranking.rs +++ b/core/codegen/tests/route-ranking.rs @@ -41,12 +41,12 @@ fn get0b(_n: u8) { } #[test] fn test_rank_collision() { - use rocket::error::LaunchErrorKind; + use rocket::error::ErrorKind; let rocket = rocket::ignite().mount("/", routes![get0, get0b]); let client_result = Client::tracked(rocket); match client_result.as_ref().map_err(|e| e.kind()) { - Err(LaunchErrorKind::Collision(..)) => { /* o.k. */ }, + Err(ErrorKind::Collision(..)) => { /* o.k. */ }, Ok(_) => panic!("client succeeded unexpectedly"), Err(e) => panic!("expected collision, got {}", e) } diff --git a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr index 385f233b..0864618c 100644 --- a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr @@ -6,7 +6,7 @@ error: invalid path URI: expected token / but found a at index 0 | = help: expected path in origin form: "/path/" -error: invalid path URI: expected token / but none was found at index 0 +error: invalid path URI: unexpected EOF: expected token / at index 0 --> $DIR/route-path-bad-syntax.rs:8:8 | 8 | #[get("")] diff --git a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr index f3ad2517..ef7b7a64 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr @@ -42,17 +42,17 @@ error[E0277]: the trait bound `S: FromUriParam` is n | = note: required by `from_uri_param` -error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied +error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied --> $DIR/typed-uri-bad-type.rs:53:26 | 53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); - | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` + | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following implementations were found: > > > - = note: required because of the requirements on the impl of `FromUriParam>` for `Option` + = note: required because of the requirements on the impl of `FromUriParam>` for `std::option::Option` = note: required by `from_uri_param` error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied diff --git a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr index 5de5013a..f3eaf2e5 100644 --- a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr @@ -5,7 +5,7 @@ error: invalid path URI: expected token / but found a at index 0 5 | #[get("a")] | ^^^ -error: invalid path URI: expected token / but none was found at index 0 +error: invalid path URI: unexpected EOF: expected token / at index 0 --- help: expected path in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:8:7 | diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 63d6440b..2d8321d9 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -22,7 +22,7 @@ private-cookies = ["cookie/private", "cookie/key-expansion"] [dependencies] smallvec = "1.0" percent-encoding = "2" -hyper = { version = "0.13.0", default-features = false } +hyper = { version = "0.13.0", default-features = false, features = ["runtime"] } http = "0.2" mime = "0.3.13" time = "0.2.11" @@ -36,16 +36,13 @@ ref-cast = "1.0" uncased = "0.9" parking_lot = "0.11" either = "1" +pear = "0.2" [dependencies.cookie] git = "https://github.com/SergioBenitez/cookie-rs.git" -rev = "9675944" +rev = "1c3ca83" features = ["percent-encode"] -[dependencies.pear] -git = "https://github.com/SergioBenitez/Pear.git" -rev = "4b68055" - [dev-dependencies] rocket = { version = "0.5.0-dev", path = "../lib" } diff --git a/core/http/src/cookies.rs b/core/http/src/cookies.rs index 38ecdccb..e9ebd18f 100644 --- a/core/http/src/cookies.rs +++ b/core/http/src/cookies.rs @@ -20,9 +20,16 @@ mod key { pub struct Key; impl Key { + pub fn from(_: &[u8]) -> Self { Key } + pub fn derive_from(_: &[u8]) -> Self { Key } pub fn generate() -> Self { Key } pub fn try_generate() -> Option { Some(Key) } - pub fn derive_from(_bytes: &[u8]) -> Self { Key } + } + + impl PartialEq for Key { + fn eq(&self, _: &Self) -> bool { + true + } } } @@ -121,7 +128,7 @@ mod key { /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # +/// # #[cfg(feature = "private-cookies")] { /// use rocket::http::Status; /// use rocket::outcome::IntoOutcome; /// use rocket::request::{self, Request, FromRequest}; @@ -141,6 +148,7 @@ mod key { /// .or_forward(()) /// } /// } +/// # } /// # fn main() { } /// ``` /// diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs index 5a39a0cb..fe998abe 100644 --- a/core/http/src/listener.rs +++ b/core/http/src/listener.rs @@ -10,8 +10,8 @@ use hyper::server::accept::Accept; use log::{debug, error}; -use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::Delay; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::{TcpListener, TcpStream}; // TODO.async: 'Listener' and 'Connection' provide common enough functionality @@ -32,10 +32,10 @@ pub trait Connection: AsyncRead + AsyncWrite { fn remote_addr(&self) -> Option; } -/// This is a genericized version of hyper's AddrIncoming that is intended to be +/// This is a generic version of hyper's AddrIncoming that is intended to be /// usable with listeners other than a plain TCP stream, e.g. TLS and/or Unix -/// sockets. It does this by bridging the `Listener` trait to what hyper wants -/// (an Accept). This type is internal to Rocket. +/// sockets. It does so by bridging the `Listener` trait to what hyper wants (an +/// Accept). This type is internal to Rocket. #[must_use = "streams do nothing unless polled"] pub struct Incoming { listener: L, diff --git a/core/http/src/tls.rs b/core/http/src/tls.rs index ecda6540..90f8eca1 100644 --- a/core/http/src/tls.rs +++ b/core/http/src/tls.rs @@ -1,70 +1,49 @@ -use std::fs; +use std::io; use std::future::Future; -use std::io::{self, BufReader}; use std::net::SocketAddr; -use std::path::Path; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; +use rustls::internal::pemfile; +use rustls::{Certificate, PrivateKey, ServerConfig}; use tokio::net::{TcpListener, TcpStream}; - -use tokio_rustls::{TlsAcceptor, server::TlsStream}; +use tokio_rustls::{TlsAcceptor, Accept, server::TlsStream}; use tokio_rustls::rustls; -pub use rustls::internal::pemfile; -pub use rustls::{Certificate, PrivateKey, ServerConfig}; - use crate::listener::{Connection, Listener}; -#[derive(Debug)] -pub enum Error { - Io(io::Error), - BadCerts, - BadKeyCount, - BadKey, +fn load_certs(reader: &mut dyn io::BufRead) -> io::Result> { + pemfile::certs(reader) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid certificate")) } -// TODO.async: consider using async fs operations -pub fn load_certs>(path: P) -> Result, Error> { - let certfile = fs::File::open(path.as_ref()).map_err(|e| Error::Io(e))?; - let mut reader = BufReader::new(certfile); - pemfile::certs(&mut reader).map_err(|_| Error::BadCerts) -} - -pub fn load_private_key>(path: P) -> Result { - use std::io::Seek; - use std::io::BufRead; - - let keyfile = fs::File::open(path.as_ref()).map_err(Error::Io)?; - let mut reader = BufReader::new(keyfile); +fn load_private_key(reader: &mut dyn io::BufRead) -> io::Result { + use std::io::{Cursor, Error, Read, ErrorKind::Other}; // "rsa" (PKCS1) PEM files have a different first-line header than PKCS8 // PEM files, use that to determine the parse function to use. let mut first_line = String::new(); - reader.read_line(&mut first_line).map_err(Error::Io)?; - reader.seek(io::SeekFrom::Start(0)).map_err(Error::Io)?; + reader.read_line(&mut first_line)?; let private_keys_fn = match first_line.trim_end() { "-----BEGIN RSA PRIVATE KEY-----" => pemfile::rsa_private_keys, "-----BEGIN PRIVATE KEY-----" => pemfile::pkcs8_private_keys, - _ => return Err(Error::BadKey), + _ => return Err(Error::new(Other, "invalid key header")) }; - let key = private_keys_fn(&mut reader) - .map_err(|_| Error::BadKey) + let key = private_keys_fn(&mut Cursor::new(first_line).chain(reader)) + .map_err(|_| Error::new(Other, "invalid key file")) .and_then(|mut keys| match keys.len() { - 0 => Err(Error::BadKey), + 0 => Err(Error::new(Other, "no valid keys found; is the file malformed?")), 1 => Ok(keys.remove(0)), - _ => Err(Error::BadKeyCount), + n => Err(Error::new(Other, format!("expected 1 key, found {}", n))), })?; // Ensure we can use the key. - if rustls::sign::RSASigningKey::new(&key).is_err() { - Err(Error::BadKey) - } else { - Ok(key) - } + rustls::sign::RSASigningKey::new(&key) + .map_err(|_| Error::new(Other, "key parsed but is unusable")) + .map(|_| key) } pub struct TlsListener { @@ -75,7 +54,7 @@ pub struct TlsListener { enum TlsListenerState { Listening, - Accepting(Pin, io::Error>> + Send>>), + Accepting(Accept), } impl Listener for TlsListener { @@ -85,22 +64,21 @@ impl Listener for TlsListener { self.listener.local_addr().ok() } - fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll> { + fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll> { loop { - match &mut self.state { + match self.state { TlsListenerState::Listening => { match self.listener.poll_accept(cx) { Poll::Pending => return Poll::Pending, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), Poll::Ready(Ok((stream, _addr))) => { - self.state = TlsListenerState::Accepting(Box::pin(self.acceptor.accept(stream))); - } - Poll::Ready(Err(e)) => { - return Poll::Ready(Err(e)); + let fut = self.acceptor.accept(stream); + self.state = TlsListenerState::Accepting(fut); } } } - TlsListenerState::Accepting(fut) => { - match fut.as_mut().poll(cx) { + TlsListenerState::Accepting(ref mut fut) => { + match Pin::new(fut).poll(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(result) => { self.state = TlsListenerState::Listening; @@ -113,11 +91,21 @@ impl Listener for TlsListener { } } -pub async fn bind_tls( +pub async fn bind_tls( address: SocketAddr, - cert_chain: Vec, - key: PrivateKey + mut cert_chain: C, + mut private_key: K, ) -> io::Result { + let cert_chain = load_certs(&mut cert_chain).map_err(|e| { + let msg = format!("malformed TLS certificate chain: {}", e); + io::Error::new(e.kind(), msg) + })?; + + let key = load_private_key(&mut private_key).map_err(|e| { + let msg = format!("malformed TLS private key: {}", e); + io::Error::new(e.kind(), msg) + })?; + let listener = TcpListener::bind(address).await?; let client_auth = rustls::NoClientAuth::new(); diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 08579884..24036b16 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -19,7 +19,7 @@ edition = "2018" all-features = true [features] -default = ["secrets"] +default = [] tls = ["rocket_http/tls"] secrets = ["rocket_http/private-cookies"] @@ -29,7 +29,6 @@ rocket_http = { version = "0.5.0-dev", path = "../http" } futures = "0.3.0" yansi = "0.5" log = { version = "0.4", features = ["std"] } -toml = "0.5" num_cpus = "1.0" state = "0.4.1" time = "0.2.11" @@ -39,12 +38,12 @@ atty = "0.2" async-trait = "0.1" ref-cast = "1.0" atomic = "0.5" -ubyte = "0.10" parking_lot = "0.11" - -[dependencies.pear] -git = "https://github.com/SergioBenitez/Pear.git" -rev = "4b68055" +ubyte = {version = "0.10", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +figment = { version = "0.9.2", features = ["toml", "env"] } +rand = "0.7" +either = "1" [dependencies.tokio] version = "0.2.9" @@ -56,8 +55,7 @@ version_check = "0.9.1" [dev-dependencies] bencher = "0.1" -# TODO: Find a way to not depend on this. -lazy_static = "1.0" +figment = { version = "0.9.2", features = ["test"] } [[bench]] name = "format-routing" diff --git a/core/lib/benches/format-routing.rs b/core/lib/benches/format-routing.rs index 77c4113e..75fcf630 100644 --- a/core/lib/benches/format-routing.rs +++ b/core/lib/benches/format-routing.rs @@ -2,7 +2,6 @@ #[macro_use] extern crate bencher; use rocket::local::blocking::Client; -use rocket::config::{Environment, Config, LoggingLevel}; #[get("/", format = "application/json")] fn get() -> &'static str { "get" } @@ -11,8 +10,8 @@ fn get() -> &'static str { "get" } fn post() -> &'static str { "post" } fn rocket() -> rocket::Rocket { - let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); - rocket::custom(config.unwrap()).mount("/", routes![get, post]) + rocket::custom(rocket::Config::figment().merge(("log_level", "off"))) + .mount("/", routes![get, post]) } use bencher::Bencher; diff --git a/core/lib/benches/ranked-routing.rs b/core/lib/benches/ranked-routing.rs index 662776a8..07bd7ea9 100644 --- a/core/lib/benches/ranked-routing.rs +++ b/core/lib/benches/ranked-routing.rs @@ -1,8 +1,6 @@ #[macro_use] extern crate rocket; #[macro_use] extern crate bencher; -use rocket::config::{Environment, Config, LoggingLevel}; - #[get("/", format = "application/json", rank = 1)] fn get() -> &'static str { "json" } @@ -22,8 +20,7 @@ fn post2() -> &'static str { "html" } fn post3() -> &'static str { "plain" } fn rocket() -> rocket::Rocket { - let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); - rocket::custom(config.unwrap()) + rocket::custom(rocket::Config::figment().merge(("log_level", "off"))) .mount("/", routes![get, get2, get3]) .mount("/", routes![post, post2, post3]) } diff --git a/core/lib/benches/simple-routing.rs b/core/lib/benches/simple-routing.rs index 062792f0..186fe089 100644 --- a/core/lib/benches/simple-routing.rs +++ b/core/lib/benches/simple-routing.rs @@ -1,15 +1,11 @@ #[macro_use] extern crate rocket; #[macro_use] extern crate bencher; -use rocket::config::{Environment, Config, LoggingLevel}; use rocket::http::RawStr; #[get("/")] fn hello_world() -> &'static str { "Hello, world!" } -#[get("/")] -fn get_index() -> &'static str { "index" } - #[put("/")] fn put_index() -> &'static str { "index" } @@ -29,15 +25,15 @@ fn index_c() -> &'static str { "index" } fn index_dyn_a(_a: &RawStr) -> &'static str { "index" } fn hello_world_rocket() -> rocket::Rocket { - let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); - rocket::custom(config.unwrap()).mount("/", routes![hello_world]) + let config = rocket::Config::figment().merge(("log_level", "off")); + rocket::custom(config).mount("/", routes![hello_world]) } fn rocket() -> rocket::Rocket { - let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); - rocket::custom(config.unwrap()) - .mount("/", routes![get_index, put_index, post_index, index_a, - index_b, index_c, index_dyn_a]) + hello_world_rocket() + .mount("/", routes![ + put_index, post_index, index_a, index_b, index_c, index_dyn_a + ]) } use bencher::Bencher; diff --git a/core/lib/src/config/builder.rs b/core/lib/src/config/builder.rs deleted file mode 100644 index c29d849d..00000000 --- a/core/lib/src/config/builder.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use crate::config::{Result, Config, Value, Environment, LoggingLevel}; -use crate::data::Limits; - -/// Structure following the builder pattern for building `Config` structures. -#[derive(Clone)] -pub struct ConfigBuilder { - /// The environment that this configuration corresponds to. - pub environment: Environment, - /// The address to serve on. - pub address: String, - /// The port to serve on. - pub port: u16, - /// The number of workers to run in parallel. - pub workers: u16, - /// Keep-alive timeout in seconds or disabled if 0. - pub keep_alive: u32, - /// How much information to log. - pub log_level: LoggingLevel, - /// 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. - pub limits: Limits, - /// Any extra parameters that aren't part of Rocket's config. - pub extras: HashMap, - /// The root directory of this config, if any. - pub root: Option, -} - -impl ConfigBuilder { - /// Create a new `ConfigBuilder` instance using the default parameters from - /// the given `environment`. - /// - /// This method is typically called indirectly via [`Config::build()`]. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .port(700) - /// .workers(12) - /// .finalize(); - /// - /// # assert!(config.is_ok()); - /// ``` - pub fn new(environment: Environment) -> ConfigBuilder { - let config = Config::new(environment); - ConfigBuilder { - environment: config.environment, - address: config.address, - port: config.port, - workers: config.workers, - keep_alive: config.keep_alive.unwrap_or(0), - log_level: config.log_level, - secret_key: None, - tls: None, - limits: config.limits, - extras: config.extras, - root: None, - } - } - - /// Sets the `address` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .unwrap(); - /// - /// assert_eq!(config.address.as_str(), "127.0.0.1"); - /// ``` - pub fn address>(mut self, address: A) -> Self { - self.address = address.into(); - self - } - - /// Sets the `port` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .port(1329) - /// .unwrap(); - /// - /// assert_eq!(config.port, 1329); - /// ``` - #[inline] - pub fn port(mut self, port: u16) -> Self { - self.port = port; - self - } - - /// Sets `workers` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .workers(64) - /// .unwrap(); - /// - /// assert_eq!(config.workers, 64); - /// ``` - #[inline] - pub fn workers(mut self, workers: u16) -> Self { - self.workers = workers; - self - } - - /// Sets the keep-alive timeout to `timeout` seconds. If `timeout` is `0`, - /// keep-alive is disabled. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .keep_alive(10) - /// .unwrap(); - /// - /// assert_eq!(config.keep_alive, Some(10)); - /// - /// let config = Config::build(Environment::Staging) - /// .keep_alive(0) - /// .unwrap(); - /// - /// assert_eq!(config.keep_alive, None); - /// ``` - #[inline] - pub fn keep_alive(mut self, timeout: u32) -> Self { - self.keep_alive = timeout; - self - } - - /// Sets the `log_level` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment, LoggingLevel}; - /// - /// let config = Config::build(Environment::Staging) - /// .log_level(LoggingLevel::Critical) - /// .unwrap(); - /// - /// assert_eq!(config.log_level, LoggingLevel::Critical); - /// ``` - #[inline] - pub fn log_level(mut self, log_level: LoggingLevel) -> Self { - self.log_level = log_level; - self - } - - /// Sets the `secret_key` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment, LoggingLevel}; - /// - /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; - /// let mut config = Config::build(Environment::Staging) - /// .secret_key(key) - /// .unwrap(); - /// ``` - pub fn secret_key>(mut self, key: K) -> Self { - self.secret_key = Some(key.into()); - self - } - - /// Sets the `limits` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// use rocket::data::{Limits, ToByteUnit}; - /// - /// let mut config = Config::build(Environment::Staging) - /// .limits(Limits::new().limit("json", 5.mebibytes())) - /// .unwrap(); - /// ``` - pub fn limits(mut self, limits: Limits) -> Self { - self.limits = limits; - self - } - - /// Sets the TLS configuration in the configuration being built. - /// - /// Certificates are read from `certs_path`. The certificate chain must be - /// in X.509 PEM format. The private key is read from `key_path`. The - /// private key must be an RSA key in either PKCS#1 or PKCS#8 PEM format. - /// - /// # 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 = Some((certs_path.into(), key_path.into())); - self - } - - /// Sets the `environment` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .environment(Environment::Production) - /// .unwrap(); - /// - /// assert_eq!(config.environment, Environment::Production); - /// ``` - #[inline] - pub fn environment(mut self, env: Environment) -> Self { - self.environment = env; - self - } - - /// Sets the `root` in the configuration being built. - /// - /// # Example - /// - /// ```rust - /// # use std::path::Path; - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .root("/my_app/dir") - /// .unwrap(); - /// - /// assert_eq!(config.root().unwrap(), Path::new("/my_app/dir")); - /// ``` - pub fn root>(mut self, path: P) -> Self { - self.root = Some(path.as_ref().to_path_buf()); - self - } - - /// Adds an extra configuration parameter with `name` and `value` to the - /// configuration being built. The value can be any type that implements - /// `Into` including `&str`, `String`, `Vec>`, - /// `HashMap, V: Into>`, and most integer and float - /// types. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("pi", 3.14) - /// .extra("custom_dir", "/a/b/c") - /// .unwrap(); - /// - /// assert_eq!(config.get_float("pi"), Ok(3.14)); - /// assert_eq!(config.get_str("custom_dir"), Ok("/a/b/c")); - /// ``` - pub fn extra>(mut self, name: &str, value: V) -> Self { - self.extras.insert(name.into(), value.into()); - self - } - - /// Return the `Config` structure that was being built by this builder. - /// - /// # Errors - /// - /// If the address or secret key fail to parse, returns a `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .port(700) - /// .workers(12) - /// .keep_alive(0) - /// .finalize(); - /// - /// assert!(config.is_ok()); - /// - /// let config = Config::build(Environment::Staging) - /// .address("123.123.123.123.123 whoops!") - /// .finalize(); - /// - /// assert!(config.is_err()); - /// ``` - pub fn finalize(self) -> Result { - let mut config = Config::new(self.environment); - config.set_address(self.address)?; - config.set_port(self.port); - config.set_workers(self.workers); - config.set_keep_alive(self.keep_alive); - config.set_log_level(self.log_level); - config.set_extras(self.extras); - config.set_limits(self.limits); - - if let Some(root) = self.root { - config.set_root(root); - } - - if let Some((certs_path, key_path)) = self.tls { - config.set_tls(&certs_path, &key_path)?; - } - - if let Some(key) = self.secret_key { - config.set_secret_key(key)?; - } - - Ok(config) - } - - /// Return the `Config` structure that was being built by this builder. - /// - /// # Panics - /// - /// Panics if the supplied address, secret key, or TLS configuration fail to - /// parse. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .unwrap(); - /// - /// assert_eq!(config.address.as_str(), "127.0.0.1"); - /// ``` - #[inline(always)] - pub fn unwrap(self) -> Config { - self.finalize().expect("ConfigBuilder::unwrap() failed") - } - - /// Returns the `Config` structure that was being built by this builder. - /// - /// # Panics - /// - /// Panics if the supplied address, secret key, or TLS configuration fail to - /// parse. If a panic occurs, the error message `msg` is printed. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .expect("the configuration is bad!"); - /// - /// assert_eq!(config.address.as_str(), "127.0.0.1"); - /// ``` - #[inline(always)] - pub fn expect(self, msg: &str) -> Config { - self.finalize().expect(msg) - } -} diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index be622099..031e31a0 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -1,995 +1,336 @@ -use std::collections::HashMap; -use std::net::ToSocketAddrs; -use std::path::{Path, PathBuf}; -use std::convert::AsRef; -use std::fmt; +use std::net::{IpAddr, Ipv4Addr}; -use crate::http::private::cookie::Key; -use crate::config::Environment::*; -use crate::config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel}; -use crate::config::{FullConfig, Table, Value, Array, Datetime}; +use figment::{Figment, Profile, Provider, Metadata, error::Result}; +use figment::providers::{Serialized, Env, Toml, Format}; +use figment::value::{Map, Dict}; +use serde::{Deserialize, Serialize}; +use yansi::Paint; + +use crate::config::{SecretKey, TlsConfig, LogLevel}; use crate::data::Limits; -use super::custom_values::*; - -/// Structure for Rocket application configuration. +/// Rocket server configuration. /// -/// # Usage +/// See the [module level docs](crate::config) as well as the [configuration +/// guide] for further details. /// -/// A `Config` structure is typically built using [`Config::build()`] and -/// builder methods on the returned [`ConfigBuilder`] structure: +/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ /// -/// ```rust -/// use rocket::config::{Config, Environment}; +/// # Defaults /// -/// # #[allow(unused_variables)] -/// let config = Config::build(Environment::Staging) -/// .address("127.0.0.1") -/// .port(700) -/// .workers(12) -/// .unwrap(); -/// ``` +/// All configuration values have a default, documented in the [fields](#fields) +/// section below. [`Config::debug_default()`] returns the default values for +/// the debug profile while [`Config::release_default()`] the default values for +/// the release profile. The [`Config::default()`] method automatically selects +/// the appropriate of the two based on the selected profile. With the exception +/// of `log_level`, which is `normal` in `debug` and `critical` in `release`, +/// and `secret_key`, which is regenerated from a random value if not set in +/// "debug" mode only, all of the values are identical in either profile. /// -/// ## General Configuration +/// # Provider Details /// -/// For more information about Rocket's configuration, see the -/// [`config`](crate::config) module documentation. -#[derive(Clone)] +/// `Config` is a Figment [`Provider`] with the following characteristics: +/// +/// * **Profile** +/// +/// The selected profile is the value of the `ROCKET_PROFILE` environment +/// variable. If the environment variable is not set, the profile is +/// selected based on whether compilation is in debug mode, where "debug" is +/// selected, or release mode, where "release" is selected. +/// [`Config::DEBUG_PROFILE`] and [`Config::RELEASE_PROFILE`] encode these +/// values as constants, while [`Config::DEFAULT_PROFILE`] selects the +/// appropriate of the two at compile-time. +/// +/// * **Metadata** +/// +/// This provider is named `Rocket Config`. It does not specify a +/// [`Source`](figment::Source) and uses default interpolatation. +/// +/// * **Data** +/// +/// The data emitted by this provider are the keys and values corresponding +/// to the fields and values of the structure. The dictionary is emitted to +/// the "default" meta-profile. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Config { - /// The environment that this configuration corresponds to. - pub environment: Environment, - /// The address to serve on. - pub address: String, - /// The port to serve on. + /// IP address to serve on. **(default: `127.0.0.1`)** + pub address: IpAddr, + /// Port to serve on. **(default: `8000`)** pub port: u16, - /// The number of workers to run concurrently. + /// Number of threads to use for executing futures. **(default: `cores * 2`)** pub workers: u16, - /// Keep-alive timeout in seconds or None if disabled. - pub keep_alive: Option, - /// How much information to log. - pub log_level: LoggingLevel, - /// The secret key. - pub(crate) secret_key: SecretKey, - /// TLS configuration. - pub(crate) tls: Option, - /// Streaming data limits. + /// Keep-alive timeout in seconds; disabled when `0`. **(default: `5`)** + pub keep_alive: u32, + /// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)** + pub log_level: LogLevel, + /// Whether to use colors and emoji when logging. **(default: `true`)** + #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] + pub cli_colors: bool, + /// The secret key for signing and encrypting. **(default: `0`)** + pub secret_key: SecretKey, + /// The TLS configuration, if any. **(default: `None`)** + pub tls: Option, + /// Streaming read size limits. **(default: [`Limits::default()`])** 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 was loaded from, if any. - pub(crate) config_file_path: Option, - /// The path root-relative files will be rooted from. - pub(crate) root_path: Option, + /// Whether `ctrl-c` initiates a server shutdown. **(default: `true`)** + #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] + pub ctrlc: bool, } -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) => { - super::custom_values::$type($config, $name, $value) - .and_then(|parsed| $map($config.$set(parsed))) - })+ - _ => $rest - } - ) +impl Default for Config { + /// Returns the default configuration based on the compilation profile. This + /// is [`Config::debug_default()`] in `debug` and + /// [`Config::release_default()`] in `release`. + /// + /// # Example + /// + /// ```rust + /// use rocket::Config; + /// + /// let config = Config::default(); + /// ``` + fn default() -> Config { + #[cfg(debug_assertions)] { Config::debug_default() } + #[cfg(not(debug_assertions))] { Config::release_default() } + } } impl Config { - /// Reads configuration values from the nearest `Rocket.toml`, searching for - /// a file by that name upwards from the current working directory to its - /// parents, as well as from environment variables named `ROCKET_{PARAM}` as - /// specified in the [`config`](crate::config) module documentation. Returns - /// the active configuration. - /// - /// Specifically, this method: - /// - /// 1. Locates the nearest `Rocket.toml` configuration file. - /// 2. Retrieves the active environment according to - /// [`Environment::active()`]. - /// 3. Parses and validates the configuration file in its entirety, - /// extracting the values from the active environment. - /// 4. Overrides parameters with values set in `ROCKET_{PARAM}` - /// environment variables. - /// - /// Any error encountered while performing these steps is returned. + /// The default "debug" profile. + pub const DEBUG_PROFILE: Profile = Profile::const_new("debug"); + + /// The default "release" profile. + pub const RELEASE_PROFILE: Profile = Profile::const_new("release"); + + /// The default profile: "debug" on `debug`, "release" on `release`. + #[cfg(debug_assertions)] + pub const DEFAULT_PROFILE: Profile = Self::DEBUG_PROFILE; + + /// The default profile: "debug" on `debug`, "release" on `release`. + #[cfg(not(debug_assertions))] + pub const DEFAULT_PROFILE: Profile = Self::RELEASE_PROFILE; + + const DEPRECATED_KEYS: &'static [(&'static str, Option<&'static str>)] = &[ + ("env", Some("profile")), ("log", Some("log_level")), + ]; + + /// Returns the default configuration for the `debug` profile, irrespective + /// of the compilation profile. For the default Rocket will use, which is + /// chosen based on the configuration profile, call [`Config::default()`]. + /// See [Defaults](#Defaults) for specifics. /// /// # Example /// /// ```rust - /// # if false { - /// let config = rocket::Config::read().unwrap(); - /// # } + /// use rocket::Config; + /// + /// let config = Config::debug_default(); /// ``` - pub fn read() -> Result { - Self::read_from(&FullConfig::find_config_path()?) + pub fn debug_default() -> Config { + Config { + address: Ipv4Addr::new(127, 0, 0, 1).into(), + port: 8000, + workers: num_cpus::get() as u16 * 2, + keep_alive: 5, + log_level: LogLevel::Normal, + cli_colors: true, + secret_key: SecretKey::zero(), + tls: None, + limits: Limits::default(), + ctrlc: true, + } } - /// This method is exactly like [`Config::read()`] except it uses the file - /// at `path` as the configuration file instead of searching for the nearest - /// `Rocket.toml`. The file must have the same format as `Rocket.toml`. - /// - /// See [`Config::read()`] for more. + /// Returns the default configuration for the `release` profile, + /// irrespective of the compilation profile. For the default Rocket will + /// use, which is chosen based on the configuration profile, call + /// [`Config::default()`]. See [Defaults](#Defaults) for specifics. /// /// # Example /// /// ```rust - /// # if false { - /// let config = rocket::Config::read_from("/var/my-config.toml").unwrap(); - /// # } + /// use rocket::Config; + /// + /// let config = Config::release_default(); /// ``` - pub fn read_from>(path: P) -> Result { - FullConfig::read_from(path.as_ref()).map(FullConfig::take_active) + pub fn release_default() -> Config { + Config { + log_level: LogLevel::Critical, + ..Config::debug_default() + } } - /// Returns a builder for `Config` structure where the default parameters - /// are set to those of `env`. This _does not_ read any configuration - /// parameters from any source. See [`config`](crate::config) for a list of - /// defaults. + /// Returns the default provider figment used by [`rocket::ignite()`]. + /// + /// The default figment reads from the following sources, in ascending + /// priority order: + /// + /// 1. [`Config::default()`] (see [Defaults](#Defaults)) + /// 2. `Rocket.toml` _or_ filename in `ROCKET_CONFIG` environment variable + /// 3. `ROCKET_` prefixed environment variables + /// + /// The profile selected is the value set in the `ROCKET_PROFILE` + /// environment variable. If it is not set, it defaults to `debug` when + /// compiled in debug mode and `release` when compiled in release mode. + /// + /// [`rocket::ignite()`]: crate::rocket::ignite() /// /// # Example /// /// ```rust - /// use rocket::config::{Config, Environment}; + /// use rocket::Config; + /// use serde::Deserialize; /// - /// # #[allow(unused_variables)] - /// let config = Config::build(Environment::Staging) - /// .address("127.0.0.1") - /// .port(700) - /// .workers(12) - /// .unwrap(); + /// #[derive(Deserialize)] + /// struct MyConfig { + /// app_key: String, + /// } + /// + /// let my_config = Config::figment().extract::(); /// ``` - pub fn build(env: Environment) -> ConfigBuilder { - ConfigBuilder::new(env) + pub fn figment() -> Figment { + Figment::from(Config::default()) + .merge(Toml::file(Env::var_or("ROCKET_CONFIG", "Rocket.toml")).nested()) + .merge(Env::prefixed("ROCKET_").ignore(&["PROFILE"]).global()) } - /// Returns a `Config` with the default parameters for the environment - /// `env`. This _does not_ read any configuration parameters from any - /// source. See [`config`](crate::config) for a list of defaults. + /// Attempts to extract a `Config` from `provider`. /// /// # Panics /// - /// Panics if randomness cannot be retrieved from the OS. + /// If extraction fails, prints an error message indicating the failure and + /// panics. /// /// # Example /// /// ```rust - /// use rocket::config::{Config, Environment}; + /// use figment::{Figment, providers::{Toml, Format, Env}}; /// - /// let mut my_config = Config::new(Environment::Production); - /// my_config.set_port(1001); + /// // Use Rocket's default `Figment`, but allow values from `MyApp.toml` + /// // and `MY_APP_` prefixed environment variables to supersede its values. + /// let figment = rocket::Config::figment() + /// .merge(Toml::file("MyApp.toml").nested()) + /// .merge(Env::prefixed("MY_APP_")); + /// + /// let config = rocket::Config::from(figment); /// ``` - pub fn new(env: Environment) -> Config { - Config::default(env).expect("failed to read randomness from the OS") - } - - /// Returns a `Config` with the default parameters of the active environment - /// as determined by the `ROCKET_ENV` environment variable. This _does not_ - /// read any configuration parameters from any source. - /// - /// If `ROCKET_ENV` is not set, the returned `Config` uses development - /// environment parameters when the application was compiled in `debug` mode - /// and production environment parameters when the application was compiled - /// in `release` mode. - /// - /// This is equivalent to `Config::new(Environment::active()?)`. - /// - /// # Errors - /// - /// Returns a `BadEnv` error if `ROCKET_ENV` is set and contains an invalid - /// or unknown environment name. - /// - /// # Example - /// - /// ```rust - /// let mut my_config = rocket::Config::active().unwrap(); - /// my_config.set_port(1001); - /// ``` - pub fn active() -> Result { - Ok(Config::new(Environment::active()?)) - } - - /// Returns a `Config` with the default parameters of the development - /// environment. See [`config`](crate::config) for a list of defaults. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut my_config = Config::development(); - /// my_config.set_port(1001); - /// ``` - pub fn development() -> Config { - Config::new(Environment::Development) - } - - /// Returns a `Config` with the default parameters of the staging - /// environment. See [`config`](crate::config) for a list of defaults. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut my_config = Config::staging(); - /// my_config.set_port(1001); - /// ``` - pub fn staging() -> Config { - Config::new(Environment::Staging) - } - - /// Returns a `Config` with the default parameters of the production - /// environment. See [`config`](crate::config) for a list of defaults. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut my_config = Config::production(); - /// my_config.set_port(1001); - /// ``` - pub fn production() -> Config { - Config::new(Environment::Production) - } - - /// Returns the default configuration for the environment `env` given that - /// the configuration was stored at `path`. This doesn't read the file at - /// `path`; it simply uses `path` to set the config path property in the - /// returned `Config`. - /// - /// # Error - /// - /// Return a `BadFilePath` error if `path` does not have a parent. - pub(crate) fn default_from

(env: Environment, path: P) -> Result - where P: AsRef - { - let mut config = Config::default(env)?; - - let config_file_path = path.as_ref().to_path_buf(); - if let Some(parent) = config_file_path.parent() { - config.set_root(parent); - } else { - let msg = "Configuration files must be rooted in a directory."; - return Err(ConfigError::BadFilePath(config_file_path.clone(), msg)); - } - - config.config_file_path = Some(config_file_path); - Ok(config) - } - - /// Returns the default configuration for the environment `env`. - pub(crate) fn default(env: Environment) -> Result { - // Note: This may truncate if num_cpus::get() / 2 > u16::max. That's okay. - let default_workers = (num_cpus::get() * 2) as u16; - - // Use a generated secret key by default. - let new_key = Key::try_generate().ok_or(ConfigError::RandFailure)?; - let key = SecretKey::Generated(new_key); - - Ok(match env { - Development => { - Config { - environment: Development, - address: "localhost".to_string(), - port: 8000, - workers: default_workers, - keep_alive: Some(5), - log_level: LoggingLevel::Normal, - secret_key: key, - tls: None, - limits: Limits::default(), - extras: HashMap::new(), - config_file_path: None, - root_path: None, + pub fn from(provider: T) -> Self { + // Check for now depreacted config values. + let figment = Figment::from(&provider); + for (key, replacement) in Self::DEPRECATED_KEYS { + if figment.find_value(key).is_ok() { + warn!("found value for deprecated config key `{}`", Paint::white(key)); + if let Some(new_key) = replacement { + info_!("key has been by replaced by `{}`", Paint::white(new_key)); } } - Staging => { - Config { - environment: Staging, - address: "0.0.0.0".to_string(), - port: 8000, - workers: default_workers, - keep_alive: Some(5), - log_level: LoggingLevel::Normal, - secret_key: key, - tls: None, - limits: Limits::default(), - extras: HashMap::new(), - config_file_path: None, - root_path: None, - } - } - Production => { - Config { - environment: Production, - address: "0.0.0.0".to_string(), - port: 8000, - workers: default_workers, - keep_alive: Some(5), - log_level: LoggingLevel::Critical, - secret_key: key, - tls: None, - limits: Limits::default(), - extras: HashMap::new(), - config_file_path: None, - root_path: None, - } - } - }) - } - - /// Constructs a `BadType` error given the entry `name`, the invalid `val` - /// at that entry, and the `expect`ed type name. - #[inline(always)] - 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_file_path.clone()) - } - - /// Sets the configuration `val` for the `name` entry. If the `name` is one - /// 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`. - /// - /// For each of the default values, the following `Value` variant is - /// expected. If a different variant is supplied, a `BadType` `Err` is - /// returned: - /// - /// * **address**: String - /// * **port**: Integer (16-bit unsigned) - /// * **workers**: Integer (16-bit unsigned) - /// * **keep_alive**: Integer - /// * **log**: String - /// * **secret_key**: String (256-bit base64 or base16) - /// * **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), - keep_alive => (u32, set_keep_alive, ok), - log => (log_level, set_log_level, ok), - secret_key => (str, set_secret_key, id), - tls => (tls_config, set_raw_tls, id), - limits => (limits, set_limits, ok), - | _ => { - self.extras.insert(name.into(), val.clone()); - Ok(()) - } - ) - } - - /// Sets the root directory of this configuration to `root`. - /// - /// # Example - /// - /// ```rust - /// # use std::path::Path; - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// config.set_root("/var/my_app"); - /// - /// # #[cfg(not(windows))] - /// assert_eq!(config.root().unwrap(), Path::new("/var/my_app")); - /// ``` - pub fn set_root>(&mut self, path: P) { - self.root_path = Some(path.as_ref().into()); - } - - /// Sets the address of `self` to `address`. - /// - /// # Errors - /// - /// If `address` is not a valid IP address or hostname, returns a `BadType` - /// error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// 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()); - /// ``` - pub fn set_address>(&mut self, address: A) -> Result<()> { - let address = address.into(); - if (&*address, 0u16).to_socket_addrs().is_err() { - return Err(self.bad_type("address", "string", "a valid hostname or IP")); } - self.address = address; - Ok(()) - } + #[allow(unused_mut)] + let mut config = figment.extract::().unwrap_or_else(|e| { + pretty_print_error(e); + panic!("aborting due to configuration error(s)") + }); - /// Sets the `port` of `self` to `port`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// config.set_port(1024); - /// assert_eq!(config.port, 1024); - /// ``` - #[inline] - pub fn set_port(&mut self, port: u16) { - self.port = port; - } + #[cfg(all(feature = "secrets", not(test), not(rocket_unsafe_secret_key)))] { + if config.secret_key.is_zero() { + if figment.profile() != Self::DEBUG_PROFILE { + crate::logger::try_init(LogLevel::Debug, true, false); + error!("secrets enabled in `release` without `secret_key`"); + info_!("disable `secrets` feature or configure a `secret_key`"); + panic!("aborting due to configuration error(s)") + } else { + warn!("secrets enabled in `debug` without `secret_key`"); + info_!("disable `secrets` feature or configure a `secret_key`"); + info_!("this becomes a hard error in `release`"); + } - /// Sets the number of `workers` in `self` to `workers`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// config.set_workers(64); - /// assert_eq!(config.workers, 64); - /// ``` - #[inline] - pub fn set_workers(&mut self, workers: u16) { - self.workers = workers; - } - - /// Sets the keep-alive timeout to `timeout` seconds. If `timeout` is `0`, - /// keep-alive is disabled. - /// - /// # Example - /// - /// ```rust - /// let mut config = rocket::Config::development(); - /// - /// // Set keep-alive timeout to 10 seconds. - /// config.set_keep_alive(10); - /// assert_eq!(config.keep_alive, Some(10)); - /// - /// // Disable keep-alive. - /// config.set_keep_alive(0); - /// assert_eq!(config.keep_alive, None); - /// ``` - #[inline] - pub fn set_keep_alive(&mut self, timeout: u32) { - if timeout == 0 { - self.keep_alive = None; - } else { - self.keep_alive = Some(timeout); + // in debug, generate a key for a bit more security + config.secret_key = SecretKey::generate().unwrap_or(SecretKey::zero()); + } } - } - /// Sets the `secret_key` in `self` to `key` which must be a 256-bit base64 - /// or base16 (hex) encoded string. - /// - /// # Errors - /// - /// If `key` is not a valid 256-bit encoded string, returns a `BadType` - /// error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// - /// // A base64 encoded key. - /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="; - /// assert!(config.set_secret_key(key).is_ok()); - /// - /// // A base16 (hex) encoded key. - /// let key = "fe4c5b09a9ac372156e44ce133bc940685ef5e0394d6e9274aadacc21e4f2643"; - /// assert!(config.set_secret_key(key).is_ok()); - /// - /// // An invalid key. - /// assert!(config.set_secret_key("hello? anyone there?").is_err()); - /// ``` - pub fn set_secret_key>(&mut self, key: K) -> Result<()> { - let key = key.into(); - let e = self.bad_type("secret_key", "string", "a 256-bit base64 or hex encoded string"); - - // `binascii` requires a bit more space than actual output for padding - let mut bytes = [0u8; 36]; - let bytes = match key.len() { - 44 => binascii::b64decode(key.as_bytes(), &mut bytes).map_err(|_| e)?, - 64 => binascii::hex2bin(key.as_bytes(), &mut bytes).map_err(|_| e)?, - _ => return Err(e) - }; - - self.secret_key = SecretKey::Provided(Key::derive_from(&bytes)); - Ok(()) - } - - /// Sets the logging level for `self` to `log_level`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, LoggingLevel, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// config.set_log_level(LoggingLevel::Critical); - /// assert_eq!(config.log_level, LoggingLevel::Critical); - /// ``` - #[inline] - pub fn set_log_level(&mut self, log_level: LoggingLevel) { - self.log_level = log_level; - } - - /// Sets the receive limits in `self` to `limits`. - /// - /// # Example - /// - /// ```rust - /// use rocket::data::{Limits, ToByteUnit}; - /// - /// let mut config = rocket::Config::development(); - /// config.set_limits(Limits::default().limit("json", 4.mebibytes())); - /// ``` - #[inline] - pub fn set_limits(&mut self, limits: Limits) { - self.limits = limits; - } - - /// Sets the TLS configuration in `self`. - /// - /// Certificates are read from `certs_path`. The certificate chain must be - /// in X.509 PEM format. The private key is read from `key_path`. The - /// private key must be an RSA key in either PKCS#1 or PKCS#8 PEM format. - /// - /// # Errors - /// - /// If reading either the certificates or private key fails, an error of - /// variant `Io` is returned. If either the certificates or private key - /// files are malformed or cannot be parsed, an error of `BadType` is - /// returned. - /// - /// # Example - /// - /// ```rust - /// # use rocket::config::ConfigError; - /// # fn config_test() -> Result<(), ConfigError> { - /// let mut config = rocket::Config::development(); - /// config.set_tls("/etc/ssl/my_certs.pem", "/etc/ssl/priv.key")?; - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "tls")] - pub fn set_tls(&mut self, certs_path: &str, key_path: &str) -> Result<()> { - use crate::http::tls::{load_certs, load_private_key, Error}; - - let pem_err = "malformed PEM file"; - - // Load the certificates. - let certs = load_certs(self.root_relative(certs_path)) - .map_err(|e| match e { - Error::Io(e) => ConfigError::Io(e, "tls.certs"), - _ => self.bad_type("tls", pem_err, "a valid certificates file") - })?; - - // And now the private key. - let key = load_private_key(self.root_relative(key_path)) - .map_err(|e| match e { - Error::Io(e) => ConfigError::Io(e, "tls.key"), - _ => self.bad_type("tls", pem_err, "a valid private key file") - })?; - - self.tls = Some(TlsConfig { certs, key }); - Ok(()) - } - - #[doc(hidden)] - #[cfg(not(feature = "tls"))] - pub fn set_tls(&mut self, _: &str, _: &str) -> Result<()> { - self.tls = Some(TlsConfig); - Ok(()) - } - - #[inline(always)] - fn set_raw_tls(&mut self, _paths: (&str, &str)) -> Result<()> { - #[cfg(not(test))] - { self.set_tls(_paths.0, _paths.1) } - - // During unit testing, we don't want to actually read certs/keys. - #[cfg(test)] - { Ok(()) } - } - - /// Sets the extras for `self` to be the key/value pairs in `extras`. - /// encoded string. - /// - /// # Example - /// - /// ```rust - /// use std::collections::HashMap; - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// - /// // Create the `extras` map. - /// let mut extras = HashMap::new(); - /// extras.insert("another_port".to_string(), 1044.into()); - /// extras.insert("templates".to_string(), "my_dir".into()); - /// - /// config.set_extras(extras); - /// ``` - #[inline] - pub fn set_extras(&mut self, extras: HashMap) { - self.extras = extras; - } - - /// Returns an iterator over the names and values of all of the extras in - /// `self`. - /// - /// # Example - /// - /// ```rust - /// use std::collections::HashMap; - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// assert_eq!(config.extras().count(), 0); - /// - /// // Add a couple of extras to the config. - /// let mut extras = HashMap::new(); - /// extras.insert("another_port".to_string(), 1044.into()); - /// extras.insert("templates".to_string(), "my_dir".into()); - /// config.set_extras(extras); - /// - /// assert_eq!(config.extras().count(), 2); - /// ``` - #[inline] - pub fn extras<'a>(&'a self) -> impl Iterator { - self.extras.iter().map(|(k, v)| (k.as_str(), v)) + config } /// Returns `true` if TLS is enabled. /// - /// Always returns `false` if the `tls` compilation feature is not enabled. + /// TLS is enabled when the `tls` feature is enabled and TLS has been + /// configured. + /// + /// # Example + /// + /// ```rust + /// let config = rocket::Config::default(); + /// if config.tls_enabled() { + /// println!("TLS is enabled!"); + /// } else { + /// println!("TLS is disabled."); + /// } + /// ``` pub fn tls_enabled(&self) -> bool { - if cfg!(feature = "tls") { - self.tls.is_some() + cfg!(feature = "tls") && self.tls.is_some() + } + + pub(crate) fn pretty_print(&self, profile: &Profile) { + use crate::logger::PaintExt; + + launch_info!("{}Configured for {}.", Paint::emoji("🔧 "), profile); + launch_info_!("address: {}", Paint::default(&self.address).bold()); + launch_info_!("port: {}", Paint::default(&self.port).bold()); + launch_info_!("workers: {}", Paint::default(self.workers).bold()); + launch_info_!("log level: {}", Paint::default(self.log_level).bold()); + launch_info_!("secret key: {:?}", Paint::default(&self.secret_key).bold()); + launch_info_!("limits: {}", Paint::default(&self.limits).bold()); + launch_info_!("cli colors: {}", Paint::default(&self.cli_colors).bold()); + + let ka = self.keep_alive; + if ka > 0 { + launch_info_!("keep-alive: {}", Paint::default(format!("{}s", ka)).bold()); } else { - false + launch_info_!("keep-alive: {}", Paint::default("disabled").bold()); } - } - /// Retrieves the secret key from `self`. - #[inline] - pub(crate) fn secret_key(&self) -> &Key { - self.secret_key.inner() - } - - /// Attempts to retrieve the extra named `name` as a raw value. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment, Value}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("name", "value") - /// .unwrap(); - /// - /// assert_eq!(config.get_extra("name"), Ok(&Value::String("value".into()))); - /// assert!(config.get_extra("other").is_err()); - /// ``` - pub fn get_extra<'a>(&'a self, name: &str) -> Result<&'a Value> { - self.extras.get(name).ok_or_else(|| ConfigError::Missing(name.into())) - } - - /// Attempts to retrieve the extra named `name` as a borrowed string. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not a string, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("my_extra", "extra_value") - /// .unwrap(); - /// - /// assert_eq!(config.get_str("my_extra"), Ok("extra_value")); - /// ``` - pub fn get_str<'a>(&'a self, name: &str) -> Result<&'a str> { - let val = self.get_extra(name)?; - val.as_str().ok_or_else(|| self.bad_type(name, val.type_str(), "a string")) - } - - /// Attempts to retrieve the extra named `name` as an owned string. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not a string, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("my_extra", "extra_value") - /// .unwrap(); - /// - /// assert_eq!(config.get_string("my_extra"), Ok("extra_value".to_string())); - /// ``` - pub fn get_string(&self, name: &str) -> Result { - self.get_str(name).map(|s| s.to_string()) - } - - /// Attempts to retrieve the extra named `name` as an integer. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not an integer, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("my_extra", 1025) - /// .unwrap(); - /// - /// assert_eq!(config.get_int("my_extra"), Ok(1025)); - /// ``` - pub fn get_int(&self, name: &str) -> Result { - let val = self.get_extra(name)?; - 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. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not a boolean, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("my_extra", true) - /// .unwrap(); - /// - /// assert_eq!(config.get_bool("my_extra"), Ok(true)); - /// ``` - pub fn get_bool(&self, name: &str) -> Result { - let val = self.get_extra(name)?; - 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. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not a float, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// - /// let config = Config::build(Environment::Staging) - /// .extra("pi", 3.14159) - /// .unwrap(); - /// - /// assert_eq!(config.get_float("pi"), Ok(3.14159)); - /// ``` - pub fn get_float(&self, name: &str) -> Result { - let val = self.get_extra(name)?; - 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. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// 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) -> Result<&Array> { - let val = self.get_extra(name)?; - val.as_array().ok_or_else(|| self.bad_type(name, val.type_str(), "an array")) - } - - /// Attempts to retrieve the extra named `name` as a table. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// 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) -> Result<&Table> { - let val = self.get_extra(name)?; - val.as_table().ok_or_else(|| self.bad_type(name, val.type_str(), "a table")) - } - - /// Attempts to retrieve the extra named `name` as a datetime value. - /// - /// # Errors - /// - /// If an extra with `name` doesn't exist, returns an `Err` of `Missing`. - /// If an extra with `name` _does_ exist but is not a datetime, returns a - /// `BadType` error. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::{Config, Environment, Value, Datetime}; - /// - /// let date = "1979-05-27T00:32:00-07:00".parse::().unwrap(); - /// - /// let config = Config::build(Environment::Staging) - /// .extra("my_date", Value::Datetime(date.clone())) - /// .unwrap(); - /// - /// assert_eq!(config.get_datetime("my_date"), Ok(&date)); - /// ``` - pub fn get_datetime(&self, name: &str) -> Result<&Datetime> { - let val = self.get_extra(name)?; - val.as_datetime() - .ok_or_else(|| self.bad_type(name, val.type_str(), "a datetime")) - } - - /// Returns the root path of the configuration, if one is known. - /// - /// For configurations loaded from a `Rocket.toml` file, this will be the - /// directory in which the file is stored. For instance, if the - /// configuration file is at `/tmp/Rocket.toml`, the path `/tmp` is - /// returned. For other configurations, this will be the path set via - /// [`Config::set_root()`] or [`ConfigBuilder::root()`]. - /// - /// # Example - /// - /// ```rust - /// use std::env::current_dir; - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// assert_eq!(config.root(), None); - /// - /// let cwd = current_dir().expect("have cwd"); - /// config.set_root(&cwd); - /// assert_eq!(config.root().unwrap(), cwd); - /// ``` - pub fn root(&self) -> Option<&Path> { - self.root_path.as_ref().map(|p| p.as_ref()) - } - - /// Returns `path` relative to this configuration. - /// - /// The path that is returned depends on whether: - /// - /// 1. Whether `path` is absolute or relative. - /// 2. Whether there is a [`Config::root()`] configured. - /// 3. Whether there is a current directory. - /// - /// If `path` is absolute, it is returned unaltered. Otherwise, if `path` is - /// relative and there is a root configured, the root is prepended to `path` - /// and the newlt concatenated path is returned. Otherwise, if there is a - /// current directory, it is preprended to `path` and the newly concatenated - /// path is returned. Finally, if all else fails, the path is simply - /// returned. - /// - /// # Example - /// - /// ```rust - /// use std::path::Path; - /// use std::env::current_dir; - /// - /// use rocket::config::{Config, Environment}; - /// - /// let mut config = Config::new(Environment::Staging); - /// - /// let cwd = current_dir().expect("have cwd"); - /// config.set_root(&cwd); - /// assert_eq!(config.root().unwrap(), cwd); - /// - /// assert_eq!(config.root_relative("abc"), cwd.join("abc")); - /// # #[cfg(not(windows))] - /// assert_eq!(config.root_relative("/abc"), Path::new("/abc")); - /// # #[cfg(windows)] - /// # assert_eq!(config.root_relative("C:\\abc"), Path::new("C:\\abc")); - /// ``` - pub fn root_relative>(&self, path: P) -> PathBuf { - let path = path.as_ref(); - if path.is_absolute() { - path.into() - } else if let Some(root) = self.root() { - root.join(path) - } else if let Ok(cwd) = std::env::current_dir() { - cwd.join(path) - } else { - path.into() + match self.tls_enabled() { + true => launch_info_!("tls: {}", Paint::default("enabled").bold()), + false => launch_info_!("tls: {}", Paint::default("disabled").bold()), } } } -impl fmt::Debug for Config { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut s = f.debug_struct("Config"); - s.field("environment", &self.environment); - s.field("address", &self.address); - s.field("port", &self.port); - s.field("workers", &self.workers); - s.field("keep_alive", &self.keep_alive); - s.field("log_level", &self.log_level); +impl Provider for Config { + fn metadata(&self) -> Metadata { + Metadata::named("Rocket Config") + } - for (key, value) in self.extras() { - s.field(key, &value); + #[track_caller] + fn data(&self) -> Result> { + Serialized::defaults(self).data() + } + + fn profile(&self) -> Option { + Some(Profile::from_env_or("ROCKET_PROFILE", Self::DEFAULT_PROFILE)) + } +} + +#[doc(hidden)] +pub fn pretty_print_error(error: figment::Error) { + crate::logger::try_init(LogLevel::Debug, true, false); + + for e in error { + error!("{}", e.kind); + + if let (Some(ref profile), Some(ref md)) = (&e.profile, &e.metadata) { + if !e.path.is_empty() { + let key = md.interpolate(profile, &e.path); + info_!("for key {}", Paint::white(key)); + } } - s.finish() - } -} - -/// Doesn't consider the secret key or config path. -impl PartialEq for Config { - fn eq(&self, other: &Config) -> bool { - self.address == other.address - && self.port == other.port - && self.workers == other.workers - && self.log_level == other.log_level - && self.keep_alive == other.keep_alive - && self.environment == other.environment - && self.extras == other.extras + if let Some(ref md) = e.metadata { + if let Some(ref source) = md.source { + info_!("in {} {}", Paint::white(source), md.name); + } + } } } diff --git a/core/lib/src/config/custom_values.rs b/core/lib/src/config/custom_values.rs deleted file mode 100644 index a484bdcb..00000000 --- a/core/lib/src/config/custom_values.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::fmt; - -#[cfg(feature = "tls")] use crate::http::tls::{Certificate, PrivateKey}; - -use crate::http::private::cookie::Key; -use crate::config::{Result, Config, Value, ConfigError, LoggingLevel}; -use crate::data::Limits; - -#[derive(Clone)] -pub enum SecretKey { - Generated(Key), - Provided(Key) -} - -impl SecretKey { - #[inline] - pub(crate) fn inner(&self) -> &Key { - match *self { - SecretKey::Generated(ref key) | SecretKey::Provided(ref key) => key - } - } - - #[inline] - pub(crate) fn is_generated(&self) -> bool { - match *self { - #[cfg(feature = "secrets")] - SecretKey::Generated(_) => true, - _ => false - } - } -} - -impl fmt::Display for SecretKey { - #[cfg(feature = "secrets")] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - SecretKey::Generated(_) => write!(f, "generated"), - SecretKey::Provided(_) => write!(f, "provided"), - } - } - - #[cfg(not(feature = "secrets"))] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "private-cookies disabled".fmt(f) - } -} - -#[cfg(feature = "tls")] -#[derive(Clone)] -pub struct TlsConfig { - pub certs: Vec, - pub key: PrivateKey -} - -#[cfg(not(feature = "tls"))] -#[derive(Clone)] -pub struct TlsConfig; - -pub fn str<'a>(conf: &Config, name: &str, v: &'a Value) -> Result<&'a str> { - v.as_str().ok_or_else(|| conf.bad_type(name, v.type_str(), "a string")) -} - -pub fn 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 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 u32(conf: &Config, name: &str, value: &Value) -> Result { - match value.as_integer() { - Some(x) if x >= 0 && x <= (u32::max_value() as i64) => Ok(x as u32), - _ => Err(conf.bad_type(name, value.type_str(), "a 32-bit unsigned integer")) - } -} - -pub fn log_level(conf: &Config, - name: &str, - value: &Value - ) -> Result { - str(conf, name, value) - .and_then(|s| s.parse().map_err(|e| conf.bad_type(name, value.type_str(), e))) -} - -pub fn 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(str(conf, "tls.certs", value)?), - "key" => key_path = Some(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 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 = u64(conf, &format!("limits.{}", key), val)?; - limits = limits.limit(key.as_str(), val.into()); - } - - Ok(limits) -} diff --git a/core/lib/src/config/environment.rs b/core/lib/src/config/environment.rs deleted file mode 100644 index 7296d954..00000000 --- a/core/lib/src/config/environment.rs +++ /dev/null @@ -1,160 +0,0 @@ -use super::ConfigError; - -use std::fmt; -use std::str::FromStr; -use std::env; - -use self::Environment::*; - -pub const CONFIG_ENV: &str = "ROCKET_ENV"; - -/// An enum corresponding to the valid configuration environments. -#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)] -pub enum Environment { - /// The development environment. - Development, - /// The staging environment. - Staging, - /// The production environment. - Production, -} - -impl Environment { - /// List of all of the possible environments. - pub(crate) const ALL: [Environment; 3] = [Development, Staging, Production]; - - /// String of all valid environments. - pub(crate) const VALID: &'static str = "development, staging, production"; - - /// Retrieves the "active" environment as determined by the `ROCKET_ENV` - /// environment variable. If `ROCKET_ENV` is not set, returns `Development` - /// when the application was compiled in `debug` mode and `Production` when - /// the application was compiled in `release` mode. - /// - /// # Errors - /// - /// Returns a `BadEnv` `ConfigError` if `ROCKET_ENV` is set and contains an - /// invalid or unknown environment name. - pub fn active() -> Result { - match env::var(CONFIG_ENV) { - Ok(s) => s.parse().map_err(|_| ConfigError::BadEnv(s)), - #[cfg(debug_assertions)] - _ => Ok(Development), - #[cfg(not(debug_assertions))] - _ => Ok(Production), - } - } - - /// Returns `true` if `self` is `Environment::Development`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// assert!(Environment::Development.is_dev()); - /// assert!(!Environment::Production.is_dev()); - /// ``` - #[inline] - pub fn is_dev(self) -> bool { - self == Development - } - - /// Returns `true` if `self` is `Environment::Staging`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// assert!(Environment::Staging.is_stage()); - /// assert!(!Environment::Production.is_stage()); - /// ``` - #[inline] - pub fn is_stage(self) -> bool { - self == Staging - } - - /// Returns `true` if `self` is `Environment::Production`. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// assert!(Environment::Production.is_prod()); - /// assert!(!Environment::Staging.is_prod()); - /// ``` - #[inline] - pub fn is_prod(self) -> bool { - self == Production - } -} - -impl FromStr for Environment { - type Err = (); - - /// Parses a configuration environment from a string. Should be used - /// indirectly via `str`'s `parse` method. - /// - /// # Examples - /// - /// Parsing a development environment: - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// let env = "development".parse::(); - /// assert_eq!(env.unwrap(), Environment::Development); - /// - /// let env = "dev".parse::(); - /// assert_eq!(env.unwrap(), Environment::Development); - /// - /// let env = "devel".parse::(); - /// assert_eq!(env.unwrap(), Environment::Development); - /// ``` - /// - /// Parsing a staging environment: - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// let env = "staging".parse::(); - /// assert_eq!(env.unwrap(), Environment::Staging); - /// - /// let env = "stage".parse::(); - /// assert_eq!(env.unwrap(), Environment::Staging); - /// ``` - /// - /// Parsing a production environment: - /// - /// ```rust - /// use rocket::config::Environment; - /// - /// let env = "production".parse::(); - /// assert_eq!(env.unwrap(), Environment::Production); - /// - /// let env = "prod".parse::(); - /// assert_eq!(env.unwrap(), Environment::Production); - /// ``` - fn from_str(s: &str) -> Result { - let env = match s { - "dev" | "devel" | "development" => Development, - "stage" | "staging" => Staging, - "prod" | "production" => Production, - _ => return Err(()), - }; - - Ok(env) - } -} - -impl fmt::Display for Environment { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Development => write!(f, "development"), - Staging => write!(f, "staging"), - Production => write!(f, "production"), - } - } -} diff --git a/core/lib/src/config/error.rs b/core/lib/src/config/error.rs deleted file mode 100644 index 8849447d..00000000 --- a/core/lib/src/config/error.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::{io, fmt}; -use std::path::PathBuf; -use std::error::Error; - -use yansi::Paint; - -use super::Environment; -use self::ConfigError::*; - -/// The type of a configuration error. -#[derive(Debug)] -pub enum ConfigError { - /// The configuration file was not found. - NotFound, - /// There was an I/O error while reading the configuration file. - IoError, - /// There was an error retrieving randomness from the OS. - RandFailure, - /// There was an I/O error while setting a configuration parameter. - /// - /// Parameters: (io_error, config_param_name) - Io(io::Error, &'static str), - /// The path at which the configuration file was found was invalid. - /// - /// Parameters: (path, reason) - BadFilePath(PathBuf, &'static str), - /// An environment specified in `ROCKET_ENV` is invalid. - /// - /// Parameters: (environment_name) - BadEnv(String), - /// An environment specified as a table `[environment]` is invalid. - /// - /// Parameters: (environment_name, filename) - BadEntry(String, PathBuf), - /// A config key was specified with a value of the wrong type. - /// - /// Parameters: (entry_name, expected_type, actual_type, filename) - BadType(String, &'static str, &'static str, Option), - /// There was a TOML parsing error. - /// - /// Parameters: (toml_source_string, filename, error_description, line/col) - ParseError(String, PathBuf, String, Option<(usize, usize)>), - /// There was a TOML parsing error in a config environment variable. - /// - /// Parameters: (env_key, env_value, error) - BadEnvVal(String, String, String), - /// The entry (key) is unknown. - /// - /// Parameters: (key) - UnknownKey(String), - /// The entry (key) was expected but was missing. - /// - /// Parameters: (key) - Missing(String), -} - -impl ConfigError { - /// Prints this configuration error with Rocket formatting. - pub fn pretty_print(&self) { - let valid_envs = Environment::VALID; - match *self { - NotFound => error!("config file was not found"), - IoError => error!("failed reading the config file: IO error"), - RandFailure => error!("failed to read randomness from the OS"), - Io(ref error, param) => { - error!("I/O error while setting {}:", Paint::default(param).bold()); - info_!("{}", error); - } - BadFilePath(ref path, reason) => { - error!("configuration file path {} is invalid", - Paint::default(path.display()).bold()); - info_!("{}", reason); - } - BadEntry(ref name, ref filename) => { - let valid_entries = format!("{}, global", valid_envs); - error!("{} is not a known configuration environment", - Paint::default(format!("[{}]", name)).bold()); - info_!("in {}", Paint::default(filename.display()).bold()); - info_!("valid environments are: {}", Paint::default(valid_entries).bold()); - } - BadEnv(ref name) => { - error!("{} is not a valid ROCKET_ENV value", Paint::default(name).bold()); - info_!("valid environments are: {}", Paint::default(valid_envs).bold()); - } - BadType(ref name, expected, actual, ref filename) => { - error!("{} key could not be parsed", Paint::default(name).bold()); - if let Some(filename) = filename { - info_!("in {}", Paint::default(filename.display()).bold()); - } - - info_!("expected value to be {}, but found {}", - Paint::default(expected).bold(), Paint::default(actual).bold()); - } - ParseError(_, ref filename, ref desc, line_col) => { - error!("config file failed to parse due to invalid TOML"); - info_!("{}", desc); - info_!("in {}", Paint::default(filename.display()).bold()); - if let Some((line, col)) = line_col { - info_!("at line {}, column {}", - Paint::default(line + 1).bold(), Paint::default(col + 1).bold()); - } - } - BadEnvVal(ref key, ref value, ref error) => { - error!("environment variable {} could not be parsed", - Paint::default(format!("ROCKET_{}={}", key.to_uppercase(), value)).bold()); - info_!("{}", error); - } - UnknownKey(ref key) => { - error!("the configuration key {} is unknown and disallowed in \ - this position", Paint::default(key).bold()); - } - Missing(ref key) => { - error!("missing configuration key: {}", Paint::default(key).bold()); - } - } - } - - /// Returns `true` if `self` is of `NotFound` variant. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::ConfigError; - /// - /// let error = ConfigError::NotFound; - /// assert!(error.is_not_found()); - /// ``` - #[inline(always)] - pub fn is_not_found(&self) -> bool { - match *self { - NotFound => true, - _ => false - } - } -} - -impl fmt::Display for ConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - NotFound => write!(f, "config file was not found"), - IoError => write!(f, "I/O error while reading the config file"), - RandFailure => write!(f, "randomness could not be retrieved from the OS"), - Io(ref e, p) => write!(f, "I/O error while setting '{}': {}", p, e), - 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), - Missing(ref k) => write!(f, "missing key: '{}'", k), - 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 { - NotFound => "config file was not found", - IoError => "there was an I/O error while reading the config file", - RandFailure => "randomness could not be retrieved from the OS", - Io(..) => "an I/O error occurred while setting a configuration parameter", - 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", - UnknownKey(..) => "an unknown key was used in a disallowed position", - Missing(..) => "an expected key was not found", - } - } -} - -impl PartialEq for ConfigError { - fn eq(&self, other: &ConfigError) -> bool { - match (self, other) { - (&NotFound, &NotFound) => true, - (&IoError, &IoError) => true, - (&RandFailure, &RandFailure) => true, - (&Io(_, p1), &Io(_, p2)) => p1 == p2, - (&BadFilePath(ref p1, _), &BadFilePath(ref p2, _)) => p1 == p2, - (&BadEnv(ref e1), &BadEnv(ref e2)) => e1 == e2, - (&ParseError(..), &ParseError(..)) => true, - (&UnknownKey(ref k1), &UnknownKey(ref k2)) => k1 == k2, - (&BadEntry(ref e1, _), &BadEntry(ref e2, _)) => e1 == e2, - (&BadType(ref n1, e1, a1, _), &BadType(ref n2, e2, a2, _)) => { - n1 == n2 && e1 == e2 && a1 == a2 - } - (&BadEnvVal(ref k1, ref v1, _), &BadEnvVal(ref k2, ref v2, _)) => { - k1 == k2 && v1 == v2 - } - (&Missing(ref k1), &Missing(ref k2)) => k1 == k2, - (&NotFound, _) | (&IoError, _) | (&RandFailure, _) | (&Io(..), _) - | (&BadFilePath(..), _) | (&BadEnv(..), _) | (&ParseError(..), _) - | (&UnknownKey(..), _) | (&BadEntry(..), _) | (&BadType(..), _) - | (&BadEnvVal(..), _) | (&Missing(..), _) => false - } - } -} diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index d5ca40df..610e22bd 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -1,1159 +1,389 @@ -//! Application configuration and configuration parameter retrieval. +//! Server and application configuration. //! -//! This module implements configuration handling for Rocket. It implements the -//! parsing and interpretation of the `Rocket.toml` config file and -//! `ROCKET_{PARAM}` environment variables. It also allows libraries to access -//! user-configured values. +//! See the [configuration guide] for full details. //! -//! ## Application Configuration +//! [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ //! -//! ### Environments +//! ## Extracting Configuration Parameters //! -//! Rocket applications are always running in one of three environments: -//! -//! * development _or_ dev -//! * staging _or_ stage -//! * production _or_ prod -//! -//! Each environment can contain different configuration parameters. By default, -//! Rocket applications run in the **development** environment. The environment -//! can be changed via the `ROCKET_ENV` environment variable. For example, to -//! start a Rocket application in the **production** environment: -//! -//! ```sh -//! ROCKET_ENV=production ./target/release/rocket_app -//! ``` -//! -//! ### Configuration Parameters -//! -//! Each environments consists of several standard configuration parameters as -//! well as an arbitrary number of _extra_ configuration parameters, which are -//! not used by Rocket itself but can be used by external libraries. The -//! standard configuration parameters are: -//! -//! | name | type | description | examples | -//! |------------|----------------|-------------------------------------------------------------|----------------------------| -//! | address | string | ip address or host to listen on | `"localhost"`, `"1.2.3.4"` | -//! | port | integer | port number to listen on | `8000`, `80` | -//! | keep_alive | integer | keep-alive timeout in seconds | `0` (disable), `10` | -//! | workers | integer | number of concurrent thread workers | `36`, `512` | -//! | log | string | max log level: `"off"`, `"normal"`, `"debug"`, `"critical"` | `"off"`, `"normal"` | -//! | secret_key | 256-bit base64 | secret key for private cookies | `"8Xui8SI..."` (44 chars) | -//! | tls | table | tls config table with two keys (`certs`, `key`) | _see below_ | -//! | tls.certs | string | path to certificate chain in PEM format | `"private/cert.pem"` | -//! | tls.key | string | path to private key for `tls.certs` in PEM format | `"private/key.pem"` | -//! | limits | table | map from data type (string) to data limit (integer: bytes) | `{ forms = 65536 }` | -//! -//! ### Rocket.toml -//! -//! `Rocket.toml` is a Rocket application's configuration file. It can -//! optionally be used to specify the configuration parameters for each -//! environment. If it is not present, the default configuration parameters or -//! environment supplied 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 = [number_of_cpus * 2] -//! keep_alive = 5 -//! log = "normal" -//! secret_key = [randomly generated at launch] -//! limits = { forms = 32768 } -//! -//! [staging] -//! address = "0.0.0.0" -//! port = 8000 -//! workers = [number_of_cpus * 2] -//! keep_alive = 5 -//! log = "normal" -//! secret_key = [randomly generated at launch] -//! limits = { forms = 32768 } -//! -//! [production] -//! address = "0.0.0.0" -//! port = 8000 -//! workers = [number_of_cpus * 2] -//! keep_alive = 5 -//! log = "critical" -//! secret_key = [randomly generated at launch] -//! limits = { forms = 32768 } -//! ``` -//! -//! 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 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`. -//! -//! 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" -//! ``` -//! -//! ### 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" -//! ``` -//! -//! ### 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 ./your_application -//! ``` -//! -//! 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} -//! ``` -//! -//! ## Retrieving Configuration Parameters -//! -//! Configuration parameters for the currently active configuration environment -//! can be retrieved via the [`Rocket::config()`](crate::Rocket::config()) method -//! on `Rocket` and `get_` methods on [`Config`] structure. -//! -//! [`Rocket::config()`]: crate::Rocket::config() -//! -//! The retrivial of configuration parameters usually occurs at launch time via -//! a [launch fairing](crate::fairing::Fairing). If information about the -//! configuration is needed later in the program, an attach fairing can be used -//! to store the information as managed state. As an example of the latter, -//! consider the following short program which reads the `token` configuration -//! parameter and stores the value or a default in a `Token` managed state -//! value: +//! Rocket exposes the active [`Figment`] via [`Rocket::figment()`] and +//! [`Cargo::figment()`]. Any value that implements [`Deserialize`] can be +//! extracted from the figment: //! //! ```rust //! use rocket::fairing::AdHoc; //! -//! struct Token(i64); +//! #[derive(serde::Deserialize)] +//! struct AppConfig { +//! id: Option, +//! port: u16, +//! } //! -//! fn main() { -//! rocket::ignite() -//! .attach(AdHoc::on_attach("Token Config", |mut rocket| async { -//! println!("Adding token managed state from config..."); -//! let token_val = rocket.config().await.get_int("token").unwrap_or(-1); -//! Ok(rocket.manage(Token(token_val))) -//! })) -//! # ; +//! #[rocket::launch] +//! fn rocket() -> _ { +//! rocket::ignite().attach(AdHoc::config::()) //! } //! ``` +//! +//! [`Figment`]: figment::Figment +//! [`Rocket::figment()`]: crate::Rocket::figment() +//! [`Cargo::figment()`]: crate::Cargo::figment() +//! [`Deserialize`]: serde::Deserialize +//! +//! ## Custom Providers +//! +//! A custom provider can be set via [`rocket::custom()`], which replaces calls to +//! [`rocket::ignite()`]. The configured provider can be built on top of +//! [`Config::figment()`], [`Config::default()`], both, or neither. The +//! [Figment](@figment) documentation has full details on instantiating existing +//! providers like [`Toml`]() and [`Json`] as well as creating custom providers for +//! more complex cases. +//! +//! Configuration values can be overridden at runtime by merging figment's tuple +//! providers with Rocket's default provider: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! use rocket::data::{Limits, ToByteUnit}; +//! +//! #[launch] +//! fn rocket() -> _ { +//! let figment = rocket::Config::figment() +//! .merge(("port", 1111)) +//! .merge(("limits", Limits::new().limit("json", 2.mebibytes()))); +//! +//! rocket::custom(figment).mount("/", routes![/* .. */]) +//! } +//! ``` +//! +//! An application that wants to use Rocket's defaults for [`Config`], but not +//! its configuration sources, while allowing the application to be configured +//! via an `App.toml` file and `APP_` environment variables, can be structured +//! as follows: +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! use serde::{Serialize, Deserialize}; +//! use figment::{Figment, providers::{Format, Toml, Serialized, Env}}; +//! use rocket::fairing::AdHoc; +//! +//! #[derive(Debug, Deserialize, Serialize)] +//! struct Config { +//! app_value: usize, +//! /* and so on.. */ +//! } +//! +//! impl Default for Config { +//! fn default() -> Config { +//! Config { app_value: 3, } +//! } +//! } +//! +//! #[launch] +//! fn rocket() -> _ { +//! let figment = Figment::from(rocket::Config::default()) +//! .merge(Serialized::defaults(Config::default())) +//! .merge(Toml::file("App.toml")) +//! .merge(Env::prefixed("APP_")); +//! +//! rocket::custom(figment) +//! .mount("/", routes![/* .. */]) +//! .attach(AdHoc::config::()) +//! } +//! ``` +//! +//! [`rocket::custom()`]: crate::rocket::custom() +//! [`rocket::ignite()`]: crate::rocket::ignite() +//! [`Toml`]: figment::providers::Toml +//! [`Json`]: figment::providers::Json -mod error; -mod environment; +mod secret_key; mod config; -mod builder; -mod toml_ext; -mod custom_values; +mod tls; -use std::env; -use std::fs::File; -use std::collections::HashMap; -use std::io::Read; -use std::path::{Path, PathBuf}; +#[doc(hidden)] pub use config::pretty_print_error; -use toml; - -pub use toml::value::{Array, Map, Table, Value, Datetime}; -pub use self::error::ConfigError; -pub use self::environment::Environment; -pub use self::config::Config; -pub use self::builder::ConfigBuilder; -pub use crate::logger::LoggingLevel; -pub(crate) use self::toml_ext::LoggedValue; - -use crate::logger::COLORS_ENV; -use crate::http::uncased; -use self::Environment::*; -use self::environment::CONFIG_ENV; -use self::toml_ext::parse_simple_toml_value; - -const CONFIG_FILENAME: &str = "Rocket.toml"; -const GLOBAL_ENV_NAME: &str = "global"; -const ENV_VAR_PREFIX: &str = "ROCKET_"; - -const CODEGEN_DEBUG_ENV: &str = "ROCKET_CODEGEN_DEBUG"; -const CONFIG_FILE_ENV: &str = "ROCKET_CONFIG_FILE"; -const PREHANDLED_VARS: [&str; 4] = [CODEGEN_DEBUG_ENV, CONFIG_FILE_ENV, CONFIG_ENV, COLORS_ENV]; - -/// Wraps `std::result` with the error type of [`ConfigError`]. -pub type Result = std::result::Result; - -/// Stores a "full" config, which is all `Config`s for every environment. -#[derive(Debug, PartialEq)] -pub(crate) struct FullConfig { - pub active_env: Environment, - config: HashMap, -} - -impl FullConfig { - /// Read the configuration from the `Rocket.toml` file. The file is searched - /// for recursively up the tree, starting from the CWD. - pub fn read_from(path: &Path) -> Result { - // Try to open the config file for reading. - let mut handle = File::open(path).map_err(|_| ConfigError::IoError)?; - - // Read the configure file to a string for parsing. - let mut contents = String::new(); - handle.read_to_string(&mut contents).map_err(|_| ConfigError::IoError)?; - - // Parse the config and return the result. - let mut config = FullConfig::parse(contents, path)?; - - // Override any config values with those from the environment. - config.override_from_env()?; - - Ok(config) - } - - /// Return the default configuration for all environments and marks the - /// active environment (from `CONFIG_ENV`) as active. Overrides the defaults - /// with values from the `ROCKET_{PARAM}` environment variables. Doesn't - /// read any other sources. - pub fn env_default() -> Result { - let mut config = Self::active_default_with_path(None)?; - config.override_from_env()?; - Ok(config) - } - - /// Return the default configuration for all environments and marks the - /// active environment (from `CONFIG_ENV`) as active. This doesn't read - /// `filename`, nor any other config values from any source; it simply uses - /// `filename` to set up the config path property in the returned `Config`. - fn active_default_with_path(path: Option<&Path>) -> Result { - let mut defaults = HashMap::new(); - if let Some(path) = path { - defaults.insert(Development, Config::default_from(Development, &path)?); - defaults.insert(Staging, Config::default_from(Staging, &path)?); - defaults.insert(Production, Config::default_from(Production, &path)?); - } else { - defaults.insert(Development, Config::default(Development)?); - defaults.insert(Staging, Config::default(Staging)?); - defaults.insert(Production, Config::default(Production)?); - } - - Ok(FullConfig { - active_env: Environment::active()?, - config: defaults, - }) - } - - /// Returns the path to the config file that should be parsed. - /// - /// If the environment variable `CONFIG_FILE_ENV` is set, that path is - /// assumed to be the config file. Assuming such a file exists, that path is - /// returned. If the file doesn't exist, an error is returned. - /// - /// If the variable isn't set, Iteratively search for `CONFIG_FILENAME` - /// starting at the current working directory and working up through its - /// parents. Returns the path to the discovered file. - fn find_config_path() -> Result { - if let Some(path) = env::var_os(CONFIG_FILE_ENV) { - let config = Path::new(&path); - if config.metadata().map_or(false, |m| m.is_file()) { - return Ok(config.into()); - } else { - let msg = "The user-supplied config file does not exist."; - return Err(ConfigError::BadFilePath(config.into(), msg)); - } - } - - let cwd = env::current_dir().map_err(|_| ConfigError::NotFound)?; - let mut current = cwd.as_path(); - - loop { - let config = current.join(CONFIG_FILENAME); - if config.metadata().map_or(false, |m| m.is_file()) { - return Ok(config); - } - - match current.parent() { - Some(p) => current = p, - None => break, - } - } - - Err(ConfigError::NotFound) - } - - #[inline] - fn get_mut(&mut self, env: Environment) -> &mut Config { - match self.config.get_mut(&env) { - Some(config) => config, - None => panic!("set(): {} config is missing.", env), - } - } - - /// Set the configuration for the environment `env` to be the configuration - /// derived from the TOML table `kvs`. The environment must already exist in - /// `self`, otherwise this function panics. Any existing values are - /// overridden by those in `kvs`. - fn set_from_table(&mut self, env: Environment, kvs: &Table) -> Result<()> { - for (key, value) in kvs { - self.get_mut(env).set_raw(key, value)?; - } - - Ok(()) - } - - /// Retrieves the `Config` for the environment `env`. - #[cfg(test)] - pub fn get(&self, env: Environment) -> &Config { - match self.config.get(&env) { - Some(config) => config, - None => panic!("get(): {} config is missing.", env), - } - } - - /// Retrieves the `Config` for the active environment. - #[cfg(test)] - pub fn active(&self) -> &Config { - self.get(self.active_env) - } - - /// Retrieves the `Config` for the active environment. - pub fn take_active(mut self) -> Config { - self.config.remove(&self.active_env).expect("missing active config") - } - - // Override all environments with values from env variables if present. - fn override_from_env(&mut self) -> Result<()> { - for (key, val) in env::vars() { - if key.len() < ENV_VAR_PREFIX.len() { - continue - } else if !uncased::eq(&key[..ENV_VAR_PREFIX.len()], ENV_VAR_PREFIX) { - continue - } - - // Skip environment variables that are handled elsewhere. - 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 = 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)) - }; - - for env in &Environment::ALL { - 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 */ } - } - } - } - - Ok(()) - } - - /// Parses the configuration from the Rocket.toml file. Also overrides any - /// values there with values from the environment. - fn parse(src: S, filename: P) -> Result - where S: Into, P: AsRef - { - use self::ConfigError::ParseError; - - // Parse the source as TOML, if possible. - let src = src.into(); - let path = filename.as_ref().to_path_buf(); - let table = match src.parse::() { - Ok(toml::Value::Table(table)) => table, - Ok(value) => { - let err = format!("expected a table, found {}", value.type_str()); - return Err(ConfigError::ParseError(src, path, err, Some((1, 1)))); - } - Err(e) => return Err(ParseError(src, path, e.to_string(), e.line_col())) - }; - - // Create a config with the defaults; set the env to the active one. - let mut config = FullConfig::active_default_with_path(Some(filename.as_ref()))?; - - // Store all of the global overrides, if any, for later use. - let mut global = None; - - // Parse the values from the TOML file. - for (entry, value) in table { - // Each environment must be a table. - let kv_pairs = match value.as_table() { - Some(table) => table, - None => return Err(ConfigError::BadType( - entry, "a table", value.type_str(), Some(path.clone()) - )) - }; - - // Store the global table for later use and move on. - if entry.as_str() == GLOBAL_ENV_NAME { - global = Some(kv_pairs.clone()); - continue; - } - - // This is not the global table. Parse the environment name from the - // table entry name and then set all of the key/values. - match entry.as_str().parse() { - Ok(env) => config.set_from_table(env, kv_pairs)?, - Err(_) => Err(ConfigError::BadEntry(entry.clone(), path.clone()))? - } - } - - // Override all of the environments with the global values. - if let Some(ref global_kv_pairs) = global { - for env in &Environment::ALL { - config.set_from_table(*env, global_kv_pairs)?; - } - } - - Ok(config) - } -} +pub use config::Config; +pub use crate::logger::LogLevel; +pub use secret_key::SecretKey; +pub use tls::TlsConfig; #[cfg(test)] -mod test { - use std::env; - use std::sync::Mutex; +mod tests { + use std::net::Ipv4Addr; + use figment::Figment; - use super::{Config, FullConfig, ConfigError, ConfigBuilder}; - use super::{Environment, GLOBAL_ENV_NAME}; - use super::environment::CONFIG_ENV; - use super::Environment::*; - use super::Result; + use crate::config::{Config, TlsConfig}; + use crate::logger::LogLevel; + use crate::data::{Limits, ToByteUnit}; - use crate::logger::LoggingLevel; + #[test] + fn test_default_round_trip() { + let figment = Figment::from(Config::default()); - const TEST_CONFIG_FILENAME: &'static str = "/tmp/testing/Rocket.toml"; + assert_eq!(figment.profile(), Config::DEFAULT_PROFILE); - // TODO: It's a shame we have to depend on lazy_static just for this. - lazy_static::lazy_static! { - static ref ENV_LOCK: Mutex = Mutex::new(0); - } + #[cfg(debug_assertions)] + assert_eq!(figment.profile(), Config::DEBUG_PROFILE); - macro_rules! check_config { - ($rconfig:expr, $econfig:expr) => ( - let expected = $econfig.finalize().unwrap(); - match $rconfig { - Ok(config) => assert_eq!(config.active(), &expected), - Err(e) => panic!("Config {} failed: {:?}", stringify!($rconfig), e) - } - ); + #[cfg(not(debug_assertions))] + assert_eq!(figment.profile(), Config::RELEASE_PROFILE); - ($env:expr, $rconfig:expr, $econfig:expr) => ( - let expected = $econfig.finalize().unwrap(); - match $rconfig { - Ok(ref config) => assert_eq!(config.get($env), &expected), - Err(ref e) => panic!("Config {} failed: {:?}", stringify!($rconfig), e) - } - ); - } + let config: Config = figment.extract().unwrap(); + assert_eq!(config, Config::default()); - fn env_default() -> Result { - FullConfig::env_default() - } + #[cfg(debug_assertions)] + assert_eq!(config, Config::debug_default()); - fn default_config(env: Environment) -> ConfigBuilder { - ConfigBuilder::new(env) + #[cfg(not(debug_assertions))] + assert_eq!(config, Config::release_default()); + + assert_eq!(Config::from(Config::default()), Config::default()); } #[test] - fn test_defaults() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); + fn test_profile_env() { + figment::Jail::expect_with(|jail| { + jail.set_env("ROCKET_PROFILE", "debug"); + let figment = Figment::from(Config::default()); + assert_eq!(figment.profile(), "debug"); - // First, without an environment. Should get development defaults on - // debug builds and productions defaults on non-debug builds. - env::remove_var(CONFIG_ENV); - #[cfg(debug_assertions)] check_config!(env_default(), default_config(Development)); - #[cfg(not(debug_assertions))] check_config!(env_default(), default_config(Production)); + jail.set_env("ROCKET_PROFILE", "release"); + let figment = Figment::from(Config::default()); + assert_eq!(figment.profile(), "release"); - // Now with an explicit dev environment. - for env in &["development", "dev"] { - env::set_var(CONFIG_ENV, env); - check_config!(env_default(), default_config(Development)); - } + jail.set_env("ROCKET_PROFILE", "random"); + let figment = Figment::from(Config::default()); + assert_eq!(figment.profile(), "random"); - // Now staging. - for env in &["stage", "staging"] { - env::set_var(CONFIG_ENV, env); - check_config!(env_default(), default_config(Staging)); - } - - // Finally, production. - for env in &["prod", "production"] { - env::set_var(CONFIG_ENV, env); - check_config!(env_default(), default_config(Production)); - } + Ok(()) + }); } #[test] - fn test_bad_environment_vars() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); + fn test_toml_file() { + figment::Jail::expect_with(|jail| { + jail.create_file("Rocket.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = 0 + "#)?; - for env in &["", "p", "pr", "pro", "prodo", " prod", "dev ", "!dev!", "🚀 "] { - env::set_var(CONFIG_ENV, env); - let err = ConfigError::BadEnv(env.to_string()); - assert!(env_default().err().map_or(false, |e| e == err)); - } + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: false, + ..Config::default() + }); - // Test that a bunch of invalid environment names give the right error. - env::remove_var(CONFIG_ENV); - for env in &["p", "pr", "pro", "prodo", "bad", "meow", "this", "that"] { - let toml_table = format!("[{}]\n", env); - let e_str = env.to_string(); - let err = ConfigError::BadEntry(e_str, TEST_CONFIG_FILENAME.into()); - assert!(FullConfig::parse(toml_table, TEST_CONFIG_FILENAME) - .err().map_or(false, |e| e == err)); - } + jail.create_file("Rocket.toml", r#" + [global] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = 0 + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: false, + ..Config::default() + }); + + jail.create_file("Rocket.toml", r#" + [global] + ctrlc = 0 + + [global.tls] + certs = "/ssl/cert.pem" + key = "/ssl/key.pem" + + [global.limits] + forms = "1mib" + json = "10mib" + stream = "50kib" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + ctrlc: false, + tls: Some(TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")), + limits: Limits::default() + .limit("forms", 1.mebibytes()) + .limit("json", 10.mebibytes()) + .limit("stream", 50.kibibytes()), + ..Config::default() + }); + + jail.create_file("Rocket.toml", r#" + [global.tls] + certs = "cert.pem" + key = "key.pem" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + tls: Some(TlsConfig::from_paths( + jail.directory().join("cert.pem"), jail.directory().join("key.pem") + )), + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = 0 + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: false, + ..Config::default() + }); + + Ok(()) + }); } #[test] - fn test_good_full_config_files() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); + fn test_profiles_merge() { + figment::Jail::expect_with(|jail| { + jail.create_file("Rocket.toml", r#" + [default.limits] + stream = "50kb" - let config_str = r#" - address = "1.2.3.4" - port = 7810 - workers = 21 - log = "critical" - keep_alive = 0 - secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" - template_dir = "mine" - json = true - pi = 3.14 - "#; + [global] + limits = { forms = "2kb" } - let mut expected = default_config(Development) - .address("1.2.3.4") - .port(7810) - .workers(21) - .log_level(LoggingLevel::Critical) - .keep_alive(0) - .secret_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=") - .extra("template_dir", "mine") - .extra("json", true) - .extra("pi", 3.14); + [debug.limits] + file = "100kb" + "#)?; - expected.environment = Development; - let dev_config = ["[dev]", config_str].join("\n"); - let parsed = FullConfig::parse(dev_config, TEST_CONFIG_FILENAME); - check_config!(Development, parsed, expected.clone()); - check_config!(Staging, parsed, default_config(Staging)); - check_config!(Production, parsed, default_config(Production)); + jail.set_env("ROCKET_PROFILE", "unknown"); + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + limits: Limits::default() + .limit("stream", 50.kilobytes()) + .limit("forms", 2.kilobytes()), + ..Config::default() + }); - expected.environment = Staging; - let stage_config = ["[stage]", config_str].join("\n"); - let parsed = FullConfig::parse(stage_config, TEST_CONFIG_FILENAME); - check_config!(Staging, parsed, expected.clone()); - check_config!(Development, parsed, default_config(Development)); - check_config!(Production, parsed, default_config(Production)); + jail.set_env("ROCKET_PROFILE", "debug"); + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + limits: Limits::default() + .limit("stream", 50.kilobytes()) + .limit("forms", 2.kilobytes()) + .limit("file", 100.kilobytes()), + ..Config::default() + }); - expected.environment = Production; - let prod_config = ["[prod]", config_str].join("\n"); - let parsed = FullConfig::parse(prod_config, TEST_CONFIG_FILENAME); - check_config!(Production, parsed, expected); - check_config!(Development, parsed, default_config(Development)); - check_config!(Staging, parsed, default_config(Staging)); + Ok(()) + }); } #[test] - fn test_good_address_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"); + fn test_env_vars_merge() { + figment::Jail::expect_with(|jail| { + jail.set_env("ROCKET_PORT", 9999); + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + port: 9999, + ..Config::default() + }); - check_config!(FullConfig::parse(r#" - [development] - address = "localhost" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("localhost") - }); + jail.set_env("ROCKET_TLS", r#"{certs="certs.pem"}"#); + let first_figment = Config::figment(); + jail.set_env("ROCKET_TLS", r#"{key="key.pem"}"#); + let prev_figment = Config::figment().join(&first_figment); + let config = Config::from(&prev_figment); + assert_eq!(config, Config { + port: 9999, + tls: Some(TlsConfig::from_paths("certs.pem", "key.pem")), + ..Config::default() + }); - check_config!(FullConfig::parse(r#" - [development] - address = "127.0.0.1" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("127.0.0.1") - }); + jail.set_env("ROCKET_TLS", r#"{certs="new.pem"}"#); + let config = Config::from(Config::figment().join(&prev_figment)); + assert_eq!(config, Config { + port: 9999, + tls: Some(TlsConfig::from_paths("new.pem", "key.pem")), + ..Config::default() + }); - check_config!(FullConfig::parse(r#" - [development] - address = "::" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("::") - }); + jail.set_env("ROCKET_LIMITS", r#"{stream=100kiB}"#); + let config = Config::from(Config::figment().join(&prev_figment)); + assert_eq!(config, Config { + port: 9999, + tls: Some(TlsConfig::from_paths("new.pem", "key.pem")), + limits: Limits::default().limit("stream", 100.kibibytes()), + ..Config::default() + }); - check_config!(FullConfig::parse(r#" - [dev] - address = "2001:db8::370:7334" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("2001:db8::370:7334") - }); - - check_config!(FullConfig::parse(r#" - [dev] - address = "0.0.0.0" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Development).address("0.0.0.0") - }); + Ok(()) + }); } #[test] - fn test_bad_address_values() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [development] - address = 0000 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [development] - address = true - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [development] - address = "........" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - address = "1.2.3.4:100" - "#.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!(FullConfig::parse(r#" - [staging] - tls = { certs = "some/path.pem", key = "some/key.pem" } - "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); - - assert!(FullConfig::parse(r#" - [staging.tls] - certs = "some/path.pem" - key = "some/key.pem" - "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); - - assert!(FullConfig::parse(r#" - [global.tls] - certs = "some/path.pem" - key = "some/key.pem" - "#.to_string(), TEST_CONFIG_FILENAME).is_ok()); - - assert!(FullConfig::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!(FullConfig::parse(r#" - [development] - tls = "hello" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [development] - tls = { certs = "some/path.pem" } - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [development] - tls = { certs = "some/path.pem", key = "some/key.pem", extra = "bah" } - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::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. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::set_var(CONFIG_ENV, "stage"); - - check_config!(FullConfig::parse(r#" - [stage] - port = 100 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).port(100) - }); - - check_config!(FullConfig::parse(r#" - [stage] - port = 6000 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).port(6000) - }); - - check_config!(FullConfig::parse(r#" - [stage] - port = 65535 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).port(65535) - }); - } - - #[test] - fn test_bad_port_values() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [development] - port = true - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [production] - port = "hello" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - port = -1 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - port = 65536 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - port = 105836 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - fn test_good_workers_values() { - // 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!(FullConfig::parse(r#" - [stage] - workers = 1 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).workers(1) - }); - - check_config!(FullConfig::parse(r#" - [stage] - workers = 300 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).workers(300) - }); - - check_config!(FullConfig::parse(r#" - [stage] - workers = 65535 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).workers(65535) - }); - } - - #[test] - fn test_bad_workers_values() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [development] - workers = true - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [production] - workers = "hello" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - workers = -1 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - workers = 65536 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [staging] - workers = 105836 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - fn test_good_keep_alives() { - // 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!(FullConfig::parse(r#" - [stage] - keep_alive = 10 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).keep_alive(10) - }); - - check_config!(FullConfig::parse(r#" - [stage] - keep_alive = 0 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).keep_alive(0) - }); - - check_config!(FullConfig::parse(r#" - [stage] - keep_alive = 348 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).keep_alive(348) - }); - - check_config!(FullConfig::parse(r#" - [stage] - keep_alive = 0 - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).keep_alive(0) - }); - } - - #[test] - fn test_bad_keep_alives() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [dev] - keep_alive = true - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - keep_alive = -10 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - keep_alive = "Some(10)" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - keep_alive = 4294967296 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - fn test_good_log_levels() { - // 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!(FullConfig::parse(r#" - [stage] - log = "normal" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).log_level(LoggingLevel::Normal) - }); - - - check_config!(FullConfig::parse(r#" - [stage] - log = "debug" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).log_level(LoggingLevel::Debug) - }); - - check_config!(FullConfig::parse(r#" - [stage] - log = "critical" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).log_level(LoggingLevel::Critical) - }); - - check_config!(FullConfig::parse(r#" - [stage] - log = "off" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).log_level(LoggingLevel::Off) - }); - } - - #[test] - fn test_bad_log_level_values() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [dev] - log = false - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [development] - log = 0 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [prod] - log = "no" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - 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!(FullConfig::parse(r#" - [stage] - secret_key = "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).secret_key( - "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0=" - ) - }); - - check_config!(FullConfig::parse(r#" - [stage] - secret_key = "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" - "#.to_string(), TEST_CONFIG_FILENAME), { - default_config(Staging).secret_key( - "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU=" - ) - }); - } - - #[test] - 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!(FullConfig::parse(r#" - [dev] - secret_key = true - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - secret_key = 1283724897238945234897 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - secret_key = "abcv" - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - fn test_bad_toml() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - env::remove_var(CONFIG_ENV); - - assert!(FullConfig::parse(r#" - [dev - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - 1. = 2 - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - - assert!(FullConfig::parse(r#" - [dev] - secret_key = "abcv" = other - "#.to_string(), TEST_CONFIG_FILENAME).is_err()); - } - - #[test] - fn test_global_overrides() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - - // Test first that we can override each environment. - for env in &Environment::ALL { - env::set_var(CONFIG_ENV, env.to_string()); - - check_config!(FullConfig::parse(format!(r#" - [{}] - address = "::1" - "#, GLOBAL_ENV_NAME), TEST_CONFIG_FILENAME), { - default_config(*env).address("::1") - }); - - check_config!(FullConfig::parse(format!(r#" - [{}] - database = "mysql" - "#, GLOBAL_ENV_NAME), TEST_CONFIG_FILENAME), { - default_config(*env).extra("database", "mysql") - }); - - check_config!(FullConfig::parse(format!(r#" - [{}] - port = 3980 - "#, GLOBAL_ENV_NAME), TEST_CONFIG_FILENAME), { - default_config(*env).port(3980) - }); - } - } - - #[test] - fn test_env_override() { - // Take the lock so changing the environment doesn't cause races. - let _env_lock = ENV_LOCK.lock().unwrap(); - - let pairs = [ - ("log", "critical"), ("LOG", "debug"), ("PORT", "8110"), - ("address", "1.2.3.4"), ("EXTRA_EXTRA", "true"), ("workers", "3") - ]; - - let check_value = |key: &str, val: &str, config: &Config| { - match key { - "log" => assert_eq!(config.log_level, val.parse().unwrap()), - "port" => assert_eq!(config.port, val.parse::().unwrap()), - "address" => assert_eq!(config.address, val), - "extra_extra" => assert_eq!(config.get_bool(key).unwrap(), true), - "workers" => assert_eq!(config.workers, val.parse::().unwrap()), - _ => panic!("Unexpected key: {}", key) - } - }; - - // Check that setting the environment variable actually changes the - // config for the default active and nonactive environments. - for &(key, val) in &pairs { - env::set_var(format!("ROCKET_{}", key), val); - - // Check that it overrides the active config. - for env in &Environment::ALL { - env::set_var(CONFIG_ENV, env.to_string()); - let rconfig = env_default().unwrap(); - check_value(&*key.to_lowercase(), val, rconfig.active()); - } - - // And non-active configs. - let rconfig = env_default().unwrap(); - for env in &Environment::ALL { - check_value(&*key.to_lowercase(), val, rconfig.get(*env)); - } - } - - // Clear the variables so they don't override for the next test. - for &(key, _) in &pairs { - env::remove_var(format!("ROCKET_{}", key)) - } - - // Now we build a config file to test that the environment variables - // override configurations from files as well. - let toml = r#" - [dev] - address = "1.2.3.4" - - [stage] - address = "2.3.4.5" - - [prod] - address = "10.1.1.1" - - [global] - address = "1.2.3.4" - port = 7810 - workers = 21 - log = "normal" - "#; - - // Check that setting the environment variable actually changes the - // config for the default active environments. - for &(key, val) in &pairs { - env::set_var(format!("ROCKET_{}", key), val); - - let mut r = FullConfig::parse(toml, TEST_CONFIG_FILENAME).unwrap(); - r.override_from_env().unwrap(); - check_value(&*key.to_lowercase(), val, r.active()); - - // And non-active configs. - for env in &Environment::ALL { - check_value(&*key.to_lowercase(), val, r.get(*env)); - } - } - - // Clear the variables so they don't override for the next test. - for &(key, _) in &pairs { - env::remove_var(format!("ROCKET_{}", key)) - } + fn test_precedence() { + figment::Jail::expect_with(|jail| { + jail.create_file("Rocket.toml", r#" + [global.limits] + forms = "1mib" + stream = "50kb" + file = "100kb" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + limits: Limits::default() + .limit("forms", 1.mebibytes()) + .limit("stream", 50.kilobytes()) + .limit("file", 100.kilobytes()), + ..Config::default() + }); + + jail.set_env("ROCKET_LIMITS", r#"{stream=3MiB,capture=2MiB}"#); + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + limits: Limits::default() + .limit("file", 100.kilobytes()) + .limit("forms", 1.mebibytes()) + .limit("stream", 3.mebibytes()) + .limit("capture", 2.mebibytes()), + ..Config::default() + }); + + jail.set_env("ROCKET_PROFILE", "foo"); + let val: Result = Config::figment().extract_inner("profile"); + assert!(val.is_err()); + + Ok(()) + }); } } diff --git a/core/lib/src/config/secret_key.rs b/core/lib/src/config/secret_key.rs new file mode 100644 index 00000000..13b7f3ca --- /dev/null +++ b/core/lib/src/config/secret_key.rs @@ -0,0 +1,218 @@ +use std::fmt; +use std::ops::Deref; + +use serde::{de, ser, Deserialize, Serialize}; + +use crate::http::private::cookie::Key; +use crate::request::{Outcome, Request, FromRequest}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +enum Kind { + Zero, + Generated, + Provided +} + +/// A cryptographically secure secret key. +/// +/// A `SecretKey` is primarily used by [private cookies]. See the [configuration +/// guide] for further details. It can be configured from 256-bit random +/// material or a 512-bit master key, each as either a base64-encoded string or +/// raw bytes. When compiled in debug mode with the `secrets` feature enabled, a +/// key set a `0` is automatically regenerated from the OS's random source if +/// available. +/// +/// ```rust +/// # use rocket::figment::Figment; +/// let figment = Figment::from(rocket::Config::default()) +/// .merge(("secret_key", "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=")); +/// +/// assert!(!rocket::Config::from(figment).secret_key.is_zero()); +/// +/// let figment = Figment::from(rocket::Config::default()) +/// .merge(("secret_key", vec![0u8; 64])); +/// +/// # /* as far as I can tell, there's no way to test this properly +/// # https://github.com/rust-lang/cargo/issues/6570 +/// # https://github.com/rust-lang/cargo/issues/4737 +/// # https://github.com/rust-lang/rust/issues/43031 +/// assert!(!rocket::Config::from(figment).secret_key.is_zero()); +/// # */ +/// ``` +/// +/// [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies +/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/#secret-key +#[derive(PartialEq, Clone)] +pub struct SecretKey { + key: Key, + kind: Kind, +} + +impl SecretKey { + /// Returns a secret key that is all zeroes. + pub(crate) fn zero() -> SecretKey { + SecretKey { key: Key::from(&[0; 64]), kind: Kind::Zero } + } + + /// Creates a `SecretKey` from a 512-bit `master` key. For security, + /// `master` _must_ be cryptographically random. + /// + /// # Panics + /// + /// Panics if `master` < 64 bytes. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::SecretKey; + /// + /// # let master = vec![0u8; 64]; + /// let key = SecretKey::from(&master); + /// ``` + pub fn from(master: &[u8]) -> SecretKey { + let kind = match master.iter().all(|&b| b == 0) { + true => Kind::Zero, + false => Kind::Provided + }; + + SecretKey { key: Key::from(master), kind } + } + + /// Derives a `SecretKey` from 256 bits of cryptographically random + /// `material`. For security, `material` _must_ be cryptographically random. + /// + /// # Panics + /// + /// Panics if `material` < 32 bytes. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::SecretKey; + /// + /// # let material = vec![0u8; 32]; + /// let key = SecretKey::derive_from(&material); + /// ``` + pub fn derive_from(material: &[u8]) -> SecretKey { + SecretKey { key: Key::derive_from(material), kind: Kind::Provided } + } + + /// Attempts to generate a `SecretKey` from randomness retrieved from the + /// OS. If randomness from the OS isn't available, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::SecretKey; + /// + /// let key = SecretKey::generate(); + /// ``` + pub fn generate() -> Option { + Some(SecretKey { key: Key::try_generate()?, kind: Kind::Generated }) + } + + /// Returns `true` if `self` is the `0`-key. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::SecretKey; + /// + /// let master = vec![0u8; 64]; + /// let key = SecretKey::from(&master); + /// assert!(key.is_zero()); + /// ``` + pub fn is_zero(&self) -> bool { + self.kind == Kind::Zero + } +} + +#[doc(hidden)] +impl Deref for SecretKey { + type Target = Key; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +#[crate::async_trait] +impl<'a, 'r> FromRequest<'a, 'r> for &'a SecretKey { + type Error = std::convert::Infallible; + + async fn from_request(req: &'a Request<'r>) -> Outcome { + Outcome::Success(&req.state.config.secret_key) + } +} + +impl Serialize for SecretKey { + fn serialize(&self, ser: S) -> Result { + // We encode as "zero" to avoid leaking the key. + ser.serialize_bytes(&[0; 32][..]) + } +} + +impl<'de> Deserialize<'de> for SecretKey { + fn deserialize>(de: D) -> Result { + use {binascii::{b64decode, hex2bin}, de::Unexpected::Str}; + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = SecretKey; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("256-bit base64 or hex string, or 32-byte slice") + } + + fn visit_str(self, val: &str) -> Result { + let e = |s| E::invalid_value(Str(s), &"256-bit base64 or hex"); + + // `binascii` requires a more space than actual output for padding + let mut buf = [0u8; 96]; + let bytes = match val.len() { + 44 | 88 => b64decode(val.as_bytes(), &mut buf).map_err(|_| e(val))?, + 64 => hex2bin(val.as_bytes(), &mut buf).map_err(|_| e(val))?, + n => Err(E::invalid_length(n, &"44 or 88 for base64, 64 for hex"))? + }; + + self.visit_bytes(bytes) + } + + fn visit_bytes(self, bytes: &[u8]) -> Result { + if bytes.len() < 32 { + Err(E::invalid_length(bytes.len(), &"at least 32")) + } else if bytes.iter().all(|b| *b == 0) { + Ok(SecretKey::zero()) + } else if bytes.len() >= 64 { + Ok(SecretKey::from(bytes)) + } else { + Ok(SecretKey::derive_from(bytes)) + } + } + + fn visit_seq(self, mut seq: A) -> Result + where A: de::SeqAccess<'de> + { + let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(byte) = seq.next_element()? { + bytes.push(byte); + } + + self.visit_bytes(&bytes) + } + } + + de.deserialize_any(Visitor) + } +} + +impl fmt::Debug for SecretKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.kind { + Kind::Zero => f.write_str("[zero]"), + Kind::Generated => f.write_str("[generated]"), + Kind::Provided => f.write_str("[provided]"), + } + } +} diff --git a/core/lib/src/config/tls.rs b/core/lib/src/config/tls.rs new file mode 100644 index 00000000..77270a92 --- /dev/null +++ b/core/lib/src/config/tls.rs @@ -0,0 +1,148 @@ +use figment::value::magic::{Either, RelativePathBuf}; +use serde::{Deserialize, Serialize}; + +/// TLS configuration: a certificate chain and a private key. +/// +/// Both `certs` and `key` can be configured as a path or as raw bytes. `certs` +/// must be a DER-encoded X.509 TLS certificate chain, while `key` must be a +/// DER-encoded ASN.1 key in either PKCS#8 or PKCS#1 format. +/// +/// The following example illustrates manual configuration: +/// +/// ```rust +/// # use rocket::figment::Figment; +/// let figment = Figment::from(rocket::Config::default()) +/// .merge(("tls.certs", "strings/are/paths/certs.pem")) +/// .merge(("tls.key", vec![0; 32])); +/// +/// let config = rocket::Config::from(figment); +/// let tls_config = config.tls.as_ref().unwrap(); +/// assert!(tls_config.certs().is_left()); +/// assert!(tls_config.key().is_right()); +/// ``` +/// +/// When a path is configured in a file source, such as `Rocket.toml`, relative +/// paths are interpreted as being relative to the source file's directory. +#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)] +pub struct TlsConfig { + /// Path or raw bytes for the DER-encoded X.509 TLS certificate chain. + pub(crate) certs: Either>, + /// Path or raw bytes to DER-encoded ASN.1 key in either PKCS#8 or PKCS#1 + /// format. + pub(crate) key: Either>, +} + +impl TlsConfig { + /// Constructs a `TlsConfig` from paths to a `certs` certificate-chain + /// a `key` private-key. This method does no validation; it simply creates a + /// structure suitable for passing into a [`Config`]. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::TlsConfig; + /// + /// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem"); + /// ``` + pub fn from_paths(certs: C, key: K) -> Self + where C: AsRef, K: AsRef + { + TlsConfig { + certs: Either::Left(certs.as_ref().to_path_buf().into()), + key: Either::Left(key.as_ref().to_path_buf().into()) + } + } + + /// Constructs a `TlsConfig` from byte buffers to a `certs` + /// certificate-chain a `key` private-key. This method does no validation; + /// it simply creates a structure suitable for passing into a [`Config`]. + /// + /// # Example + /// + /// ```rust + /// use rocket::config::TlsConfig; + /// + /// # let certs_buf = &[]; + /// # let key_buf = &[]; + /// let tls_config = TlsConfig::from_bytes(certs_buf, key_buf); + /// ``` + pub fn from_bytes(certs: &[u8], key: &[u8]) -> Self { + TlsConfig { + certs: Either::Right(certs.to_vec().into()), + key: Either::Right(key.to_vec().into()) + } + } + + /// Returns the value of the `certs` parameter. + /// + /// # Example + /// + /// ```rust + /// # use rocket::figment::Figment; + /// let figment = Figment::from(rocket::Config::default()) + /// .merge(("tls.certs", vec![0; 32])) + /// .merge(("tls.key", "/etc/ssl/key.pem")); + /// + /// let config = rocket::Config::from(figment); + /// let tls_config = config.tls.as_ref().unwrap(); + /// let cert_bytes = tls_config.certs().right().unwrap(); + /// assert!(cert_bytes.iter().all(|&b| b == 0)); + /// ``` + pub fn certs(&self) -> either::Either { + match &self.certs { + Either::Left(path) => either::Either::Left(path.relative()), + Either::Right(bytes) => either::Either::Right(&bytes), + } + } + + /// Returns the value of the `key` parameter. + /// + /// # Example + /// + /// ```rust + /// # use rocket::figment::Figment; + /// # use std::path::Path; + /// let figment = Figment::from(rocket::Config::default()) + /// .merge(("tls.certs", vec![0; 32])) + /// .merge(("tls.key", "/etc/ssl/key.pem")); + /// + /// let config = rocket::Config::from(figment); + /// let tls_config = config.tls.as_ref().unwrap(); + /// let key_path = tls_config.key().left().unwrap(); + /// assert_eq!(key_path, Path::new("/etc/ssl/key.pem")); + /// ``` + pub fn key(&self) -> either::Either { + match &self.key { + Either::Left(path) => either::Either::Left(path.relative()), + Either::Right(bytes) => either::Either::Right(&bytes), + } + } +} + +#[cfg(feature = "tls")] +type Reader = Box; + +#[cfg(feature = "tls")] +impl TlsConfig { + pub(crate) fn to_readers(&self) -> std::io::Result<(Reader, Reader)> { + use std::{io::{self, Error}, fs}; + use yansi::Paint; + + fn to_reader(value: &Either>) -> io::Result { + match value { + Either::Left(path) => { + let path = path.relative(); + let file = fs::File::open(&path).map_err(move |e| { + Error::new(e.kind(), format!("error reading TLS file `{}`: {}", + Paint::white(figment::Source::File(path)), e)) + })?; + + Ok(Box::new(io::BufReader::new(file))) + } + Either::Right(vec) => Ok(Box::new(io::Cursor::new(vec.clone()))), + } + } + + Ok((to_reader(&self.certs)?, to_reader(&self.key)?)) + } +} diff --git a/core/lib/src/config/toml_ext.rs b/core/lib/src/config/toml_ext.rs deleted file mode 100644 index f1a42e06..00000000 --- a/core/lib/src/config/toml_ext.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::fmt; -use std::result::Result as StdResult; - -use crate::config::Value; - -use pear::macros::{parse, parser, switch}; -use pear::parsers::*; -use pear::combinators::*; - -type Input<'a> = pear::input::Pear<&'a str>; -type Result<'a, T> = pear::input::Result>; - -#[inline(always)] -pub fn is_whitespace(&byte: &char) -> bool { - byte.is_ascii_whitespace() -} - -#[inline(always)] -fn is_not_separator(&byte: &char) -> bool { - match byte { - ',' | '{' | '}' | '[' | ']' => false, - _ => true - } -} - -// FIXME: Be more permissive here? -#[inline(always)] -fn is_ident_char(&byte: &char) -> bool { - byte.is_ascii_alphanumeric() || byte == '_' || byte == '-' -} - -#[parser] -fn array<'a>(input: &mut Input<'a>) -> Result<'a, Value> { - Value::Array(delimited_collect('[', value, ',', ']')?) -} - -#[parser] -fn key<'a>(input: &mut Input<'a>) -> Result<'a, String> { - take_some_while(is_ident_char)?.to_string() -} - -#[parser] -fn key_value<'a>(input: &mut Input<'a>) -> Result<'a, (String, Value)> { - let key = (surrounded(key, is_whitespace)?, eat('=')?).0.to_string(); - (key, surrounded(value, is_whitespace)?) -} - -#[parser] -fn table<'a>(input: &mut Input<'a>) -> Result<'a, Value> { - Value::Table(delimited_collect('{', key_value, ',', '}')?) -} - -#[parser] -fn value<'a>(input: &mut Input<'a>) -> Result<'a, Value> { - skip_while(is_whitespace)?; - let val = switch! { - eat_slice("true") => Value::Boolean(true), - eat_slice("false") => Value::Boolean(false), - peek('{') => table()?, - peek('[') => array()?, - peek('"') => Value::String(delimited('"', |_| true, '"')?.to_string()), - _ => { - let value_str = take_some_while(is_not_separator)?; - if let Ok(int) = value_str.parse::() { - Value::Integer(int) - } else if let Ok(float) = value_str.parse::() { - Value::Float(float) - } else { - Value::String(value_str.into()) - } - } - }; - - skip_while(is_whitespace)?; - val -} - -pub fn parse_simple_toml_value(input: &str) -> StdResult { - parse!(value: input).map_err(|e| e.to_string()) -} - -/// A simple wrapper over a `Value` reference with a custom implementation of -/// `Display`. This is used to log config values at initialization. -pub struct LoggedValue<'a>(pub &'a Value); - -impl fmt::Display for LoggedValue<'_> { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::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 toml::map::Map; - - use super::parse_simple_toml_value; - use super::Value::{self, *}; - - 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()); - assert_parse!("[1, 2, 3]", vec![1, 2, 3].into()); - assert_parse!("[1.32, 2]", Array(vec![1.32.into(), 2.into()])); - - assert_parse!("{}", Table(Map::new())); - - assert_parse!("{a=b}", Table({ - let mut map = Map::new(); - map.insert("a".into(), "b".into()); - map - })); - - assert_parse!("{v=1, on=true,pi=3.14}", Table({ - let mut map = Map::new(); - map.insert("v".into(), 1.into()); - map.insert("on".into(), true.into()); - map.insert("pi".into(), 3.14.into()); - map - })); - - assert_parse!("{v=[1, 2, 3], v2=[a, \"b\"], on=true,pi=3.14}", Table({ - let mut map = Map::new(); - map.insert("v".into(), vec![1, 2, 3].into()); - map.insert("v2".into(), vec!["a", "b"].into()); - map.insert("on".into(), true.into()); - map.insert("pi".into(), 3.14.into()); - map - })); - - assert_parse!("{v=[[1], [2, 3], [4,5]]}", Table({ - let mut map = Map::new(); - let first: Value = vec![1].into(); - let second: Value = vec![2, 3].into(); - let third: Value = vec![4, 5].into(); - map.insert("v".into(), vec![first, second, third].into()); - map - })); - } -} diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index 24ac7c6a..03c1d49f 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -7,7 +7,7 @@ use crate::outcome::{self, IntoOutcome}; use crate::outcome::Outcome::*; use crate::http::Status; use crate::request::Request; -use crate::data::{Data, ByteUnit}; +use crate::data::Data; /// Type alias for the `Outcome` of a `FromTransformedData` conversion. pub type Outcome = outcome::Outcome; @@ -417,7 +417,7 @@ impl<'a> FromTransformedData<'a> for Data { } } -/// A varaint of [`FromTransformedData`] for data guards that don't require +/// A variant of [`FromTransformedData`] for data guards that don't require /// transformations. /// /// When transformation of incoming data isn't required, data guards should @@ -502,7 +502,8 @@ impl<'a> FromTransformedData<'a> for Data { /// } /// /// // Read the data into a String. -/// let string = match data.open(LIMIT).stream_to_string().await { +/// let limit = req.limits().get("person").unwrap_or(LIMIT); +/// let string = match data.open(limit).stream_to_string().await { /// Ok(string) => string, /// Err(e) => return Outcome::Failure((Status::InternalServerError, format!("{}", e))) /// }; @@ -602,7 +603,10 @@ impl<'a, T: FromTransformedData<'a> + 'a> FromTransformedData<'a> for Option } #[cfg(debug_assertions)] +use crate::data::ByteUnit; + #[crate::async_trait] +#[cfg(debug_assertions)] impl FromData for String { type Error = std::io::Error; @@ -615,8 +619,8 @@ impl FromData for String { } } -#[cfg(debug_assertions)] #[crate::async_trait] +#[cfg(debug_assertions)] impl FromData for Vec { type Error = std::io::Error; diff --git a/core/lib/src/data/limits.rs b/core/lib/src/data/limits.rs index 7bdb10ba..bafb0124 100644 --- a/core/lib/src/data/limits.rs +++ b/core/lib/src/data/limits.rs @@ -1,8 +1,11 @@ use std::fmt; +use serde::{Serialize, Deserialize}; +use crate::request::{Request, FromRequest, Outcome}; + use crate::data::{ByteUnit, ToByteUnit}; -/// Mapping from data type to size limits. +/// Mapping from data types to read limits. /// /// A `Limits` structure contains a mapping from a given data type ("forms", /// "json", and so on) to the maximum size in bytes that should be accepted by a @@ -12,7 +15,7 @@ use crate::data::{ByteUnit, ToByteUnit}; /// /// # Defaults /// -/// As documented in [`config`](crate::config), the default limits are as follows: +/// The default limits are: /// /// * **forms**: 32KiB /// @@ -24,38 +27,82 @@ use crate::data::{ByteUnit, ToByteUnit}; /// use rocket::data::{Limits, ToByteUnit}; /// /// // Set a limit of 64KiB for forms and 3MiB for JSON. -/// let limits = Limits::new() +/// let limits = Limits::default() /// .limit("forms", 64.kibibytes()) /// .limit("json", 3.mebibytes()); /// ``` -#[derive(Debug, Clone)] +/// +/// The configured limits can be retrieved via the `&Limits` request guard: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use std::io; +/// +/// use rocket::data::{Data, Limits, ToByteUnit}; +/// use rocket::response::Debug; +/// +/// #[post("/echo", data = "")] +/// async fn echo(data: Data, limits: &Limits) -> Result> { +/// let limit = limits.get("data").unwrap_or(1.mebibytes()); +/// Ok(data.open(limit).stream_to_string().await?) +/// } +/// ``` +/// +/// ...or via the [`Request::limits()`] method: +/// +/// ``` +/// # #[macro_use] extern crate rocket; +/// use rocket::request::Request; +/// use rocket::data::{self, Data, FromData}; +/// +/// # struct MyType; +/// # type MyError = (); +/// #[rocket::async_trait] +/// impl FromData for MyType { +/// type Error = MyError; +/// +/// async fn from_data(req: &Request<'_>, data: Data) -> data::Outcome { +/// let limit = req.limits().get("my-data-type"); +/// /* .. */ +/// # unimplemented!() +/// } +/// } +/// ``` +#[serde(transparent)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Limits { // We cache this internally but don't share that fact in the API. - pub(crate) forms: ByteUnit, - extra: Vec<(String, ByteUnit)> + #[serde(with = "figment::util::vec_tuple_map")] + limits: Vec<(String, ByteUnit)> } +/// The default limits are: +/// +/// * **forms**: 32KiB impl Default for Limits { fn default() -> Limits { // Default limit for forms is 32KiB. - Limits { forms: 32.kibibytes(), extra: Vec::new() } + Limits { limits: vec![("forms".into(), 32.kibibytes())] } } } impl Limits { - /// Construct a new `Limits` structure with the default limits set. + /// Construct a new `Limits` structure with no limits set. /// /// # Example /// /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// - /// let limits = Limits::new(); + /// let limits = Limits::default(); /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); + /// + /// let limits = Limits::new(); + /// assert_eq!(limits.get("forms"), None); /// ``` #[inline] pub fn new() -> Self { - Limits::default() + Limits { limits: vec![] } } /// Adds or replaces a limit in `self`, consuming `self` and returning a new @@ -66,7 +113,7 @@ impl Limits { /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// - /// let limits = Limits::new().limit("json", 1.mebibytes()); + /// let limits = Limits::default().limit("json", 1.mebibytes()); /// /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); /// assert_eq!(limits.get("json"), Some(1.mebibytes())); @@ -76,24 +123,12 @@ impl Limits { /// ``` pub fn limit>(mut self, name: S, limit: ByteUnit) -> Self { let name = name.into(); - match name.as_str() { - "forms" => self.forms = limit, - _ => { - let mut found = false; - for tuple in &mut self.extra { - if tuple.0 == name { - tuple.1 = limit; - found = true; - break; - } - } - - if !found { - self.extra.push((name, limit)) - } - } + match self.limits.iter_mut().find(|(k, _)| *k == name) { + Some((_, v)) => *v = limit, + None => self.limits.push((name, limit)), } + self.limits.sort_by(|a, b| a.0.cmp(&b.0)); self } @@ -104,35 +139,35 @@ impl Limits { /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// - /// let limits = Limits::new().limit("json", 64.mebibytes()); + /// let limits = Limits::default().limit("json", 64.mebibytes()); /// /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); /// assert_eq!(limits.get("json"), Some(64.mebibytes())); /// assert!(limits.get("msgpack").is_none()); /// ``` 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 + self.limits.iter() + .find(|(k, _)| *k == name) + .map(|(_, v)| *v) } } impl fmt::Display for Limits { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "forms = {}", self.forms)?; - for (key, val) in &self.extra { - write!(f, ", {}* = {}", key, val)?; + for (i, (k, v)) in self.limits.iter().enumerate() { + if i != 0 { f.write_str(", ")? } + write!(f, "{} = {}", k, v)?; } Ok(()) } } +#[crate::async_trait] +impl<'a, 'r> FromRequest<'a, 'r> for &'r Limits { + type Error = std::convert::Infallible; + + async fn from_request(req: &'a Request<'r>) -> Outcome { + Outcome::Success(req.limits()) + } +} diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 7512ebe4..5962231e 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -7,81 +7,30 @@ use yansi::Paint; use crate::router::Route; -/// An error that occurs when running a Rocket server. -/// -/// Errors can happen immediately upon launch ([`LaunchError`]) -/// or more rarely during the server's execution. -#[derive(Debug)] -pub enum Error { - Launch(LaunchError), - Run(Box), -} - -impl fmt::Display for Error { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Launch(e) => write!(f, "Rocket failed to launch: {}", e), - Error::Run(e) => write!(f, "error while running server: {}", e), - } - } -} - -impl std::error::Error for Error { } - -/// The kind of launch error that occurred. -/// -/// 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 or because a fairing -/// encountered an error; these are represented by the `Collision` and -/// `FailedFairing` variants, respectively. -#[derive(Debug)] -pub enum LaunchErrorKind { - /// Binding to the provided address/port failed. - Bind(io::Error), - /// An I/O error occurred during launch. - Io(io::Error), - /// Route collisions were detected. - Collision(Vec<(Route, Route)>), - /// A launch fairing reported an error. - FailedFairings(Vec<&'static str>), -} - /// An error that occurs during launch. /// -/// A `LaunchError` is returned by [`launch()`](crate::Rocket::launch()) when -/// launching an application fails. +/// An `Error` is returned by [`launch()`](crate::Rocket::launch()) when +/// launching an application fails or, more rarely, when the runtime fails after +/// lauching. /// /// # 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: +/// `println!("Error: {}", e)` is called, where `e: Error`, 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 -/// use rocket::error::Error; -/// /// # let _ = async { /// if let Err(error) = rocket::ignite().launch().await { -/// match error { -/// Error::Launch(error) => { -/// // This case is only reached if launching failed. This println "inspects" the error. -/// println!("Launch failed! Error: {}", error); +/// // This println "inspects" the error. +/// println!("Launch failed! Error: {}", error); /// -/// // This call to drop (explicit here for demonstration) will do nothing. -/// drop(error); -/// } -/// Error::Run(error) => { -/// // This case is reached if launching succeeds, but the server had a fatal error later -/// println!("Server failed! Error: {}", error); -/// } -/// } +/// // This call to drop (explicit here for demonstration) will do nothing. +/// drop(error); /// } -/// /// # }; /// ``` /// @@ -100,8 +49,8 @@ pub enum LaunchErrorKind { /// /// # Usage /// -/// A `LaunchError` value should usually be allowed to `drop` without -/// inspection. There are two exceptions to this suggestion. +/// An `Error` value should usually be allowed to `drop` without inspection. +/// There are at least two exceptions: /// /// 1. If you are writing a library or high-level application on-top of /// Rocket, you likely want to inspect the value before it drops to avoid a @@ -109,15 +58,42 @@ pub enum LaunchErrorKind { /// value. /// /// 2. You want to display your own error messages. -pub struct LaunchError { +pub struct Error { handled: AtomicBool, - kind: LaunchErrorKind + kind: ErrorKind } -impl LaunchError { +/// The kind error that occurred. +/// +/// 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 or because a fairing +/// encountered an error; these are represented by the `Collision` and +/// `FailedFairing` variants, respectively. +#[derive(Debug)] +pub enum ErrorKind { + /// Binding to the provided address/port failed. + Bind(io::Error), + /// An I/O error occurred during launch. + Io(io::Error), + /// An I/O error occurred in the runtime. + Runtime(Box), + /// Route collisions were detected. + Collision(Vec<(Route, Route)>), + /// A launch fairing reported an error. + FailedFairings(Vec<&'static str>), +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Error::new(kind) + } +} + +impl Error { #[inline(always)] - pub(crate) fn new(kind: LaunchErrorKind) -> LaunchError { - LaunchError { handled: AtomicBool::new(false), kind } + pub(crate) fn new(kind: ErrorKind) -> Error { + Error { handled: AtomicBool::new(false), kind } } #[inline(always)] @@ -135,43 +111,38 @@ impl LaunchError { /// # Example /// /// ```rust - /// use rocket::error::Error; + /// use rocket::error::ErrorKind; + /// /// # let _ = async { /// if let Err(error) = rocket::ignite().launch().await { - /// match error { - /// Error::Launch(err) => println!("Found a launch error: {}", err.kind()), - /// Error::Run(err) => println!("Error at runtime"), + /// match error.kind() { + /// ErrorKind::Io(e) => println!("found an i/o launch error: {}", e), + /// e => println!("something else happened: {}", e) /// } /// } /// # }; /// ``` #[inline] - pub fn kind(&self) -> &LaunchErrorKind { + pub fn kind(&self) -> &ErrorKind { self.mark_handled(); &self.kind } } -impl From for LaunchError { - #[inline] - fn from(error: io::Error) -> LaunchError { - LaunchError::new(LaunchErrorKind::Io(error)) - } -} - -impl fmt::Display for LaunchErrorKind { +impl fmt::Display for ErrorKind { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - LaunchErrorKind::Bind(ref e) => write!(f, "binding failed: {}", e), - LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), - LaunchErrorKind::Collision(_) => write!(f, "route collisions detected"), - LaunchErrorKind::FailedFairings(_) => write!(f, "a launch fairing failed"), + match self { + ErrorKind::Bind(e) => write!(f, "binding failed: {}", e), + ErrorKind::Io(e) => write!(f, "I/O error: {}", e), + ErrorKind::Collision(_) => write!(f, "route collisions detected"), + ErrorKind::FailedFairings(_) => write!(f, "a launch fairing failed"), + ErrorKind::Runtime(e) => write!(f, "runtime error: {}", e) } } } -impl fmt::Debug for LaunchError { +impl fmt::Debug for Error { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.mark_handled(); @@ -179,7 +150,7 @@ impl fmt::Debug for LaunchError { } } -impl fmt::Display for LaunchError { +impl fmt::Display for Error { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.mark_handled(); @@ -187,22 +158,24 @@ impl fmt::Display for LaunchError { } } -impl Drop for LaunchError { +impl Drop for Error { fn drop(&mut self) { if self.was_handled() { return } match *self.kind() { - LaunchErrorKind::Bind(ref e) => { + ErrorKind::Bind(ref e) => { error!("Rocket failed to bind network socket to given address/port."); - panic!("{}", e); + info_!("{}", e); + panic!("aborting due to binding o error"); } - LaunchErrorKind::Io(ref e) => { + ErrorKind::Io(ref e) => { error!("Rocket failed to launch due to an I/O error."); - panic!("{}", e); + info_!("{}", e); + panic!("aborting due to i/o error"); } - LaunchErrorKind::Collision(ref collisions) => { + ErrorKind::Collision(ref collisions) => { error!("Rocket failed to launch due to the following routing collisions:"); for &(ref a, ref b) in collisions { info_!("{} {} {}", a, Paint::red("collides with").italic(), b) @@ -211,13 +184,18 @@ impl Drop for LaunchError { info_!("Note: Collisions can usually be resolved by ranking routes."); panic!("route collisions detected"); } - LaunchErrorKind::FailedFairings(ref failures) => { + ErrorKind::FailedFairings(ref failures) => { error!("Rocket failed to launch due to failing fairings:"); for fairing in failures { info_!("{}", fairing); } - panic!("launch fairing failure"); + panic!("aborting due to launch fairing failure"); + } + ErrorKind::Runtime(ref err) => { + error!("An error occured in the runtime:"); + info_!("{}", err); + panic!("aborting due to runtime failure"); } } } diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index 7ffbaf2d..1e0aed63 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -92,9 +92,8 @@ impl AdHoc { /// let fairing = AdHoc::on_attach("No-Op", |rocket| async { Ok(rocket) }); /// ``` pub fn on_attach(name: &'static str, f: F) -> AdHoc - where - F: FnOnce(Rocket) -> Fut + Send + 'static, - Fut: Future> + Send + 'static, + where F: FnOnce(Rocket) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, { AdHoc { name, @@ -102,6 +101,43 @@ impl AdHoc { } } + /// Constructs an `AdHoc` attach fairing that extracts a configuration of + /// type `T` from the configured provider and stores it in managed state. If + /// extractions fails, pretty-prints the error message and errors the attach + /// fairing. + /// + /// # Example + /// + /// ```rust + /// use serde::Deserialize; + /// use rocket::fairing::AdHoc; + /// + /// #[derive(Deserialize)] + /// struct Config { + /// field: String, + /// other: usize, + /// /* and so on.. */ + /// } + /// + /// let fairing = AdHoc::config::(); + /// ``` + pub fn config<'de, T>() -> AdHoc + where T: serde::Deserialize<'de> + Send + Sync + 'static + { + AdHoc::on_attach(std::any::type_name::(), |mut rocket| async { + let figment = rocket.figment().await; + let app_config = match figment.extract::() { + Ok(config) => config, + Err(e) => { + crate::config::pretty_print_error(e); + return Err(rocket); + } + }; + + Ok(rocket.manage(app_config)) + }) + } + /// Constructs an `AdHoc` launch fairing named `name`. The function `f` will /// be called by Rocket just prior to launching. /// diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 477bf166..1ea2fa52 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -55,24 +55,27 @@ //! } //! //! #[launch] -//! fn rocket() -> rocket::Rocket { +//! fn rocket() -> _ { //! rocket::ignite().mount("/", routes![hello]) //! } //! ``` //! //! ## Features //! -//! The `secrets` feature, which enables [private cookies], is enabled by -//! default. This necessitates pulling in additional dependencies. To avoid -//! these dependencies when your application does not use private cookies, -//! disable the `secrets` feature: +//! There are two optional, disabled-by-default features: +//! +//! * **secrets:** Enables support for [private cookies]. +//! * **tls:** Enables support for [TLS]. +//! +//! The features can be enabled in `Cargo.toml`: //! //! ```toml //! [dependencies] -//! rocket = { version = "0.5.0-dev", default-features = false } +//! rocket = { version = "0.5.0-dev", features = ["secrets", "tls"] } //! ``` //! -//! [private cookies]: crate::http::CookieJar#private-cookies +//! [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies +//! [TLS]: https://rocket.rs/v0.5/guide/configuration/#tls //! //! ## Configuration //! @@ -98,10 +101,13 @@ pub use async_trait::*; #[macro_use] extern crate log; +/// These are public dependencies! Update docs if these are changed, especially +/// figment's version number in docs. #[doc(hidden)] pub use yansi; pub use futures; pub use tokio; +pub use figment; #[doc(hidden)] #[macro_use] pub mod logger; #[macro_use] pub mod outcome; @@ -132,6 +138,7 @@ mod rocket; mod codegen; mod ext; +#[doc(hidden)] pub use log::{info, warn, error, debug}; #[doc(inline)] pub use crate::response::Response; #[doc(hidden)] pub use crate::codegen::{StaticRouteInfo, StaticCatcherInfo}; #[doc(inline)] pub use crate::data::Data; @@ -148,9 +155,9 @@ pub fn ignite() -> Rocket { } /// Alias to [`Rocket::custom()`]. Creates a new instance of `Rocket` with a -/// custom configuration. -pub fn custom(config: Config) -> Rocket { - Rocket::custom(config) +/// custom configuration provider. +pub fn custom(provider: T) -> Rocket { + Rocket::custom(provider) } // TODO.async: More thoughtful plan for async tests @@ -170,6 +177,7 @@ pub fn async_test(fut: impl std::future::Future + Send) -> R { pub fn async_main(fut: impl std::future::Future + Send) -> R { tokio::runtime::Builder::new() .threaded_scheduler() + .thread_name("rocket-worker-thread") .enable_all() .build() .expect("create tokio runtime") diff --git a/core/lib/src/local/asynchronous/client.rs b/core/lib/src/local/asynchronous/client.rs index 4c2cb4d5..8a42e5df 100644 --- a/core/lib/src/local/asynchronous/client.rs +++ b/core/lib/src/local/asynchronous/client.rs @@ -5,7 +5,7 @@ use parking_lot::RwLock; use crate::local::asynchronous::{LocalRequest, LocalResponse}; use crate::rocket::{Rocket, Cargo}; use crate::http::{private::cookie, Method}; -use crate::error::LaunchError; +use crate::error::Error; /// An `async` client to construct and dispatch local requests. /// @@ -57,7 +57,7 @@ impl Client { pub(crate) async fn _new( mut rocket: Rocket, tracked: bool - ) -> Result { + ) -> Result { rocket.prelaunch_check().await?; let cargo = rocket.into_cargo().await; let cookies = RwLock::new(cookie::CookieJar::new()); diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index cd842a7c..29b19bc5 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -89,7 +89,7 @@ impl<'c> LocalResponse<'c> { async move { let response: Response<'c> = f(request).await; - let mut cookies = CookieJar::new(request.state.config.secret_key()); + let mut cookies = CookieJar::new(&request.state.config.secret_key); for cookie in response.cookies() { cookies.add_original(cookie.into_owned()); } diff --git a/core/lib/src/local/blocking/client.rs b/core/lib/src/local/blocking/client.rs index 8959e37b..1dad1ab1 100644 --- a/core/lib/src/local/blocking/client.rs +++ b/core/lib/src/local/blocking/client.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::cell::RefCell; -use crate::error::LaunchError; +use crate::error::Error; use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}}; use crate::rocket::{Rocket, Cargo}; use crate::http::Method; @@ -31,7 +31,7 @@ pub struct Client { } impl Client { - fn _new(rocket: Rocket, tracked: bool) -> Result { + fn _new(rocket: Rocket, tracked: bool) -> Result { let mut runtime = tokio::runtime::Builder::new() .basic_scheduler() .enable_all() diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index ad07dbe1..a1b8ade5 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -58,7 +58,7 @@ macro_rules! pub_client_impl { /// # Errors /// /// If launching the `Rocket` instance would fail, excepting network errors, - /// the `LaunchError` is returned. + /// the `Error` is returned. /// /// ```rust,no_run #[doc = $import] @@ -67,7 +67,7 @@ macro_rules! pub_client_impl { /// let client = Client::tracked(rocket); /// ``` #[inline(always)] - pub $($prefix)? fn tracked(rocket: Rocket) -> Result { + pub $($prefix)? fn tracked(rocket: Rocket) -> Result { Self::_new(rocket, true) $(.$suffix)? } @@ -83,7 +83,7 @@ macro_rules! pub_client_impl { /// # Errors /// /// If launching the `Rocket` instance would fail, excepting network - /// errors, the `LaunchError` is returned. + /// errors, the `Error` is returned. /// /// ```rust,no_run #[doc = $import] @@ -91,7 +91,7 @@ macro_rules! pub_client_impl { /// let rocket = rocket::ignite(); /// let client = Client::untracked(rocket); /// ``` - pub $($prefix)? fn untracked(rocket: Rocket) -> Result { + pub $($prefix)? fn untracked(rocket: Rocket) -> Result { Self::_new(rocket, false) $(.$suffix)? } @@ -100,7 +100,7 @@ macro_rules! pub_client_impl { since = "0.5", note = "choose between `Client::untracked()` and `Client::tracked()`" )] - pub $($prefix)? fn new(rocket: Rocket) -> Result { + pub $($prefix)? fn new(rocket: Rocket) -> Result { Self::tracked(rocket) $(.$suffix)? } @@ -159,7 +159,7 @@ macro_rules! pub_client_impl { /// ``` #[inline(always)] pub fn cookies(&self) -> crate::http::CookieJar<'_> { - let key = self.rocket().config.secret_key(); + let key = &self.rocket().config.secret_key; let jar = self._with_raw_cookies(|jar| jar.clone()); crate::http::CookieJar::from(jar, key) } diff --git a/core/lib/src/logger.rs b/core/lib/src/logger.rs index edace752..3e825d57 100644 --- a/core/lib/src/logger.rs +++ b/core/lib/src/logger.rs @@ -1,49 +1,58 @@ //! Rocket's logging infrastructure. -use std::{fmt, env}; +use std::fmt; use std::str::FromStr; use log; use yansi::Paint; +use serde::{de, Serialize, Serializer, Deserialize, Deserializer}; -pub(crate) const COLORS_ENV: &str = "ROCKET_CLI_COLORS"; +#[derive(Debug)] +struct RocketLogger(LogLevel); -struct RocketLogger(LoggingLevel); - -/// Defines the different levels for log messages. +/// Defines the maximum level of log messages to show. #[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub enum LoggingLevel { - /// Only shows errors, warnings, and launch information. +pub enum LogLevel { + /// Only shows errors and warnings: `"critical"`. Critical, - /// Shows everything except debug and trace information. + /// Shows everything except debug and trace information: `"normal"`. Normal, - /// Shows everything. + /// Shows everything: `"debug"`. Debug, - /// Shows nothing. + /// Shows nothing: "`"off"`". Off, } -impl LoggingLevel { +impl LogLevel { + fn as_str(&self) -> &str { + match self { + LogLevel::Critical => "critical", + LogLevel::Normal => "normal", + LogLevel::Debug => "debug", + LogLevel::Off => "off", + } + } + #[inline(always)] fn to_level_filter(self) -> log::LevelFilter { match self { - LoggingLevel::Critical => log::LevelFilter::Warn, - LoggingLevel::Normal => log::LevelFilter::Info, - LoggingLevel::Debug => log::LevelFilter::Trace, - LoggingLevel::Off => log::LevelFilter::Off + LogLevel::Critical => log::LevelFilter::Warn, + LogLevel::Normal => log::LevelFilter::Info, + LogLevel::Debug => log::LevelFilter::Trace, + LogLevel::Off => log::LevelFilter::Off } } } -impl FromStr for LoggingLevel { +impl FromStr for LogLevel { type Err = &'static str; fn from_str(s: &str) -> Result { - let level = match s { - "critical" => LoggingLevel::Critical, - "normal" => LoggingLevel::Normal, - "debug" => LoggingLevel::Debug, - "off" => LoggingLevel::Off, + let level = match &*s.to_ascii_lowercase() { + "critical" => LogLevel::Critical, + "normal" => LogLevel::Normal, + "debug" => LogLevel::Debug, + "off" => LogLevel::Off, _ => return Err("a log level (off, debug, normal, critical)") }; @@ -51,16 +60,25 @@ impl FromStr for LoggingLevel { } } -impl fmt::Display for LoggingLevel { +impl fmt::Display for LogLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let string = match *self { - LoggingLevel::Critical => "critical", - LoggingLevel::Normal => "normal", - LoggingLevel::Debug => "debug", - LoggingLevel::Off => "off" - }; + write!(f, "{}", self.as_str()) + } +} - write!(f, "{}", string) +impl Serialize for LogLevel { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for LogLevel { + fn deserialize>(de: D) -> Result { + let string = String::deserialize(de)?; + LogLevel::from_str(&string).map_err(|_| de::Error::invalid_value( + de::Unexpected::Str(&string), + &figment::error::OneOf( &["critical", "normal", "debug", "off"]) + )) } } @@ -100,14 +118,14 @@ impl log::Log for RocketLogger { let configged_level = self.0; let from_hyper = record.module_path().map_or(false, |m| m.starts_with("hyper::")); let from_rustls = record.module_path().map_or(false, |m| m.starts_with("rustls::")); - if configged_level != LoggingLevel::Debug && (from_hyper || from_rustls) { + if configged_level != LogLevel::Debug && (from_hyper || from_rustls) { return; } // In Rocket, we abuse targets with suffix "_" to indicate indentation. let is_launch = record.target().starts_with("launch"); if record.target().ends_with('_') { - if configged_level != LoggingLevel::Critical || is_launch { + if configged_level != LogLevel::Critical || is_launch { print!(" {} ", Paint::default("=>").bold()); } } @@ -145,89 +163,45 @@ impl log::Log for RocketLogger { } } -pub(crate) fn try_init(level: LoggingLevel, verbose: bool) -> bool { - if level == LoggingLevel::Off { +pub(crate) fn try_init(level: LogLevel, colors: bool, verbose: bool) -> bool { + if level == LogLevel::Off { return false; } if !atty::is(atty::Stream::Stdout) || (cfg!(windows) && !Paint::enable_windows_ascii()) - || env::var_os(COLORS_ENV).map(|v| v == "0" || v == "off").unwrap_or(false) + || !colors { Paint::disable(); } - push_max_level(level); if let Err(e) = log::set_boxed_logger(Box::new(RocketLogger(level))) { if verbose { eprintln!("Logger failed to initialize: {}", e); } - pop_max_level(); return false; } + log::set_max_level(level.to_level_filter()); true } -use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering}; - -static PUSHED: AtomicBool = AtomicBool::new(false); -static LAST_LOG_FILTER: AtomicUsize = AtomicUsize::new(0); - -fn filter_to_usize(filter: log::LevelFilter) -> usize { - match filter { - log::LevelFilter::Off => 0, - log::LevelFilter::Error => 1, - log::LevelFilter::Warn => 2, - log::LevelFilter::Info => 3, - log::LevelFilter::Debug => 4, - log::LevelFilter::Trace => 5, - } -} - -fn usize_to_filter(num: usize) -> log::LevelFilter { - match num { - 0 => log::LevelFilter::Off, - 1 => log::LevelFilter::Error, - 2 => log::LevelFilter::Warn, - 3 => log::LevelFilter::Info, - 4 => log::LevelFilter::Debug, - 5 => log::LevelFilter::Trace, - _ => unreachable!("max num is 5 in filter_to_usize") - } -} - -pub(crate) fn push_max_level(level: LoggingLevel) { - LAST_LOG_FILTER.store(filter_to_usize(log::max_level()), Ordering::Release); - PUSHED.store(true, Ordering::Release); - log::set_max_level(level.to_level_filter()); -} - -pub(crate) fn pop_max_level() { - if PUSHED.load(Ordering::Acquire) { - log::set_max_level(usize_to_filter(LAST_LOG_FILTER.load(Ordering::Acquire))); - } -} - -pub(crate) trait PaintExt { +pub trait PaintExt { fn emoji(item: &str) -> Paint<&str>; } impl PaintExt for Paint<&str> { /// Paint::masked(), but hidden on Windows due to broken output. See #1122. - fn emoji(item: &str) -> Paint<&str> { - if cfg!(windows) { - Paint::masked("") - } else { - Paint::masked(item) - } + fn emoji(_item: &str) -> Paint<&str> { + #[cfg(windows)] { Paint::masked("") } + #[cfg(not(windows))] { Paint::masked(_item) } } } #[doc(hidden)] -pub fn init(level: LoggingLevel) -> bool { - try_init(level, true) +pub fn init(level: LogLevel) -> bool { + try_init(level, true, true) } // Expose logging macros as (hidden) funcions for use by core/contrib codegen. diff --git a/core/lib/src/request/form/form.rs b/core/lib/src/request/form/form.rs index 2f5cf2a9..e6ecff05 100644 --- a/core/lib/src/request/form/form.rs +++ b/core/lib/src/request/form/form.rs @@ -2,13 +2,14 @@ use std::ops::Deref; use crate::outcome::Outcome::*; use crate::request::{Request, form::{FromForm, FormItems, FormDataError}}; -use crate::data::{Outcome, Transform, Transformed, Data, FromTransformedData, TransformFuture, FromDataFuture}; +use crate::data::{Data, Outcome, Transform, Transformed, ToByteUnit}; +use crate::data::{TransformFuture, FromTransformedData, FromDataFuture}; use crate::http::{Status, uri::{Query, FromUriParam}}; /// A data guard for parsing [`FromForm`] types strictly. /// -/// This type implements the [`FromTransformedData`] trait. It provides a generic means to -/// parse arbitrary structures from incoming form data. +/// This type implements the [`FromTransformedData`] trait. It provides a +/// generic means to parse arbitrary structures from incoming form data. /// /// # Strictness /// @@ -197,7 +198,8 @@ impl<'f, T: FromForm<'f> + Send + 'f> FromTransformedData<'f> for Form { return Transform::Borrowed(Forward(data)); } - match data.open(request.limits().forms).stream_to_string().await { + let limit = request.limits().get("forms").unwrap_or(32.kibibytes()); + match data.open(limit).stream_to_string().await { Ok(form_string) => Transform::Borrowed(Success(form_string)), Err(e) => { let err = (Status::InternalServerError, FormDataError::Io(e)); @@ -207,10 +209,13 @@ impl<'f, T: FromForm<'f> + Send + 'f> FromTransformedData<'f> for Form { }) } - fn from_data(_: &'f Request<'_>, o: Transformed<'f, Self>) -> FromDataFuture<'f, Self, Self::Error> { - Box::pin(futures::future::ready(o.borrowed().and_then(|data| { - >::from_data(data, true).map(Form) - }))) + fn from_data( + _: &'f Request<'_>, + o: Transformed<'f, Self> + ) -> FromDataFuture<'f, Self, Self::Error> { + Box::pin(async move { + o.borrowed().and_then(|data| >::from_data(data, true).map(Form)) + }) } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index fc3c4973..6b1e4da1 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -95,7 +95,7 @@ impl<'r> Request<'r> { managed: &rocket.managed_state, shutdown: &rocket.shutdown_handle, route: Atomic::new(None), - cookies: CookieJar::new(rocket.config.secret_key()), + cookies: CookieJar::new(&rocket.config.secret_key), accept: Storage::new(), content_type: Storage::new(), cache: Arc::new(Container::new()), @@ -749,7 +749,7 @@ impl<'r> Request<'r> { impl<'r> Request<'r> { // Only used by doc-tests! Needs to be `pub` because doc-test are external. pub fn example)>(method: Method, uri: &str, f: F) { - let rocket = Rocket::custom(Config::development()); + let rocket = Rocket::custom(Config::default()); let uri = Origin::parse(uri).expect("invalid URI in example"); let mut request = Request::new(&rocket, method, uri); f(&mut request); diff --git a/core/lib/src/request/tests.rs b/core/lib/src/request/tests.rs index 9e50feaa..4fffbae2 100644 --- a/core/lib/src/request/tests.rs +++ b/core/lib/src/request/tests.rs @@ -20,8 +20,7 @@ macro_rules! assert_headers { $(expected.entry($key).or_insert(vec![]).append(&mut vec![$($value),+]);)+ // Dispatch the request and check that the headers are what we expect. - let config = Config::development(); - let r = Rocket::custom(config); + let r = Rocket::custom(Config::default()); let req = Request::from_hyp(&r, h_method, h_headers, &h_uri, h_addr).unwrap(); let actual_headers = req.headers(); for (key, values) in expected.iter() { diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 810fefc0..5b2bc461 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -11,16 +11,17 @@ use ref_cast::RefCast; use yansi::Paint; use state::Container; +use figment::Figment; use crate::{logger, handler}; -use crate::config::{Config, FullConfig, ConfigError, LoggedValue}; +use crate::config::Config; use crate::request::{Request, FormItems}; use crate::data::Data; use crate::catcher::Catcher; use crate::response::{Body, Response}; use crate::router::{Router, Route}; use crate::outcome::Outcome; -use crate::error::{LaunchError, LaunchErrorKind}; +use crate::error::{Error, ErrorKind}; use crate::fairing::{Fairing, Fairings}; use crate::logger::PaintExt; use crate::ext::AsyncReadExt; @@ -35,6 +36,7 @@ use crate::http::uri::Origin; /// application. pub struct Rocket { pub(crate) config: Config, + pub(crate) figment: Figment, pub(crate) managed_state: Container, manifest: Vec, router: Router, @@ -66,6 +68,9 @@ pub(crate) struct Token; impl Rocket { #[inline] fn _mount(&mut self, base: Origin<'static>, routes: Vec) { + // info!("[$]🛰 [/m]mounting [b]{}[/]:", log::emoji(), base); + // info!("[$]🛰 [/m]mounting [b]{}[/]:", log::emoji(), base); + // info!("{}[m]Mounting [b]{}[/]:[/]", log::emoji("🛰 "), base); info!("{}{} {}{}", Paint::emoji("🛰 "), Paint::magenta("Mounting"), @@ -107,6 +112,7 @@ impl Rocket { #[inline] async fn _attach(mut self, fairing: Box) -> Self { // Attach (and run attach-) fairings, which requires us to move `self`. + trace_!("Running attach fairing: {:?}", fairing.info()); let mut fairings = mem::replace(&mut self.fairings, Fairings::new()); self = fairings.attach(fairing, self).await; @@ -119,8 +125,9 @@ impl Rocket { // Create a "dummy" instance of `Rocket` to use while mem-swapping `self`. fn dummy() -> Rocket { Rocket { + config: Config::debug_default(), + figment: Figment::from(Config::debug_default()), manifest: vec![], - config: Config::development(), router: Router::new(), default_catcher: None, catchers: HashMap::new(), @@ -145,7 +152,7 @@ impl Rocket { // process them as a stack to maintain proper ordering. let mut manifest = mem::replace(&mut self.manifest, vec![]); while !manifest.is_empty() { - trace_!("[MANIEST PROGRESS]: {:?}", manifest); + trace_!("[MANIEST PROGRESS ({} left)]: {:?}", manifest.len(), manifest); match manifest.remove(0) { PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state), PreLaunchOp::Mount(base, routes) => self._mount(base, routes), @@ -183,10 +190,9 @@ async fn hyper_service_fn( h_addr: std::net::SocketAddr, hyp_req: hyper::Request, ) -> Result, io::Error> { - // This future must return a hyper::Response, but that's not easy - // because the response body might borrow from the request. Instead, - // we do the body writing in another future that will send us - // the response metadata (and a body channel) beforehand. + // This future must return a hyper::Response, but the response body might + // borrow from the request. Instead, write the body in another future that + // sends the response metadata (and a body channel) prior. let (tx, rx) = oneshot::channel(); tokio::spawn(async move { @@ -194,7 +200,10 @@ async fn hyper_service_fn( let (h_parts, h_body) = hyp_req.into_parts(); // Convert the Hyper request into a Rocket request. - let req_res = Request::from_hyp(&rocket, h_parts.method, h_parts.headers, &h_parts.uri, h_addr); + let req_res = Request::from_hyp( + &rocket, h_parts.method, h_parts.headers, &h_parts.uri, h_addr + ); + let mut req = match req_res { Ok(req) => req, Err(e) => { @@ -218,6 +227,7 @@ async fn hyper_service_fn( rocket.issue_response(r, tx).await; }); + // Receive the response written to `tx` by the task above. rx.await.map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } @@ -228,14 +238,9 @@ impl Rocket { response: Response<'_>, tx: oneshot::Sender>, ) { - let result = self.write_response(response, tx); - match result.await { - Ok(()) => { - info_!("{}", Paint::green("Response succeeded.")); - } - Err(e) => { - error_!("Failed to write response: {:?}.", e); - } + match self.write_response(response, tx).await { + Ok(()) => info_!("{}", Paint::green("Response succeeded.")), + Err(e) => error_!("Failed to write response: {:?}.", e), } } @@ -254,12 +259,12 @@ impl Rocket { hyp_res = hyp_res.header(name, value); } - let send_response = move |hyp_res: hyper::ResponseBuilder, body| -> io::Result<()> { - let response = hyp_res.body(body) + let send_response = move |res: hyper::ResponseBuilder, body| -> io::Result<()> { + let response = res.body(body) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; tx.send(response).map_err(|_| { - let msg = "Client disconnected before the response was started"; + let msg = "client disconnected before the response was started"; io::Error::new(io::ErrorKind::BrokenPipe, msg) }) }; @@ -488,14 +493,13 @@ impl Rocket { } // TODO.async: Solidify the Listener APIs and make this function public - async fn listen_on(mut self, listener: L) -> Result<(), crate::error::Error> + async fn listen_on(mut self, listener: L) -> Result<(), Error> where L: Listener + Send + Unpin + 'static, ::Connection: Send + Unpin + 'static, { - // Determine the address and port we actually binded to. - self.config.port = listener.local_addr().map(|a| a.port()).unwrap_or(0); - let proto = self.config.tls.as_ref().map_or("http://", |_| "https://"); - let full_addr = format!("{}:{}", self.config.address, self.config.port); + // We do this twice if `listen_on` was called through `launch()` but + // only once if `listen_on()` gets called directly. + self.prelaunch_check().await?; // Freeze managed state for synchronization-free accesses later. self.managed_state.freeze(); @@ -504,31 +508,35 @@ impl Rocket { self.fairings.pretty_print_counts(); self.fairings.handle_launch(self.cargo()); + // Determine the address and port we actually bound to. + self.config.port = listener.local_addr().map(|a| a.port()).unwrap_or(0); + let proto = self.config.tls.as_ref().map_or("http://", |_| "https://"); + let full_addr = format!("{}:{}", self.config.address, self.config.port); + launch_info!("{}{} {}{}", Paint::emoji("🚀 "), Paint::default("Rocket has launched from").bold(), Paint::default(proto).bold().underline(), Paint::default(&full_addr).bold().underline()); - // Restore the log level back to what it originally was. - logger::pop_max_level(); - - // Set the keep-alive. - // TODO.async: implement keep-alive in Listener - // let timeout = self.config.keep_alive.map(|s| Duration::from_secs(s as u64)); - // listener.set_keepalive(timeout); + // Determine keep-alives. + let http1_keepalive = self.config.keep_alive != 0; + let http2_keep_alive = match self.config.keep_alive { + 0 => None, + n => Some(std::time::Duration::from_secs(n as u64)) + }; // We need to get this before moving `self` into an `Arc`. - let mut shutdown_receiver = self.shutdown_receiver - .take().expect("shutdown receiver has already been used"); + let mut shutdown_receiver = self.shutdown_receiver.take() + .expect("shutdown receiver has already been used"); let rocket = Arc::new(self); - let service = hyper::make_service_fn(move |connection: &::Connection| { + let service = hyper::make_service_fn(move |conn: &::Connection| { let rocket = rocket.clone(); - let remote_addr = connection.remote_addr().unwrap_or_else(|| ([0, 0, 0, 0], 0).into()); + let remote = conn.remote_addr().unwrap_or_else(|| ([0, 0, 0, 0], 0).into()); async move { Ok::<_, std::convert::Infallible>(hyper::service_fn(move |req| { - hyper_service_fn(rocket.clone(), remote_addr, req) + hyper_service_fn(rocket.clone(), remote, req) })) } }); @@ -545,11 +553,13 @@ impl Rocket { } hyper::Server::builder(Incoming::from_listener(listener)) + .http1_keepalive(http1_keepalive) + .http2_keep_alive_interval(http2_keep_alive) .executor(TokioExecutor) .serve(service) .with_graceful_shutdown(async move { shutdown_receiver.recv().await; }) .await - .map_err(|e| crate::error::Error::Run(Box::new(e))) + .map_err(|e| Error::new(ErrorKind::Runtime(Box::new(e)))) } } @@ -576,97 +586,43 @@ impl Rocket { /// # }; /// ``` pub fn ignite() -> Rocket { - Config::read() - .or_else(|e| match e { - ConfigError::IoError => { - warn!("Failed to read 'Rocket.toml'. Using defaults."); - Ok(FullConfig::env_default()?.take_active()) - } - ConfigError::NotFound => Ok(FullConfig::env_default()?.take_active()), - _ => Err(e) - }) - .map(Rocket::configured) - .unwrap_or_else(|e: ConfigError| { - logger::init(logger::LoggingLevel::Debug); - e.pretty_print(); - std::process::exit(1) - }) + Rocket::custom(Config::figment()) } - /// Creates a new `Rocket` application using the supplied custom - /// configuration. The `Rocket.toml` file, if present, is ignored. Any - /// environment variables setting config parameters are ignored. + /// Creates a new `Rocket` application using the supplied configuration + /// provider. This method is typically called through the + /// [`rocket::custom()`] alias. /// - /// This method is typically called through the `rocket::custom` alias. + /// # Panics + /// + /// If there is an error reading configuration sources, this function prints + /// a nice error message and then exits the process. /// /// # Examples /// - /// ```rust - /// use rocket::config::{Config, Environment}; - /// # use rocket::config::ConfigError; + /// ```rust,no_run + /// use figment::{Figment, providers::{Toml, Env, Format}}; /// - /// # #[allow(dead_code)] - /// # fn try_config() -> Result<(), ConfigError> { - /// let config = Config::build(Environment::Staging) - /// .address("1.2.3.4") - /// .port(9234) - /// .finalize()?; + /// #[rocket::launch] + /// fn rocket() -> _ { + /// let figment = Figment::from(rocket::Config::default()) + /// .merge(Toml::file("MyApp.toml").nested()) + /// .merge(Env::prefixed("MY_APP_")); /// - /// # #[allow(unused_variables)] - /// let app = rocket::custom(config); - /// # Ok(()) - /// # } + /// rocket::custom(figment) + /// } /// ``` #[inline] - pub fn custom(config: Config) -> Rocket { - Rocket::configured(config) - } - - #[inline] - fn configured(config: Config) -> Rocket { - if logger::try_init(config.log_level, false) { - // Temporary weaken log level for launch info. - logger::push_max_level(logger::LoggingLevel::Normal); - } - - launch_info!("{}Configured for {}.", Paint::emoji("🔧 "), config.environment); - launch_info_!("address: {}", Paint::default(&config.address).bold()); - launch_info_!("port: {}", Paint::default(&config.port).bold()); - launch_info_!("log: {}", Paint::default(config.log_level).bold()); - launch_info_!("workers: {}", Paint::default(config.workers).bold()); - launch_info_!("secret key: {}", Paint::default(&config.secret_key).bold()); - launch_info_!("limits: {}", Paint::default(&config.limits).bold()); - - match config.keep_alive { - Some(v) => launch_info_!("keep-alive: {}", Paint::default(format!("{}s", v)).bold()), - None => launch_info_!("keep-alive: {}", Paint::default("disabled").bold()), - } - - let tls_configured = config.tls.is_some(); - if tls_configured && cfg!(feature = "tls") { - launch_info_!("tls: {}", Paint::default("enabled").bold()); - } else if tls_configured { - error_!("tls: {}", Paint::default("disabled").bold()); - error_!("tls is configured, but the tls feature is disabled"); - } else { - launch_info_!("tls: {}", Paint::default("disabled").bold()); - } - - if config.secret_key.is_generated() && config.environment.is_prod() { - warn!("environment is 'production' but no `secret_key` is configured"); - } - - for (name, value) in config.extras() { - launch_info_!("{} {}: {}", - Paint::yellow("[extra]"), name, - Paint::default(LoggedValue(value)).bold()); - } + pub fn custom(provider: T) -> Rocket { + let (config, figment) = (Config::from(&provider), Figment::from(provider)); + logger::try_init(config.log_level, config.cli_colors, false); + config.pretty_print(figment.profile()); let managed_state = Container::new(); let (shutdown_sender, shutdown_receiver) = mpsc::channel(1); - Rocket { - config, managed_state, + config, figment, + managed_state, shutdown_handle: Shutdown(shutdown_sender), manifest: vec![], router: Router::new(), @@ -887,7 +843,27 @@ impl Rocket { self.inspect().await.state() } - /// Returns the active configuration. + /// Returns the figment. + /// + /// This function is equivalent to `.inspect().await.figment()` and is + /// provided as a convenience. + /// + /// # Example + /// + /// ```rust + /// use rocket::Rocket; + /// use rocket::fairing::AdHoc; + /// + /// # rocket::async_test(async { + /// let mut rocket = rocket::ignite(); + /// println!("Rocket config: {:?}", rocket.config().await); + /// # }); + /// ``` + pub async fn figment(&mut self) -> &Figment { + self.inspect().await.figment() + } + + /// Returns the config. /// /// This function is equivalent to `.inspect().await.config()` and is /// provided as a convenience. @@ -938,14 +914,14 @@ impl Rocket { /// Perform "pre-launch" checks: verify that there are no routing colisions /// and that there were no fairing failures. - pub(crate) async fn prelaunch_check(&mut self) -> Result<(), LaunchError> { + pub(crate) async fn prelaunch_check(&mut self) -> Result<(), Error> { self.actualize_manifest().await; if let Err(e) = self.router.collisions() { - return Err(LaunchError::new(LaunchErrorKind::Collision(e))); + return Err(Error::new(ErrorKind::Collision(e))); } if let Some(failures) = self.fairings.failures() { - return Err(LaunchError::new(LaunchErrorKind::FailedFairings(failures.to_vec()))) + return Err(Error::new(ErrorKind::FailedFairings(failures.to_vec()))) } Ok(()) @@ -963,8 +939,6 @@ impl Rocket { /// first being inspected. See the [`Error`] documentation for more /// information. /// - /// [`Error`]: crate::error::Error - /// /// # Example /// /// ```rust @@ -976,51 +950,46 @@ impl Rocket { /// # } /// } /// ``` - pub async fn launch(mut self) -> Result<(), crate::error::Error> { + pub async fn launch(mut self) -> Result<(), Error> { use std::net::ToSocketAddrs; use futures::future::Either; - use crate::error::Error::Launch; + use crate::http::private::bind_tcp; - self.prelaunch_check().await.map_err(crate::error::Error::Launch)?; + self.prelaunch_check().await?; let full_addr = format!("{}:{}", self.config.address, self.config.port); - let addr = match full_addr.to_socket_addrs() { - Ok(mut addrs) => addrs.next().expect(">= 1 socket addr"), - Err(e) => return Err(Launch(e.into())), - }; + let addr = full_addr.to_socket_addrs() + .map(|mut addrs| addrs.next().expect(">= 1 socket addr")) + .map_err(|e| Error::new(ErrorKind::Io(e)))?; - // FIXME: Make `ctrlc` a known `Rocket` config option. // If `ctrl-c` shutdown is enabled, we `select` on `the ctrl-c` signal // and server. Otherwise, we only wait on the `server`, hence `pending`. let shutdown_handle = self.shutdown_handle.clone(); - let shutdown_signal = match self.config.get_bool("ctrlc") { - Ok(false) => futures::future::pending().boxed(), - _ => tokio::signal::ctrl_c().boxed(), + let shutdown_signal = match self.config.ctrlc { + true => tokio::signal::ctrl_c().boxed(), + false => futures::future::pending().boxed(), }; + #[cfg(feature = "tls")] let server = { - macro_rules! listen_on { - ($expr:expr) => {{ - let listener = match $expr { - Ok(ok) => ok, - Err(err) => return Err(Launch(LaunchError::new(LaunchErrorKind::Bind(err)))) - }; - self.listen_on(listener) - }}; - } + use crate::http::tls::bind_tls; - #[cfg(feature = "tls")] { - if let Some(tls) = self.config.tls.clone() { - listen_on!(crate::http::tls::bind_tls(addr, tls.certs, tls.key).await).boxed() - } else { - listen_on!(crate::http::private::bind_tcp(addr).await).boxed() - } - } - #[cfg(not(feature = "tls"))] { - listen_on!(crate::http::private::bind_tcp(addr).await).boxed() + if let Some(tls_config) = &self.config.tls { + let (certs, key) = tls_config.to_readers().map_err(ErrorKind::Io)?; + let l = bind_tls(addr, certs, key).await.map_err(ErrorKind::Bind)?; + self.listen_on(l).boxed() + } else { + let l = bind_tcp(addr).await.map_err(ErrorKind::Bind)?; + self.listen_on(l).boxed() } }; + #[cfg(not(feature = "tls"))] + let server = { + let l = bind_tcp(addr).await.map_err(ErrorKind::Bind)?; + self.listen_on(l).boxed() + }; + match futures::future::select(shutdown_signal, server).await { Either::Left((Ok(()), server)) => { // Ctrl-was pressed. Signal shutdown, wait for the server. @@ -1029,7 +998,7 @@ impl Rocket { } Either::Left((Err(err), server)) => { // Error setting up ctrl-c signal. Let the user know. - warn!("Failed to enable `ctrl+c` graceful signal shutdown."); + warn!("Failed to enable `ctrl-c` graceful signal shutdown."); info_!("Error: {}", err); server.await } @@ -1128,6 +1097,21 @@ impl Cargo { self.0.managed_state.try_get() } + /// Returns the figment for configured provider. + /// + /// # Example + /// + /// ```rust,no_run + /// # rocket::async_test(async { + /// let mut rocket = rocket::ignite(); + /// let figment = rocket.inspect().await.figment(); + /// # }); + /// ``` + #[inline(always)] + pub fn figment(&self) -> &Figment { + &self.0.figment + } + /// Returns the active configuration. /// /// # Example diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 1b64c96f..df343a85 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -401,7 +401,7 @@ mod tests { fn req_route_mt_collide(m: Method, mt1: S1, mt2: S2) -> bool where S1: Into>, S2: Into> { - let rocket = Rocket::custom(Config::development()); + let rocket = Rocket::custom(Config::default()); let mut req = Request::new(&rocket, m, Origin::dummy()); if let Some(mt_str) = mt1.into() { if m.supports_payload() { @@ -468,7 +468,7 @@ mod tests { } fn req_route_path_match(a: &'static str, b: &'static str) -> bool { - let rocket = Rocket::custom(Config::development()); + let rocket = Rocket::custom(Config::default()); let req = Request::new(&rocket, Get, Origin::parse(a).expect("valid URI")); let route = Route::ranked(0, Get, b.to_string(), dummy); route.matches(&req) diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs index 0849cdf2..cb42aeac 100644 --- a/core/lib/src/router/mod.rs +++ b/core/lib/src/router/mod.rs @@ -224,7 +224,7 @@ mod test { } fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> { - let rocket = Rocket::custom(Config::development()); + let rocket = Rocket::custom(Config::default()); let request = Request::new(&rocket, method, Origin::parse(uri).unwrap()); let matches = router.route(&request); if matches.len() > 0 { @@ -235,7 +235,7 @@ mod test { } fn matches<'a>(router: &'a Router, method: Method, uri: &str) -> Vec<&'a Route> { - let rocket = Rocket::custom(Config::development()); + let rocket = Rocket::custom(Config::default()); let request = Request::new(&rocket, method, Origin::parse(uri).unwrap()); router.route(&request) } diff --git a/core/lib/tests/limits.rs b/core/lib/tests/limits.rs index cf48f862..253e8545 100644 --- a/core/lib/tests/limits.rs +++ b/core/lib/tests/limits.rs @@ -14,16 +14,13 @@ fn index(form: Form) -> String { mod limits_tests { use rocket; - use rocket::config::{Environment, Config}; use rocket::local::blocking::Client; use rocket::http::{Status, ContentType}; use rocket::data::Limits; fn rocket_with_forms_limit(limit: u64) -> rocket::Rocket { - let config = Config::build(Environment::Development) - .limits(Limits::default().limit("forms", limit.into())) - .unwrap(); - + let limits = Limits::default().limit("forms", limit.into()); + let config = rocket::Config::figment().merge(("limits", limits)); rocket::custom(config).mount("/", routes![super::index]) } diff --git a/examples/config/Rocket.toml b/examples/config/Rocket.toml index e1e2b673..ba1f675a 100644 --- a/examples/config/Rocket.toml +++ b/examples/config/Rocket.toml @@ -2,33 +2,24 @@ # 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 +forms = "64 kB" +json = "1 MiB" +msgpack = "2 MiB" -[development] -address = "localhost" +[debug] +address = "127.0.0.1" port = 8000 workers = 1 -keep_alive = 5 -log = "normal" +keep_alive = 0 +log_level = "normal" 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" -port = 8000 -workers = 8 -keep_alive = 5 -log = "normal" -# don't use this key! generate your own and keep it private! -secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=" - -[production] -address = "0.0.0.0" +[release] +address = "127.0.0.1" port = 8000 workers = 12 keep_alive = 5 -log = "critical" +log_level = "critical" # don't use this key! generate your own and keep it private! secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index 4ae267cc..f10e5d67 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -1,3 +1,5 @@ +#[cfg(test)] mod tests; + // This example's illustration is the Rocket.toml file. #[rocket::launch] fn rocket() -> rocket::Rocket { rocket::ignite() } diff --git a/examples/config/src/tests.rs b/examples/config/src/tests.rs new file mode 100644 index 00000000..bb82a376 --- /dev/null +++ b/examples/config/src/tests.rs @@ -0,0 +1,36 @@ +use rocket::config::{Config, LogLevel}; + +async fn test_config(profile: &str) { + let mut rocket = rocket::custom(Config::figment().select(profile)); + let config = rocket.config().await; + match &*profile { + "debug" => { + assert_eq!(config.address, std::net::Ipv4Addr::LOCALHOST); + assert_eq!(config.port, 8000); + assert_eq!(config.workers, 1); + assert_eq!(config.keep_alive, 0); + assert_eq!(config.log_level, LogLevel::Normal); + } + "release" => { + assert_eq!(config.address, std::net::Ipv4Addr::LOCALHOST); + assert_eq!(config.port, 8000); + assert_eq!(config.workers, 12); + assert_eq!(config.keep_alive, 5); + assert_eq!(config.log_level, LogLevel::Critical); + assert!(!config.secret_key.is_zero()); + } + _ => { + panic!("Unknown profile: {}", profile); + } + } +} + +#[rocket::async_test] +async fn test_debug_config() { + test_config("debug").await +} + +#[rocket::async_test] +async fn test_release_config() { + test_config("release").await +} diff --git a/examples/config/tests/development.rs b/examples/config/tests/development.rs deleted file mode 100644 index 6b809328..00000000 --- a/examples/config/tests/development.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] extern crate rocket; - -mod common; - -#[test] -fn test_development_config() { - common::test_config(rocket::config::Environment::Development); -} diff --git a/examples/config/tests/production.rs b/examples/config/tests/production.rs deleted file mode 100644 index 2210e3ae..00000000 --- a/examples/config/tests/production.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] extern crate rocket; - -mod common; - -#[test] -fn test_production_config() { - common::test_config(rocket::config::Environment::Production); -} diff --git a/examples/config/tests/staging.rs b/examples/config/tests/staging.rs deleted file mode 100644 index 8250baa9..00000000 --- a/examples/config/tests/staging.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] extern crate rocket; - -mod common; - -#[test] -fn test_staging_config() { - common::test_config(rocket::config::Environment::Staging); -} diff --git a/examples/content_types/Cargo.toml b/examples/content_types/Cargo.toml index c9415ec7..7eb54855 100644 --- a/examples/content_types/Cargo.toml +++ b/examples/content_types/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" publish = false [dependencies] -tokio = { version = "0.2.0", features = ["io-util"] } rocket = { path = "../../core/lib" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/fairings/src/main.rs b/examples/fairings/src/main.rs index fe491347..10706198 100644 --- a/examples/fairings/src/main.rs +++ b/examples/fairings/src/main.rs @@ -68,8 +68,10 @@ fn rocket() -> rocket::Rocket { .attach(Counter::default()) .attach(AdHoc::on_attach("Token State", |mut rocket| async { println!("Adding token managed state..."); - let token_val = rocket.config().await.get_int("token").unwrap_or(-1); - Ok(rocket.manage(Token(token_val))) + match rocket.figment().await.extract_inner("token") { + Ok(value) => Ok(rocket.manage(Token(value))), + Err(_) => Err(rocket) + } })) .attach(AdHoc::on_launch("Launch Message", |_| { println!("Rocket is about to launch!"); diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml index b989387e..5093ce36 100644 --- a/examples/session/Cargo.toml +++ b/examples/session/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -rocket = { path = "../../core/lib" } +rocket = { path = "../../core/lib", features = ["secrets"] } [dependencies.rocket_contrib] path = "../../contrib/lib" diff --git a/examples/tera_templates/Rocket.toml b/examples/tera_templates/Rocket.toml new file mode 100644 index 00000000..fcb67324 --- /dev/null +++ b/examples/tera_templates/Rocket.toml @@ -0,0 +1,2 @@ +[global] +template_dir = "templates/" diff --git a/scripts/test.sh b/scripts/test.sh index 45bcb60f..d5849bb2 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -10,6 +10,9 @@ export PATH=${HOME}/.cargo/bin:${PATH} export CARGO_INCREMENTAL=0 CARGO="cargo" +# We set a `cfg` so that a missing `secret_key` doesn't abort tests. +export RUSTFLAGS="--cfg rocket_unsafe_secret_key" + # Checks that the versions for Cargo projects $@ all match function check_versions_match() { local last_version="" @@ -59,6 +62,7 @@ fi echo ":: Preparing. Environment is..." print_environment echo " CARGO: $CARGO" +echo " RUSTFLAGS: $RUSTFLAGS" echo ":: Ensuring all crate versions match..." check_versions_match "${ALL_PROJECT_DIRS[@]}" diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 6ae73d00..5fef4589 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -595,17 +595,25 @@ the [`CookieJar`] documentation contains complete usage information. Cookies added via the [`CookieJar::add()`] method are set _in the clear._ In other words, the value set is visible to the client. For sensitive data, Rocket -provides _private_ cookies. +provides _private_ cookies. Private cookies are similar to regular cookies +except that they are encrypted using authenticated encryption, a form of +encryption which simultaneously provides confidentiality, integrity, and +authenticity. Thus, private cookies cannot be inspected, tampered with, or +manufactured by clients. If you prefer, you can think of private cookies as +being signed and encrypted. -Private cookies are just like regular cookies except that they are encrypted -using authenticated encryption, a form of encryption which simultaneously -provides confidentiality, integrity, and authenticity. This means that private -cookies cannot be inspected, tampered with, or manufactured by clients. If you -prefer, you can think of private cookies as being signed and encrypted. +Support for private cookies must be manually enabled via the `secrets` crate +feature: + +```toml +## in Cargo.toml +rocket = { version = "0.5.0-dev", features = ["secrets"] } +``` The API for retrieving, adding, and removing private cookies is identical except methods are suffixed with `_private`. These methods are: [`get_private`], -[`add_private`], and [`remove_private`]. An example of their usage is below: +[`get_private_pending`], [`add_private`], and [`remove_private`]. An example of +their usage is below: ```rust # #[macro_use] extern crate rocket; @@ -634,13 +642,11 @@ fn logout(cookies: &CookieJar<'_>) -> Flash { ### Secret Key To encrypt private cookies, Rocket uses the 256-bit key specified in the -`secret_key` configuration parameter. If one is not specified, Rocket will -automatically generate a fresh key. Note, however, that a private cookie can -only be decrypted with the same key with which it was encrypted. As such, it is -important to set a `secret_key` configuration parameter when using private -cookies so that cookies decrypt properly after an application restart. Rocket -emits a warning if an application is run in production without a configured -`secret_key`. +`secret_key` configuration parameter. When compiled in debug mode, a fresh key +is generated automatically. In release mode, Rocket requires you to set a secret +key if the `secrets` feature is enabled. Failure to do so results in a hard +error at launch time. The value of the parameter may either be a 256-bit base64 +or hex string or a 32-byte slice. Generating a string suitable for use as a `secret_key` configuration value is usually done through tools like `openssl`. Using `openssl`, a 256-bit base64 key @@ -650,6 +656,7 @@ For more information on configuration, see the [Configuration](../configuration) section of the guide. [`get_private`]: @api/rocket/http/struct.CookieJar.html#method.get_private +[`get_private_pending`]: @api/rocket/http/struct.CookieJar.html#method.get_private_pending [`add_private`]: @api/rocket/http/struct.CookieJar.html#method.add_private [`remove_private`]: @api/rocket/http/struct.CookieJar.html#method.remove_private diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 08c51fad..d2ef0c14 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -1,242 +1,180 @@ # Configuration -Rocket aims to have a flexible and usable configuration system. Rocket -applications can be configured via a configuration file, through environment -variables, or both. Configurations are separated into three environments: -development, staging, and production. The working environment is selected via an -environment variable. +Rocket's configuration system is flexible. Based on [Figment](@figment), it +allows you to configure your application the way _you_ want while also providing +with a sensible set of defaults. -## Environment +## Overview -At any point in time, a Rocket application is operating in a given -_configuration environment_. There are three such environments: +Rocket's configuration system is based on Figment's [`Provider`]s, types which +provide configuration data. Rocket's [`Config`] and [`Config::figment()`], as +well as Figment's [`Toml`] and [`Json`], are some examples of providers. +Providers can be combined into a single [`Figment`] provider from which any +configuration structure that implements [`Deserialize`] can be extracted. - * `development` (short: `dev`) - * `staging` (short: `stage`) - * `production` (short: `prod`) +Rocket expects to be able to extract a [`Config`] structure from the provider it +is configured with. This means that no matter which configuration provider +Rocket is asked to use, it must be able to read the following configuration +values: -Without any action, Rocket applications run in the `development` environment for -debug builds and the `production` environment for non-debug builds. The -environment can be changed via the `ROCKET_ENV` environment variable. For -example, to launch an application in the `staging` environment, we can run: +| key | kind | description | debug/release default | +|----------------|-----------------|-------------------------------------------------|-----------------------| +| `address` | `IpAddr` | IP address to serve on | `127.0.0.1` | +| `port` | `u16` | Port to serve on. | `8000` | +| `workers` | `u16` | Number of threads to use for executing futures. | cpu core count * 2 | +| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | +| `log_level` | `LogLevel` | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | +| `cli_colors` | `bool` | Whether to use colors and emoji when logging. | `true` | +| `secret_key` | `SecretKey` | Secret key for signing and encrypting values. | `None` | +| `tls` | `TlsConfig` | TLS configuration, if any. | `None` | +| `tls.key` | `&[u8]`/`&Path` | Path/bytes to DER-encoded ASN.1 PKCS#1/#8 key. | | +| `tls.certs` | `&[u8]`/`&Path` | Path/bytes to DER-encoded X.509 TLS cert chain. | | +| `limits` | `Limits` | Streaming read size limits. | [`Limits::default()`] | +| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | forms = "32KiB" | +| `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` | -```sh -ROCKET_ENV=stage cargo run -``` +### Profiles -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: +Configurations can be arbitrarily namespaced by [`Profile`]s. Rocket's +[`Config`] and [`Config::figment()`] providers automatically set the +configuration profile to "debug" when compiled in "debug" mode and "release" +when compiled in release mode. With the exception of `log_level`, which changes +from `normal` in debug to `critical` in release, all of the default +configuration values are the same in all profiles. What's more, all +configuration values _have_ defaults, so no configuration needs to be supplied +to get an application going. -```sh -$ sudo ROCKET_ENV=staging cargo run +In addition to any profiles you declare, there are two meta-profiles, `default` +and `global`, which can be used to provide values that apply to _all_ profiles. +Values provided in a `default` profile are used as fall-back values when the +selected profile doesn't contain a requested values, while values in the +`global` profile supplant any values with the same name in any profile. -🔧 Configured for staging. - => address: 0.0.0.0 - => port: 8000 - => log: normal - => workers: [logical cores * 2] - => secret key: generated - => limits: forms = 32KiB - => keep-alive: 5s - => tls: disabled -🛰 Mounting '/': - => GET / (hello) -🚀 Rocket has launched from http://0.0.0.0:8000 -``` +[`Provider`]: @figment/trait.Provider.html +[`Profile`]: @figment/struct.Profile.html +[`Config`]: @api/rocket/struct.Config.html +[`Config::figment()`]: @api/struct.Config.html#method.figment +[`Toml`]: @figment/providers/struct.Toml.html +[`Json`]: @figment/providers/struct.Json.html +[`Figment`]: @api/rocket/struct.Figment.html +[`Deserialize`]: @serde/trait.Deserialize.html +[`Limits::default()`]: @api/rocket/data/struct.Limits.html#impl-Default -## Rocket.toml +### Secret Key -An optional `Rocket.toml` file can be used to specify the configuration -parameters for each environment. If it is not present, the default configuration -parameters are used. Rocket searches for the file starting at the current -working directory. If it is not found there, Rocket checks the parent directory. -Rocket continues checking parent directories until the root is reached. +The `secret_key` parameter configures a cryptographic key to use when encrypting +application values. In particular, the key is used to encrypt [private cookies], +which are available only when the `secrets` crate feature is enabled. -The file must be a series of TOML tables, at most one for each environment, and -an optional "global" table. 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: +When compiled in debug mode, a fresh key is generated automatically. In release +mode, Rocket requires you to set a secret key if the `secrets` feature is +enabled. Failure to do so results in a hard error at launch time. The value of +the parameter may either be a 256-bit base64 or hex string or a slice of 32 +bytes. -```toml -[development] -address = "localhost" -port = 8000 -workers = [number of cpus * 2] -keep_alive = 5 -log = "normal" -secret_key = [randomly generated at launch] -limits = { forms = 32768 } +[private cookies]: ../requests/#private-cookies -[staging] -address = "0.0.0.0" -port = 8000 -workers = [number of cpus * 2] -keep_alive = 5 -log = "normal" -secret_key = [randomly generated at launch] -limits = { forms = 32768 } - -[production] -address = "0.0.0.0" -port = 8000 -workers = [number of cpus * 2] -keep_alive = 5 -log = "critical" -secret_key = [randomly generated at launch] -limits = { forms = 32768 } -``` - -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 secret key, the value should a random 256-bit -value, encoded as a base64 or base16 string. Such a string can be generated -using a tool like openssl: `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 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" -``` - -## Data Limits +### Limits The `limits` parameter configures the maximum amount of data Rocket will accept -for a given data type. The parameter is a table where each key corresponds to a -data type and each value corresponds to the maximum size in bytes Rocket -should accept for that type. +for a given data type. The value is expected to be a dictionary table where each +key corresponds to a data type and each value corresponds to the maximum size in +bytes Rocket should accept for that type. Rocket can parse both integers +(`32768`) or SI unit based strings (`"32KiB"`) as limits. -By default, Rocket limits forms to 32KiB (32768 bytes). To increase the limit, -simply set the `limits.forms` configuration parameter. For example, to increase -the forms limit to 128KiB globally, we might write: +By default, Rocket specifies a `32 KiB` limit for incoming forms. Since Rocket +requires specifying a read limit whenever data is read, external data guards may +also choose to have a configure limit via the `limits` parameter. The +[`rocket_contrib::Json`] type, for instance, uses the `limits.json` parameter. + +[`rocket_contrib::Json`]: @api/rocket_contrib/json/struct.Json.html + +### TLS + +Rocket includes built-in, native support for TLS >= 1.2 (Transport Layer +Security). In order for TLS support to be enabled, Rocket must be compiled with +the `"tls"` feature: ```toml -[global.limits] -forms = 131072 +[dependencies] +rocket = { version = "0.5.0-dev", features = ["tls"] } ``` -The `limits` parameter can contain keys and values that are not endemic to -Rocket. For instance, the [`Json`] type reads the `json` limit value to cap -incoming JSON data. You should use the `limits` parameter for your application's -data limits as well. Data limits can be retrieved at runtime via the -[`Request::limits()`] method. +TLS is configured through the `tls` configuration parameter. The value of `tls` +is a dictionary with two keys: `certs` and `key`, described in the table above. +Each key's value may be either a path to a file or raw bytes corresponding to +the expected value. When a path is configured in a file source, such as +`Rocket.toml`, relative paths are interpreted as being relative to the source +file's directory. -[`Request::limits()`]: @api/rocket/struct.Request.html#method.limits -[`Json`]: @api/rocket_contrib/json/struct.Json.html#incoming-data-limits +! warning: Rocket's built-in TLS implements only TLS 1.2 and 1.3. As such, it + may not be suitable for production use. -## Extras +## Default Provider -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](@api/rocket_contrib/templates/struct.Template.html) type -accepts a value for the `template_dir` configuration parameter. The parameter -can be set in `Rocket.toml` as follows: +Rocket's default configuration provider is [`Config::figment()`]; this is the +provider that's used when calling [`rocket::ignite()`]. + +The default figment merges, at a per-key level, and reads from the following +sources, in ascending priority order: + + 1. [`Config::default()`] - which provides default values for all parameters. + 2. `Rocket.toml` _or_ TOML file path in `ROCKET_CONFIG` environment variable. + 3. `ROCKET_` prefixed environment variables. + +The selected profile is the value of the `ROCKET_PROFILE` environment variable, +or if it is not set, "debug" when compiled in debug mode and "release" when +compiled in release mode. + +As a result, without any effort, Rocket's server can be configured via a +`Rocket.toml` file and/or via environment variables, the latter of which take +precedence over the former. Note that neither the file nor any environment +variables need to be present as [`Config::default()`] is a complete +configuration source. + +[`Config::default()`]: @api/rocket/struct.Config.html#method.default + +### Rocket.toml + +Rocket searches for `Rocket.toml` or the filename in a `ROCKET_CONFIG` +environment variable starting at the current working directory. If it is not +found, the parent directory, its parent, and so on, are searched until the file +is found or the root is reached. If the path set in `ROCKET_CONFIG` is absolute, +no such search occurs, and the set path is used directly. + +The file is assumed to be _nested_, so each top-level key declares a profile and +its values the value for the profile. The following is an example of what such a +file might look like: ```toml -[development] -template_dir = "dev_templates/" +## defaults for _all_ profiles +[default] +address = "0.0.0.0" +limits = { forms = "64 kB", json = "1 MiB" } -[production] -template_dir = "prod_templates/" +## set only when compiled in debug mode, i.e, `cargo build` +[debug] +port = 8000 +## only the `json` key from `default` will be overridden; `forms` will remain +limits = { json = "10MiB" } + +## set only when the `nyc` profile is selected +[nyc] +port = 9001 + +## set only when compiled in release mode, i.e, `cargo build --release` +## don't use this secret_key! generate your own and keep it private! +[release] +port = 9999 +secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" ``` -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: +### Environment Variables -```sh -🔧 Configured for development. - => ... - => [extra] template_dir: "dev_templates/" -``` - -To retrieve a custom, extra configuration parameter in your application, we -recommend using an [ad-hoc attach fairing] in combination with [managed state]. -For example, if your application makes use of a custom `assets_dir` parameter: - -[ad-hoc attach fairing]: ../fairings/#ad-hoc-fairings -[managed state]: ../state/#managed-state - -```toml -[development] -assets_dir = "dev_assets/" - -[production] -assets_dir = "prod_assets/" -``` - -The following code will: - - 1. Read the configuration parameter in an ad-hoc `attach` fairing. - 2. Store the parsed parameter in an `AssetsDir` structure in managed state. - 3. Retrieve the parameter in an `assets` route via the `State` guard. - -```rust -# #[macro_use] extern crate rocket; - -use std::path::{Path, PathBuf}; - -use rocket::State; -use rocket::response::NamedFile; -use rocket::fairing::AdHoc; - -struct AssetsDir(String); - -#[get("/")] -async fn assets(asset: PathBuf, assets_dir: State<'_, AssetsDir>) -> Option { - NamedFile::open(Path::new(&assets_dir.0).join(asset)).await.ok() -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/", routes![assets]) - .attach(AdHoc::on_attach("Assets Config", |mut rocket| async { - let assets_dir = rocket.config().await - .get_str("assets_dir") - .unwrap_or("assets/") - .to_string(); - - Ok(rocket.manage(AssetsDir(assets_dir))) - })) -} -``` - -## 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 ./your_application - -🔧 Configured for development. - => ... - => port: 3721 -``` - -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 +Rocket reads all environment variable names prefixed with `ROCKET_` using the +string after the `_` as the name of a configuration value as the value of the +parameter as the value itself. Environment variables take precedence over values +in `Rocket.toml`. Values are parsed as loose form of TOML syntax. Consider the following examples: ```sh @@ -249,80 +187,161 @@ ROCKET_ARRAY=[1,"b",3.14] ROCKET_DICT={key="abc",val=123} ``` -## Programmatic +## Extracting Values -In addition to using environment variables or a config file, Rocket can also be -configured using the [`rocket::custom()`] method and [`ConfigBuilder`]: +Your application can extract any configuration that implements [`Deserialize`] +from the configured provider, which is exposed via [`Rocket::figment()`] and +[`Cargo::figment()`]: + +```rust +# #[macro_use] extern crate rocket; +# extern crate serde; + +use serde::Deserialize; + + +#[launch] +async fn rocket() -> _ { + let mut rocket = rocket::ignite(); + let figment = rocket.figment().await; + + #[derive(Deserialize)] + struct Config { + port: u16, + custom: Vec, + } + + // extract the entire config any `Deserialize` value + let config: Config = figment.extract().expect("config"); + + // or a piece of it into any `Deserialize` value + let custom: Vec = figment.extract_inner("custom").expect("custom"); + + rocket +} +``` + +Both values recognized by Rocket and values _not_ recognized by Rocket can be +extracted. This means you can configure values recognized by your application in +Rocket's configuration sources directly. The next section describes how you can +customize configuration sources by supplying your own `Provider`. + +Because it is common to store configuration in managed state, Rocket provides an +`AdHoc` fairing that 1) extracts a configuration from the configured provider, +2) pretty prints any errors, and 3) stores the value in managed state: + +```rust +# #[macro_use] extern crate rocket; +# extern crate serde; +# use serde::Deserialize; +# #[derive(Deserialize)] +# struct Config { +# port: u16, +# custom: Vec, +# } + +use rocket::{State, fairing::AdHoc}; + +#[get("/custom")] +fn custom(config: State<'_, Config>) -> String { + config.custom.get(0).cloned().unwrap_or("default".into()) +} + +#[launch] +fn rocket() -> _ { + rocket::ignite() + .mount("/", routes![custom]) + .attach(AdHoc::config::()) +} +``` + +[`Rocket::figment()`]: @api/rocket/struct.Rocket.html#method.figment +[`Cargo::figment()`]: @api/rocket/struct.Cargo.html#method.figment + +## Custom Providers + +A custom provider can be set via [`rocket::custom()`], which replaces calls to +[`rocket::ignite()`]. The configured provider can be built on top of +[`Config::figment()`], [`Config::default()`], both, or neither. The +[Figment](@figment) documentation has full details on instantiating existing +providers like [`Toml`] and [`Json`] as well as creating custom providers for +more complex cases. + +! note: You may need to depend on `figment` and `serde` directly. + + Rocket reexports `figment` from its crate root, so you can refer to `figment` + types via `rocket::figment`. However, Rocket does not enable all features from + the figment crate. As such, you may need to import `figment` directly: + + ` + figment = { version = "0.9", features = ["env", "toml", "json"] } + ` + + Furthermore, you should directly depend on `serde` when using its `derive` + feature, which is also not enabled by Rocket: + + ` + serde = { version = "1", features = ["derive"] } + ` + +As a first example, we override configuration values at runtime by merging +figment's tuple providers with Rocket's default provider: ```rust # #[macro_use] extern crate rocket; -use rocket::config::{Config, Environment}; +use rocket::data::{Limits, ToByteUnit}; -# fn build_config() -> rocket::config::Result { -let config = Config::build(Environment::Staging) - .address("1.2.3.4") - .port(9234) - .finalize()?; -# Ok(config) -# } +#[launch] +fn rocket() -> _ { + let figment = rocket::Config::figment() + .merge(("port", 1111)) + .merge(("limits", Limits::new().limit("json", 2.mebibytes()))); -# let config = build_config().expect("config okay"); -# /* -rocket::custom(config) - .mount("/", routes![/* .. */]) - .launch() - .await; -# */ + rocket::custom(figment).mount("/", routes![/* .. */]) +} ``` -Configuration via `rocket::custom()` replaces calls to `rocket::ignite()` and -all configuration from `Rocket.toml` or environment variables. In other words, -using `rocket::custom()` results in `Rocket.toml` and environment variables -being ignored. +More involved, consider an application that wants to use Rocket's defaults for +[`Config`], but not its configuration sources, while allowing the application to +be configured via an `App.toml` file and `APP_` environment variables: + +```rust +# #[macro_use] extern crate rocket; + +use serde::{Serialize, Deserialize}; +use figment::{Figment, providers::{Format, Toml, Serialized, Env}}; +use rocket::fairing::AdHoc; + +#[derive(Debug, Deserialize, Serialize)] +struct Config { + app_value: usize, + /* and so on.. */ +} + +impl Default for Config { + fn default() -> Config { + Config { app_value: 3, } + } +} + +#[launch] +fn rocket() -> _ { + let figment = Figment::from(rocket::Config::default()) + .merge(Serialized::defaults(Config::default())) + .merge(Toml::file("App.toml")) + .merge(Env::prefixed("APP_")); + + rocket::custom(figment) + .mount("/", routes![/* .. */]) + .attach(AdHoc::config::()) +} +``` + +Rocket will extract it's configuration from the configured provider. This means +that if values like `port` and `address` are configured in `Config`, `App.toml` +or `APP_` environment variables, Rocket will make use of them. The application +can also extract its configuration, done here via the `Adhoc::config()` fairing. [`rocket::custom()`]: @api/rocket/fn.custom.html -[`ConfigBuilder`]: @api/rocket/config/struct.ConfigBuilder.html - -## Configuring TLS - -! warning: Rocket's built-in TLS is **not** considered ready for production use. - It is intended for development use _only_. - -Rocket includes built-in, native support for TLS >= 1.2 (Transport Layer -Security). In order for TLS support to be enabled, Rocket must be compiled with -the `"tls"` feature. To do this, add the `"tls"` feature to the `rocket` -dependency in your `Cargo.toml` file: - -```toml -[dependencies] -rocket = { version = "0.5.0-dev", features = ["tls"] } -``` - -TLS is configured through the `tls` configuration parameter. The value of `tls` -must be a table with two keys: - - * `certs`: _[string]_ a path to a certificate chain in PEM format - * `key`: _[string]_ a path to a private key file in PEM format for the - certificate in `certs` - -The recommended way to specify these parameters is via the `global` environment: - -```toml -[global.tls] -certs = "/path/to/certs.pem" -key = "/path/to/key.pem" -``` - -Of course, you can always specify the configuration values per environment: - -```toml -[development] -tls = { certs = "/path/to/certs.pem", key = "/path/to/key.pem" } -``` - -Or via environment variables: - -```sh -ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} cargo run -``` +[`rocket::ignite()`]: @api/rocket/fn.custom.html diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index 004d6de4..5cf121a9 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -6,8 +6,9 @@ edition = "2018" publish = false [dev-dependencies] -rocket = { path = "../../core/lib" } +rocket = { path = "../../core/lib", features = ["secrets"] } doc-comment = "0.3" rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] } serde = { version = "1.0", features = ["derive"] } rand = "0.7" +figment = { version = "0.9.2", features = ["toml", "env"] }