Revamp configuration.

This commit completely overhauls Rocket's configuration systems, basing
it on the new Figment library. It includes many breaking changes
pertaining to configuration. They are:

  * "Environments" are replaced by "profiles".
  * 'ROCKET_PROFILE' takes the place of 'ROCKET_ENV'.
  * Profile names are now arbitrary, but 'debug' and 'release' are given
    special treatment as default profiles for the debug and release
    compilation profiles.
  * A 'default' profile now sits along-side the meta 'global' profile.
  * The concept of "extras" is no longer present; users can extract any
    values they want from the configured 'Figment'.
  * The 'Poolable' trait takes an '&Config'.
  * The 'secrets' feature is disabled by default.
  * It is a hard error if 'secrets' is enabled under the 'release'
    profile and no 'secret_key' is configured.
  * 'ConfigBuilder' no longer exists: all fields of 'Config' are public
    with public constructors for each type.
  * 'keep_alive' is disabled with '0', not 'false' or 'off'.
  * Inlined error variants into the 'Error' structure.
  * 'LoggingLevel' is now 'LogLevel'.
  * Limits can now be specified in SI units: "1 MiB".

The summary of other changes are:

  * The default config file can be configured with 'ROCKET_CONFIG'.
  * HTTP/1 and HTTP/2 keep-alive configuration is restored.
  * 'ctrlc' is now a recognized config option.
  * 'serde' is now a core dependency.
  * TLS misconfiguration errors are improved.
  * Several example use '_' as the return type of '#[launch]' fns.
  * 'AdHoc::config()' was added for simple config extraction.
  * Added more documentation for using 'Limits'.
  * Launch information is no longer treated specially.
  * The configuration guide was rewritten.

Resolves #852.
Resolves #209.
Closes #1404.
Closes #652.
This commit is contained in:
Sergio Benitez 2020-09-02 22:41:31 -07:00
parent 8da034ab83
commit 1fb061496d
61 changed files with 2045 additions and 4412 deletions

View File

@ -71,13 +71,13 @@ pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result<TokenStrea
let databases = quote_spanned!(span => ::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<Self, #conn_type>);
};
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<TokenStrea
impl<'a, 'r> #request::FromRequest<'a, 'r> for #guard_type {
type Error = ();
async fn from_request(request: &'a #request::Request<'r>) -> #request::Outcome<Self, ()> {
<#databases::Connection<Self, #conn_type>>::from_request(request).await.map(Self)
async fn from_request(req: &'a #request::Request<'r>) -> #request::Outcome<Self, ()> {
<#databases::Connection<Self, #conn_type>>::from_request(req).await.map(Self)
}
}
}.into())

View File

@ -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]

View File

@ -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]

View File

@ -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"]

View File

@ -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<String, Value>,
/// 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<Config, Error> {
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::<Self>(&db_key)
}
}
/// A wrapper around `r2d2::Error`s or a custom database error type.
@ -458,143 +478,6 @@ pub enum DbError<T> {
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<DatabaseConfig<'a>, 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<foo::Error>;
///
/// fn pool(config: DatabaseConfig) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
/// let manager = foo::ConnectionManager::new(config.url)
/// fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, Self::Error>;
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, Self::Error>;
}
#[cfg(feature = "diesel_sqlite_pool")]
@ -700,8 +583,8 @@ impl Poolable for diesel::SqliteConnection {
type Manager = diesel::r2d2::ConnectionManager<diesel::SqliteConnection>;
type Error = r2d2::Error;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
let manager = diesel::r2d2::ConnectionManager::new(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<diesel::PgConnection>;
type Error = r2d2::Error;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
let manager = diesel::r2d2::ConnectionManager::new(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<diesel::MysqlConnection>;
type Error = r2d2::Error;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
let manager = diesel::r2d2::ConnectionManager::new(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<postgres::tls::NoTls>;
type Error = DbError<postgres::Error>;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, Self::Error> {
let opts = mysql::OptsBuilder::from_opts(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<r2d2::Pool<Self::Manager>, Self::Error> {
let manager = r2d2_sqlite::SqliteConnectionManager::file(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<memcache::MemcacheError>;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
let manager = r2d2_memcache::MemcacheConnectionManager::new(config.url);
fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, 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<K, C: Poolable> {
config: Config,
pool: r2d2::Pool<C::Manager>,
semaphore: Arc<Semaphore>,
_marker: PhantomData<fn() -> K>,
}
impl<K, C: Poolable> Clone for ConnectionPool<K, C> {
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<F, R>(job: F) -> R
}
impl<K: 'static, C: Poolable> ConnectionPool<K, C> {
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::<K, C> {
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<K: 'static, C: Poolable> ConnectionPool<K, C> {
}
async fn get(&self) -> Result<Connection<K, C>, ()> {
// 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<K: 'static, C: Poolable> ConnectionPool<K, C> {
pub async fn get_one(cargo: &rocket::Cargo) -> Option<Connection<K, C>> {
match cargo.state::<Self>() {
Some(pool) => pool.get().await.ok(),
None => {
error_!("Database fairing was not attached for {}", std::any::type_name::<K>());
None
}
None => None
}
}
#[inline]
pub async fn get_pool(cargo: &rocket::Cargo) -> Option<Self> {
cargo.state::<Self>().map(|pool| pool.clone())
}
}
impl<K: 'static, C: Poolable> Connection<K, C> {
@ -911,7 +804,8 @@ impl<K, C: Poolable> Drop for Connection<K, C> {
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<K, C> {
}
}
}
#[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<String, Value> = 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"));
}
}

View File

@ -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::<Hsts>()
{
warn_!("Space Helmet: deploying with TLS without enabling HSTS.");

View File

@ -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<Rocket, Rocket> {
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::<RelativePathBuf>("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)))
}

View File

@ -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<String, Value> = Map::new();
let mut test_db_opts: Map<String, Value> = 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()`.

View File

@ -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("/<engine>/<name>")]
@ -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])
}

View File

@ -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)
}

View File

@ -6,7 +6,7 @@ error: invalid path URI: expected token / but found a at index 0
|
= help: expected path in origin form: "/path/<param>"
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("")]

View File

@ -42,17 +42,17 @@ error[E0277]: the trait bound `S: FromUriParam<rocket::http::uri::Path, _>` is n
|
= note: required by `from_uri_param`
error[E0277]: the trait bound `i32: FromUriParam<rocket::http::uri::Path, Option<{integer}>>` is not satisfied
error[E0277]: the trait bound `i32: FromUriParam<rocket::http::uri::Path, std::option::Option<{integer}>>` is not satisfied
--> $DIR/typed-uri-bad-type.rs:53:26
|
53 | uri!(optionals: id = Some(10), name = Ok("bob".into()));
| ^^^^^^^^ the trait `FromUriParam<rocket::http::uri::Path, Option<{integer}>>` is not implemented for `i32`
| ^^^^^^^^ the trait `FromUriParam<rocket::http::uri::Path, std::option::Option<{integer}>>` is not implemented for `i32`
|
= help: the following implementations were found:
<i32 as FromUriParam<P, &'x i32>>
<i32 as FromUriParam<P, &'x mut i32>>
<i32 as FromUriParam<P, i32>>
= note: required because of the requirements on the impl of `FromUriParam<rocket::http::uri::Path, Option<{integer}>>` for `Option<i32>`
= note: required because of the requirements on the impl of `FromUriParam<rocket::http::uri::Path, std::option::Option<{integer}>>` for `std::option::Option<i32>`
= note: required by `from_uri_param`
error[E0277]: the trait bound `std::string::String: FromUriParam<rocket::http::uri::Path, std::result::Result<_, _>>` is not satisfied

View File

@ -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/<param>"
--> $DIR/route-path-bad-syntax.rs:8:7
|

View File

@ -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" }

View File

@ -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<Self> { 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() { }
/// ```
///

View File

@ -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<SocketAddr>;
}
/// 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<L> {
listener: L,

View File

@ -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<Vec<Certificate>> {
pemfile::certs(reader)
.map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid certificate"))
}
// TODO.async: consider using async fs operations
pub fn load_certs<P: AsRef<Path>>(path: P) -> Result<Vec<rustls::Certificate>, 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<P: AsRef<Path>>(path: P) -> Result<rustls::PrivateKey, Error> {
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<PrivateKey> {
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<Box<dyn Future<Output=Result<TlsStream<TcpStream>, io::Error>> + Send>>),
Accepting(Accept<TcpStream>),
}
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<Result<Self::Connection, io::Error>> {
fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<Self::Connection>> {
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<C: io::BufRead + Send, K: io::BufRead + Send>(
address: SocketAddr,
cert_chain: Vec<Certificate>,
key: PrivateKey
mut cert_chain: C,
mut private_key: K,
) -> io::Result<TlsListener> {
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();

View File

@ -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"

View File

@ -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;

View File

@ -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])
}

View File

@ -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;

View File

@ -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<String>,
/// 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<String, Value>,
/// The root directory of this config, if any.
pub root: Option<PathBuf>,
}
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<A: Into<String>>(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<K: Into<String>>(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<C, K>(mut self, certs_path: C, key_path: K) -> Self
where C: Into<String>, K: Into<String>
{
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<P: AsRef<Path>>(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<Value>` including `&str`, `String`, `Vec<V: Into<Value>>`,
/// `HashMap<S: Into<String>, V: Into<Value>>`, 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<V: Into<Value>>(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<Config> {
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)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<Certificate>,
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<u64> {
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<u16> {
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<u32> {
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<LoggingLevel> {
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<Limits> {
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)
}

View File

@ -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<Environment, ConfigError> {
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::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Development);
///
/// let env = "dev".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Development);
///
/// let env = "devel".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Development);
/// ```
///
/// Parsing a staging environment:
///
/// ```rust
/// use rocket::config::Environment;
///
/// let env = "staging".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Staging);
///
/// let env = "stage".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Staging);
/// ```
///
/// Parsing a production environment:
///
/// ```rust
/// use rocket::config::Environment;
///
/// let env = "production".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Production);
///
/// let env = "prod".parse::<Environment>();
/// assert_eq!(env.unwrap(), Environment::Production);
/// ```
fn from_str(s: &str) -> Result<Self, Self::Err> {
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"),
}
}
}

View File

@ -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<PathBuf>),
/// 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
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<SecretKey> {
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<Self, Self::Error> {
Outcome::Success(&req.state.config.secret_key)
}
}
impl Serialize for SecretKey {
fn serialize<S: ser::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
// We encode as "zero" to avoid leaking the key.
ser.serialize_bytes(&[0; 32][..])
}
}
impl<'de> Deserialize<'de> for SecretKey {
fn deserialize<D: de::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
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<E: de::Error>(self, val: &str) -> Result<SecretKey, E> {
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<E: de::Error>(self, bytes: &[u8]) -> Result<SecretKey, E> {
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
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]"),
}
}
}

148
core/lib/src/config/tls.rs Normal file
View File

@ -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<RelativePathBuf, Vec<u8>>,
/// Path or raw bytes to DER-encoded ASN.1 key in either PKCS#8 or PKCS#1
/// format.
pub(crate) key: Either<RelativePathBuf, Vec<u8>>,
}
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<C, K>(certs: C, key: K) -> Self
where C: AsRef<std::path::Path>, K: AsRef<std::path::Path>
{
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<std::path::PathBuf, &[u8]> {
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<std::path::PathBuf, &[u8]> {
match &self.key {
Either::Left(path) => either::Either::Left(path.relative()),
Either::Right(bytes) => either::Either::Right(&bytes),
}
}
}
#[cfg(feature = "tls")]
type Reader = Box<dyn std::io::BufRead + Sync + Send>;
#[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<RelativePathBuf, Vec<u8>>) -> io::Result<Reader> {
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)?))
}
}

View File

@ -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<T, Input<'a>>;
#[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::<i64>() {
Value::Integer(int)
} else if let Ok(float) = value_str.parse::<f64>() {
Value::Float(float)
} else {
Value::String(value_str.into())
}
}
};
skip_while(is_whitespace)?;
val
}
pub fn parse_simple_toml_value(input: &str) -> StdResult<Value, String> {
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
}));
}
}

View File

@ -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<S, E> = outcome::Outcome<S, (Status, E), Data>;
@ -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<T>
}
#[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<u8> {
type Error = std::io::Error;

View File

@ -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 = "<data>")]
/// async fn echo(data: Data, limits: &Limits) -> Result<String, Debug<io::Error>> {
/// 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<Self, MyError> {
/// 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<S: Into<String>>(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<ByteUnit> {
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<Self, Self::Error> {
Outcome::Success(req.limits())
}
}

View File

@ -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<dyn std::error::Error + Send + Sync>),
}
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<dyn std::error::Error + Send + Sync>),
/// Route collisions were detected.
Collision(Vec<(Route, Route)>),
/// A launch fairing reported an error.
FailedFairings(Vec<&'static str>),
}
impl From<ErrorKind> 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<io::Error> 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");
}
}
}

View File

@ -92,9 +92,8 @@ impl AdHoc {
/// let fairing = AdHoc::on_attach("No-Op", |rocket| async { Ok(rocket) });
/// ```
pub fn on_attach<F, Fut>(name: &'static str, f: F) -> AdHoc
where
F: FnOnce(Rocket) -> Fut + Send + 'static,
Fut: Future<Output=Result<Rocket, Rocket>> + Send + 'static,
where F: FnOnce(Rocket) -> Fut + Send + 'static,
Fut: Future<Output=Result<Rocket, Rocket>> + 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::<Config>();
/// ```
pub fn config<'de, T>() -> AdHoc
where T: serde::Deserialize<'de> + Send + Sync + 'static
{
AdHoc::on_attach(std::any::type_name::<T>(), |mut rocket| async {
let figment = rocket.figment().await;
let app_config = match figment.extract::<T>() {
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.
///

View File

@ -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<T: figment::Provider>(provider: T) -> Rocket {
Rocket::custom(provider)
}
// TODO.async: More thoughtful plan for async tests
@ -170,6 +177,7 @@ pub fn async_test<R>(fut: impl std::future::Future<Output = R> + Send) -> R {
pub fn async_main<R>(fut: impl std::future::Future<Output = R> + Send) -> R {
tokio::runtime::Builder::new()
.threaded_scheduler()
.thread_name("rocket-worker-thread")
.enable_all()
.build()
.expect("create tokio runtime")

View File

@ -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<Client, LaunchError> {
) -> Result<Client, Error> {
rocket.prelaunch_check().await?;
let cargo = rocket.into_cargo().await;
let cookies = RwLock::new(cookie::CookieJar::new());

View File

@ -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());
}

View File

@ -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<Client, LaunchError> {
fn _new(rocket: Rocket, tracked: bool) -> Result<Client, Error> {
let mut runtime = tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()

View File

@ -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<Self, LaunchError> {
pub $($prefix)? fn tracked(rocket: Rocket) -> Result<Self, Error> {
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<Self, LaunchError> {
pub $($prefix)? fn untracked(rocket: Rocket) -> Result<Self, Error> {
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<Self, LaunchError> {
pub $($prefix)? fn new(rocket: Rocket) -> Result<Self, Error> {
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)
}

View File

@ -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<Self, Self::Err> {
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<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for LogLevel {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
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.

View File

@ -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<T> {
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<T> {
})
}
fn from_data(_: &'f Request<'_>, o: Transformed<'f, Self>) -> FromDataFuture<'f, Self, Self::Error> {
Box::pin(futures::future::ready(o.borrowed().and_then(|data| {
<Form<T>>::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| <Form<T>>::from_data(data, true).map(Form))
})
}
}

View File

@ -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<F: Fn(&mut Request<'_>)>(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);

View File

@ -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() {

View File

@ -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<PreLaunchOp>,
router: Router,
@ -66,6 +68,9 @@ pub(crate) struct Token;
impl Rocket {
#[inline]
fn _mount(&mut self, base: Origin<'static>, routes: Vec<Route>) {
// 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<dyn Fairing>) -> 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<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, 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<hyper::Response<hyper::Body>>,
) {
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<L>(mut self, listener: L) -> Result<(), crate::error::Error>
async fn listen_on<L>(mut self, listener: L) -> Result<(), Error>
where L: Listener + Send + Unpin + 'static,
<L as Listener>::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: &<L as Listener>::Connection| {
let service = hyper::make_service_fn(move |conn: &<L as Listener>::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<T: figment::Provider>(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

View File

@ -401,7 +401,7 @@ mod tests {
fn req_route_mt_collide<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool
where S1: Into<Option<&'static str>>, S2: Into<Option<&'static str>>
{
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)

View File

@ -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)
}

View File

@ -14,16 +14,13 @@ fn index(form: Form<Simple>) -> 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])
}

View File

@ -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="

View File

@ -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() }

View File

@ -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
}

View File

@ -1,8 +0,0 @@
#[macro_use] extern crate rocket;
mod common;
#[test]
fn test_development_config() {
common::test_config(rocket::config::Environment::Development);
}

View File

@ -1,8 +0,0 @@
#[macro_use] extern crate rocket;
mod common;
#[test]
fn test_production_config() {
common::test_config(rocket::config::Environment::Production);
}

View File

@ -1,8 +0,0 @@
#[macro_use] extern crate rocket;
mod common;
#[test]
fn test_staging_config() {
common::test_config(rocket::config::Environment::Staging);
}

View File

@ -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"

View File

@ -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!");

View File

@ -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"

View File

@ -0,0 +1,2 @@
[global]
template_dir = "templates/"

View File

@ -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[@]}"

View File

@ -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<Redirect> {
### 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

View File

@ -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("/<asset..>")]
async fn assets(asset: PathBuf, assets_dir: State<'_, AssetsDir>) -> Option<NamedFile> {
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<String>,
}
// 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<String> = 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<String>,
# }
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::<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<Config> {
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::<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

View File

@ -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"] }