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 databases = quote_spanned!(span => ::rocket_contrib::databases);
let request = quote!(::rocket::request); let request = quote!(::rocket::request);
let generated_types = quote_spanned! { span => let request_guard_type = quote_spanned! { span =>
/// The request guard type. /// The request guard type.
#vis struct #guard_type(#databases::Connection<Self, #conn_type>); #vis struct #guard_type(#databases::Connection<Self, #conn_type>);
}; };
Ok(quote! { Ok(quote! {
#generated_types #request_guard_type
impl #guard_type { impl #guard_type {
/// Returns a fairing that initializes the associated database /// 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 { impl<'a, 'r> #request::FromRequest<'a, 'r> for #guard_type {
type Error = (); type Error = ();
async fn from_request(request: &'a #request::Request<'r>) -> #request::Outcome<Self, ()> { async fn from_request(req: &'a #request::Request<'r>) -> #request::Outcome<Self, ()> {
<#databases::Connection<Self, #conn_type>>::from_request(request).await.map(Self) <#databases::Connection<Self, #conn_type>>::from_request(req).await.map(Self)
} }
} }
}.into()) }.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 --> $DIR/database-syntax.rs:6:1
| |
6 | #[database] 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 --> $DIR/database-syntax.rs:6:1
| |
6 | #[database] 6 | #[database]

View File

@ -14,7 +14,10 @@ edition = "2018"
[features] [features]
# Internal use only. # Internal use only.
templates = ["serde", "serde_json", "glob", "notify"] 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. # User-facing features.
default = ["json", "serve"] default = ["json", "serve"]

View File

@ -40,7 +40,9 @@
//! See [Provided](#provided) for a list of supported database and their //! See [Provided](#provided) for a list of supported database and their
//! associated feature name. //! 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 //! ```toml
//! [global.databases] //! [global.databases]
@ -97,8 +99,9 @@
//! //!
//! ## Configuration //! ## Configuration
//! //!
//! Databases can be configured via various mechanisms: `Rocket.toml`, //! Databases can be configured as any other values. Using the default
//! procedurally via `rocket::custom()`, or via environment variables. //! configuration provider, either via `Rocket.toml` or environment variables.
//! You can also use a custom provider.
//! //!
//! ### `Rocket.toml` //! ### `Rocket.toml`
//! //!
@ -138,31 +141,23 @@
//! The example below does just this: //! The example below does just this:
//! //!
//! ```rust //! ```rust
//! #[macro_use] extern crate rocket; //! # #[cfg(feature = "diesel_sqlite_pool")] {
//! use rocket::figment::{value::{Map, Value}, util::map};
//! //!
//! # #[cfg(feature = "diesel_sqlite_pool")] //! #[rocket::launch]
//! # mod test { //! fn rocket() -> _ {
//! use std::collections::HashMap; //! let db: Map<_, Value> = map! {
//! use rocket::config::{Config, Environment, Value}; //! "url" => "db.sqlite".into(),
//! "pool_size" => 10.into()
//! };
//! //!
//! #[launch] //! let figment = rocket::Config::figment()
//! fn rocket() -> rocket::Rocket { //! .merge(("databases", map!["my_db" => db]));
//! let mut database_config = HashMap::new();
//! let mut databases = HashMap::new();
//! //!
//! // This is the same as the following TOML: //! rocket::custom(figment)
//! // 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)
//! } //! }
//! # } fn main() {} //! # rocket();
//! # }
//! ``` //! ```
//! //!
//! ### Environment Variables //! ### Environment Variables
@ -246,33 +241,22 @@
//! # #[macro_use] extern crate rocket; //! # #[macro_use] extern crate rocket;
//! # #[macro_use] extern crate rocket_contrib; //! # #[macro_use] extern crate rocket_contrib;
//! # //! #
//! # #[cfg(feature = "diesel_sqlite_pool")] //! # #[cfg(feature = "diesel_sqlite_pool")] {
//! # mod test { //! # use rocket::figment::{value::{Map, Value}, util::map};
//! # use std::collections::HashMap;
//! # use rocket::config::{Config, Environment, Value};
//! #
//! use rocket_contrib::databases::diesel; //! use rocket_contrib::databases::diesel;
//! //!
//! #[database("my_db")] //! #[database("my_db")]
//! struct MyDatabase(diesel::SqliteConnection); //! struct MyDatabase(diesel::SqliteConnection);
//! //!
//! #[launch] //! #[launch]
//! fn rocket() -> rocket::Rocket { //! fn rocket() -> _ {
//! # let mut db_config = HashMap::new(); //! # let db: Map<_, Value> = map![
//! # let mut databases = HashMap::new(); //! # "url" => "db.sqlite".into(), "pool_size" => 10.into()
//! # //! # ];
//! # db_config.insert("url", Value::from("database.sqlite")); //! # let figment = rocket::Config::figment().merge(("databases", map!["my_db" => db]));
//! # db_config.insert("pool_size", Value::from(10)); //! rocket::custom(figment).attach(MyDatabase::fairing())
//! # 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 main() {} //! # }
//! ``` //! ```
//! //!
//! ## Handlers //! ## Handlers
@ -376,22 +360,23 @@
pub extern crate r2d2; pub extern crate r2d2;
#[cfg(any(feature = "diesel_sqlite_pool", #[cfg(any(
feature = "diesel_sqlite_pool",
feature = "diesel_postgres_pool", feature = "diesel_postgres_pool",
feature = "diesel_mysql_pool"))] feature = "diesel_mysql_pool"
))]
pub extern crate diesel; pub extern crate diesel;
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use rocket::config::{self, Value};
use rocket::fairing::{AdHoc, Fairing}; use rocket::fairing::{AdHoc, Fairing};
use rocket::request::{Request, Outcome, FromRequest}; use rocket::request::{Request, Outcome, FromRequest};
use rocket::outcome::IntoOutcome; use rocket::outcome::IntoOutcome;
use rocket::http::Status; use rocket::http::Status;
use rocket::tokio::sync::{OwnedSemaphorePermit, Semaphore, Mutex}; use rocket::tokio::sync::{OwnedSemaphorePermit, Semaphore, Mutex};
use rocket::tokio::time::timeout;
use self::r2d2::ManageConnection; use self::r2d2::ManageConnection;
@ -425,7 +410,7 @@ use self::r2d2::ManageConnection;
/// [`database_config`]`("my_database", &config)`: /// [`database_config`]`("my_database", &config)`:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// DatabaseConfig { /// Config {
/// url: "dummy_db.sqlite", /// url: "dummy_db.sqlite",
/// pool_size: 10, /// pool_size: 10,
/// extras: { /// extras: {
@ -434,49 +419,22 @@ use self::r2d2::ManageConnection;
/// }, /// },
/// } /// }
/// ``` /// ```
#[derive(Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct DatabaseConfig<'a> { pub struct Config {
/// The connection URL specified in the Rocket configuration. /// Connection URL specified in the Rocket configuration.
pub url: &'a str, pub url: String,
/// The size of the pool to be initialized. Defaults to the number of /// Initial pool size. Defaults to the number of Rocket workers.
/// Rocket workers.
pub pool_size: u32, pub pool_size: u32,
/// Any extra options that are included in the configuration, **excluding** /// How long to wait, in seconds, for a new connection before timing out.
/// the url and pool_size. /// Defaults to `5`.
pub extras: rocket::config::Map<String, Value>, // FIXME: Use `time`.
pub timeout: u8,
} }
/// A wrapper around `r2d2::Error`s or a custom database error type. use serde::{Serialize, Deserialize};
/// use rocket::figment::{Figment, Error, providers::Serialized};
/// This type is only relevant to implementors of the [`Poolable`] trait. See
/// the [`Poolable`] documentation for more information on how to use this type.
#[derive(Debug)]
pub enum DbError<T> {
/// The custom error type to wrap alongside `r2d2::Error`.
Custom(T),
/// The error returned by an r2d2 pool.
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),
}
impl Config {
/// Retrieves the database configuration for the database named `name`. /// Retrieves the database configuration for the database named `name`.
/// ///
/// This function is primarily used by the code generated by the `#[database]` /// This function is primarily used by the code generated by the `#[database]`
@ -496,103 +454,28 @@ pub enum ConfigError {
/// for the `my_db` and `my_other_db` databases: /// for the `my_db` and `my_other_db` databases:
/// ///
/// ```rust /// ```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>( pub fn from(cargo: &rocket::Cargo, db: &str) -> Result<Config, Error> {
name: &str, let db_key = format!("databases.{}", db);
from: &'a config::Config let key = |name: &str| format!("{}.{}", db_key, name);
) -> Result<DatabaseConfig<'a>, ConfigError> { Figment::from(cargo.figment())
// Find the first `databases` config that's a table with a key of 'name' .merge(Serialized::default(&key("pool_size"), cargo.config().workers))
// equal to `name`. .merge(Serialized::default(&key("timeout"), 5))
let connection_config = from.get_table("databases") .extract_inner::<Self>(&db_key)
.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)
},
} }
} }
/// A wrapper around `r2d2::Error`s or a custom database error type.
///
/// This type is only relevant to implementors of the [`Poolable`] trait. See
/// the [`Poolable`] documentation for more information on how to use this type.
#[derive(Debug)]
pub enum DbError<T> {
/// The custom error type to wrap alongside `r2d2::Error`.
Custom(T),
/// The error returned by an r2d2 pool.
PoolError(r2d2::Error),
} }
/// Trait implemented by `r2d2`-based database adapters. /// Trait implemented by `r2d2`-based database adapters.
@ -629,7 +512,7 @@ impl<'a> Display for ConfigError {
/// `Poolable` for `foo::Connection`: /// `Poolable` for `foo::Connection`:
/// ///
/// ```rust /// ```rust
/// use rocket_contrib::databases::{r2d2, DbError, DatabaseConfig, Poolable}; /// use rocket_contrib::databases::{r2d2, DbError, Config, Poolable};
/// # mod foo { /// # mod foo {
/// # use std::fmt; /// # use std::fmt;
/// # use rocket_contrib::databases::r2d2; /// # use rocket_contrib::databases::r2d2;
@ -661,8 +544,8 @@ impl<'a> Display for ConfigError {
/// type Manager = foo::ConnectionManager; /// type Manager = foo::ConnectionManager;
/// type Error = DbError<foo::Error>; /// type Error = DbError<foo::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 = foo::ConnectionManager::new(config.url) /// let manager = foo::ConnectionManager::new(&config.url)
/// .map_err(DbError::Custom)?; /// .map_err(DbError::Custom)?;
/// ///
/// r2d2::Pool::builder() /// r2d2::Pool::builder()
@ -692,7 +575,7 @@ pub trait Poolable: Send + Sized + 'static {
/// Creates an `r2d2` connection pool for `Manager::Connection`, returning /// Creates an `r2d2` connection pool for `Manager::Connection`, returning
/// the pool on success. /// 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")] #[cfg(feature = "diesel_sqlite_pool")]
@ -700,8 +583,8 @@ impl Poolable for diesel::SqliteConnection {
type Manager = diesel::r2d2::ConnectionManager<diesel::SqliteConnection>; type Manager = diesel::r2d2::ConnectionManager<diesel::SqliteConnection>;
type Error = r2d2::Error; type Error = r2d2::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 = diesel::r2d2::ConnectionManager::new(config.url); let manager = diesel::r2d2::ConnectionManager::new(&config.url);
r2d2::Pool::builder().max_size(config.pool_size).build(manager) 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 Manager = diesel::r2d2::ConnectionManager<diesel::PgConnection>;
type Error = r2d2::Error; type Error = r2d2::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 = diesel::r2d2::ConnectionManager::new(config.url); let manager = diesel::r2d2::ConnectionManager::new(&config.url);
r2d2::Pool::builder().max_size(config.pool_size).build(manager) 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 Manager = diesel::r2d2::ConnectionManager<diesel::MysqlConnection>;
type Error = r2d2::Error; type Error = r2d2::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 = diesel::r2d2::ConnectionManager::new(config.url); let manager = diesel::r2d2::ConnectionManager::new(&config.url);
r2d2::Pool::builder().max_size(config.pool_size).build(manager) 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 Manager = r2d2_postgres::PostgresConnectionManager<postgres::tls::NoTls>;
type Error = DbError<postgres::Error>; 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( let manager = r2d2_postgres::PostgresConnectionManager::new(
config.url.parse().map_err(DbError::Custom)?, config.url.parse().map_err(DbError::Custom)?,
postgres::tls::NoTls, postgres::tls::NoTls,
@ -750,8 +633,8 @@ impl Poolable for mysql::Conn {
type Manager = r2d2_mysql::MysqlConnectionManager; type Manager = r2d2_mysql::MysqlConnectionManager;
type Error = r2d2::Error; type Error = r2d2::Error;
fn pool(config: DatabaseConfig<'_>) -> Result<r2d2::Pool<Self::Manager>, Self::Error> { fn pool(config: &Config) -> Result<r2d2::Pool<Self::Manager>, Self::Error> {
let opts = mysql::OptsBuilder::from_opts(config.url); let opts = mysql::OptsBuilder::from_opts(&config.url);
let manager = r2d2_mysql::MysqlConnectionManager::new(opts); let manager = r2d2_mysql::MysqlConnectionManager::new(opts);
r2d2::Pool::builder().max_size(config.pool_size).build(manager) 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 Manager = r2d2_sqlite::SqliteConnectionManager;
type Error = r2d2::Error; type Error = r2d2::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_sqlite::SqliteConnectionManager::file(config.url); let manager = r2d2_sqlite::SqliteConnectionManager::file(&*config.url);
r2d2::Pool::builder().max_size(config.pool_size).build(manager) 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 Manager = r2d2_memcache::MemcacheConnectionManager;
type Error = DbError<memcache::MemcacheError>; type Error = DbError<memcache::MemcacheError>;
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_memcache::MemcacheConnectionManager::new(config.url); let manager = r2d2_memcache::MemcacheConnectionManager::new(&*config.url);
r2d2::Pool::builder().max_size(config.pool_size).build(manager).map_err(DbError::PoolError) 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. /// types are properly checked.
#[doc(hidden)] #[doc(hidden)]
pub struct ConnectionPool<K, C: Poolable> { pub struct ConnectionPool<K, C: Poolable> {
config: Config,
pool: r2d2::Pool<C::Manager>, pool: r2d2::Pool<C::Manager>,
semaphore: Arc<Semaphore>, semaphore: Arc<Semaphore>,
_marker: PhantomData<fn() -> K>, _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. /// Unstable internal details of generated code for the #[database] attribute.
/// ///
/// This type is implemented here instead of in generated code to ensure all /// 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> { 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 { AdHoc::on_attach(fairing_name, move |mut rocket| async move {
let config = database_config(config_name, rocket.config().await); let config = match Config::from(rocket.inspect().await, db_name) {
let pool = config.map(|c| (c.pool_size, C::pool(c))); Ok(config) => config,
Err(config_error) => {
rocket::error!("database configuration error for '{}'", db_name);
error_!("{}", config_error);
return Err(rocket);
}
};
match pool { match C::pool(&config) {
Ok((size, Ok(pool))) => { Ok(pool) => {
let pool_size = config.pool_size;
let managed = ConnectionPool::<K, C> { let managed = ConnectionPool::<K, C> {
pool, config, pool,
semaphore: Arc::new(Semaphore::new(size as usize)), semaphore: Arc::new(Semaphore::new(pool_size as usize)),
_marker: PhantomData, _marker: PhantomData,
}; };
Ok(rocket.manage(managed)) Ok(rocket.manage(managed))
}, },
Err(config_error) => { Err(pool_error) => {
rocket::logger::error( rocket::error!("failed to initialize pool for '{}'", db_name);
&format!("Database configuration failure: '{}'", config_name)); error_!("{:?}", pool_error);
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(rocket) Err(rocket)
}, },
} }
@ -847,28 +742,24 @@ impl<K: 'static, C: Poolable> ConnectionPool<K, C> {
} }
async fn get(&self) -> Result<Connection<K, C>, ()> { async fn get(&self) -> Result<Connection<K, C>, ()> {
// TODO: Make timeout configurable. let duration = std::time::Duration::from_secs(self.config.timeout as u64);
let permit = match tokio::time::timeout( let permit = match timeout(duration, self.semaphore.clone().acquire_owned()).await {
std::time::Duration::from_secs(5),
self.semaphore.clone().acquire_owned()
).await {
Ok(p) => p, Ok(p) => p,
Err(_) => { Err(_) => {
error_!("Failed to get a database connection within the timeout."); error_!("database connection retrieval timed out");
return Err(()); return Err(());
} }
}; };
// TODO: Make timeout configurable.
let pool = self.pool.clone(); 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 { Ok(c) => Ok(Connection {
connection: Arc::new(Mutex::new(Some(c))), connection: Arc::new(Mutex::new(Some(c))),
permit: Some(permit), permit: Some(permit),
_marker: PhantomData, _marker: PhantomData,
}), }),
Err(e) => { Err(e) => {
error_!("Failed to get a database connection: {}", e); error_!("failed to get a database connection: {}", e);
Err(()) Err(())
} }
} }
@ -878,11 +769,13 @@ impl<K: 'static, C: Poolable> ConnectionPool<K, C> {
pub async fn get_one(cargo: &rocket::Cargo) -> Option<Connection<K, C>> { pub async fn get_one(cargo: &rocket::Cargo) -> Option<Connection<K, C>> {
match cargo.state::<Self>() { match cargo.state::<Self>() {
Some(pool) => pool.get().await.ok(), Some(pool) => pool.get().await.ok(),
None => { None => None
error_!("Database fairing was not attached for {}", std::any::type_name::<K>());
None
} }
} }
#[inline]
pub async fn get_pool(cargo: &rocket::Cargo) -> Option<Self> {
cargo.state::<Self>().map(|pool| pool.clone())
} }
} }
@ -911,7 +804,8 @@ impl<K, C: Poolable> Drop for Connection<K, C> {
if let Some(conn) = connection.take() { if let Some(conn) = connection.take() {
drop(conn); 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. // released after the connection is.
drop(permit); 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) { fn on_launch(&self, cargo: &Cargo) {
if cargo.config().tls_enabled() if cargo.config().tls_enabled()
&& !cargo.config().environment.is_dev() && cargo.figment().profile() != rocket::Config::DEBUG_PROFILE
&& !self.is_enabled::<Hsts>() && !self.is_enabled::<Hsts>()
{ {
warn_!("Space Helmet: deploying with TLS without enabling 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 crate::templates::{DEFAULT_TEMPLATE_DIR, Context, Engines};
use rocket::Rocket; use rocket::Rocket;
use rocket::config::ConfigError;
use rocket::fairing::{Fairing, Info, Kind}; use rocket::fairing::{Fairing, Info, Kind};
pub(crate) use self::context::ContextManager; pub(crate) use self::context::ContextManager;
@ -152,19 +151,31 @@ impl Fairing for TemplateFairing {
/// template engines. In debug mode, the `ContextManager::new` method /// template engines. In debug mode, the `ContextManager::new` method
/// initializes a directory watcher for auto-reloading of templates. /// initializes a directory watcher for auto-reloading of templates.
async fn on_attach(&self, mut rocket: Rocket) -> Result<Rocket, Rocket> { async fn on_attach(&self, mut rocket: Rocket) -> Result<Rocket, Rocket> {
let config = rocket.config().await; use rocket::figment::{Source, value::magic::RelativePathBuf};
let mut template_root = config.root_relative(DEFAULT_TEMPLATE_DIR);
match config.get_str("template_dir") { let configured_dir = rocket.figment().await
Ok(dir) => template_root = config.root_relative(dir), .extract_inner::<RelativePathBuf>("template_dir")
Err(ConfigError::Missing(_)) => { /* ignore missing */ } .map(|path| path.relative());
let path = match configured_dir {
Ok(dir) => dir,
Err(e) if e.missing() => DEFAULT_TEMPLATE_DIR.into(),
Err(e) => { Err(e) => {
e.pretty_print(); rocket::config::pretty_print_error(e);
warn_!("Using default templates directory '{:?}'", template_root); return Err(rocket);
} }
}; };
match Context::initialize(template_root) { let root = Source::from(&*path);
match Context::initialize(path) {
Some(mut ctxt) => { 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); (self.custom_callback)(&mut ctxt.engines);
Ok(rocket.manage(ContextManager::new(ctxt))) Ok(rocket.manage(ContextManager::new(ctxt)))
} }

View File

@ -12,9 +12,8 @@ mod databases_tests {
#[cfg(all(feature = "databases", feature = "sqlite_pool"))] #[cfg(all(feature = "databases", feature = "sqlite_pool"))]
#[cfg(test)] #[cfg(test)]
mod rusqlite_integration_test { mod rusqlite_integration_test {
use rocket::config::{Config, Environment, Value, Map};
use rocket_contrib::databases::rusqlite;
use rocket_contrib::database; use rocket_contrib::database;
use rocket_contrib::databases::rusqlite;
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
@ -27,18 +26,19 @@ mod rusqlite_integration_test {
#[rocket::async_test] #[rocket::async_test]
async fn test_db() { async fn test_db() {
let mut test_db: Map<String, Value> = Map::new(); use rocket::figment::{Figment, util::map};
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();
let mut rocket = rocket::custom(config).attach(SqliteDb::fairing()).attach(SqliteDb2::fairing()); let options = map!["url" => ":memory:"];
let conn = SqliteDb::get_one(rocket.inspect().await).await.expect("unable to get connection"); 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 // Rusqlite's `transaction()` method takes `&mut self`; this tests that
// the &mut method can be called inside the closure passed to `run()`. // 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 std::path::{Path, PathBuf};
use rocket::{Rocket, http::RawStr}; use rocket::{Rocket, http::RawStr};
use rocket::config::{Config, Environment}; use rocket::config::Config;
use rocket_contrib::templates::{Template, Metadata}; use rocket_contrib::templates::{Template, Metadata};
#[get("/<engine>/<name>")] #[get("/<engine>/<name>")]
@ -27,11 +27,8 @@ mod templates_tests {
} }
fn rocket() -> Rocket { fn rocket() -> Rocket {
let config = Config::build(Environment::Development) rocket::custom(Config::figment().merge(("template_dir", template_root())))
.extra("template_dir", template_root().to_str().expect("template directory")) .attach(Template::fairing())
.expect("valid configuration");
rocket::custom(config).attach(Template::fairing())
.mount("/", routes![template_check, is_reloading]) .mount("/", routes![template_check, is_reloading])
} }

View File

@ -41,12 +41,12 @@ fn get0b(_n: u8) { }
#[test] #[test]
fn test_rank_collision() { fn test_rank_collision() {
use rocket::error::LaunchErrorKind; use rocket::error::ErrorKind;
let rocket = rocket::ignite().mount("/", routes![get0, get0b]); let rocket = rocket::ignite().mount("/", routes![get0, get0b]);
let client_result = Client::tracked(rocket); let client_result = Client::tracked(rocket);
match client_result.as_ref().map_err(|e| e.kind()) { 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"), Ok(_) => panic!("client succeeded unexpectedly"),
Err(e) => panic!("expected collision, got {}", e) 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>" = 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 --> $DIR/route-path-bad-syntax.rs:8:8
| |
8 | #[get("")] 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` = 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 --> $DIR/typed-uri-bad-type.rs:53:26
| |
53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); 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: = help: the following implementations were found:
<i32 as FromUriParam<P, &'x i32>> <i32 as FromUriParam<P, &'x i32>>
<i32 as FromUriParam<P, &'x mut i32>> <i32 as FromUriParam<P, &'x mut i32>>
<i32 as FromUriParam<P, 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` = 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 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")] 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>" --- help: expected path in origin form: "/path/<param>"
--> $DIR/route-path-bad-syntax.rs:8:7 --> $DIR/route-path-bad-syntax.rs:8:7
| |

View File

@ -22,7 +22,7 @@ private-cookies = ["cookie/private", "cookie/key-expansion"]
[dependencies] [dependencies]
smallvec = "1.0" smallvec = "1.0"
percent-encoding = "2" percent-encoding = "2"
hyper = { version = "0.13.0", default-features = false } hyper = { version = "0.13.0", default-features = false, features = ["runtime"] }
http = "0.2" http = "0.2"
mime = "0.3.13" mime = "0.3.13"
time = "0.2.11" time = "0.2.11"
@ -36,16 +36,13 @@ ref-cast = "1.0"
uncased = "0.9" uncased = "0.9"
parking_lot = "0.11" parking_lot = "0.11"
either = "1" either = "1"
pear = "0.2"
[dependencies.cookie] [dependencies.cookie]
git = "https://github.com/SergioBenitez/cookie-rs.git" git = "https://github.com/SergioBenitez/cookie-rs.git"
rev = "9675944" rev = "1c3ca83"
features = ["percent-encode"] features = ["percent-encode"]
[dependencies.pear]
git = "https://github.com/SergioBenitez/Pear.git"
rev = "4b68055"
[dev-dependencies] [dev-dependencies]
rocket = { version = "0.5.0-dev", path = "../lib" } rocket = { version = "0.5.0-dev", path = "../lib" }

View File

@ -20,9 +20,16 @@ mod key {
pub struct Key; pub struct Key;
impl Key { impl Key {
pub fn from(_: &[u8]) -> Self { Key }
pub fn derive_from(_: &[u8]) -> Self { Key }
pub fn generate() -> Self { Key } pub fn generate() -> Self { Key }
pub fn try_generate() -> Option<Self> { Some(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 /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # /// # #[cfg(feature = "private-cookies")] {
/// use rocket::http::Status; /// use rocket::http::Status;
/// use rocket::outcome::IntoOutcome; /// use rocket::outcome::IntoOutcome;
/// use rocket::request::{self, Request, FromRequest}; /// use rocket::request::{self, Request, FromRequest};
@ -141,6 +148,7 @@ mod key {
/// .or_forward(()) /// .or_forward(())
/// } /// }
/// } /// }
/// # }
/// # fn main() { } /// # fn main() { }
/// ``` /// ```
/// ///

View File

@ -10,8 +10,8 @@ use hyper::server::accept::Accept;
use log::{debug, error}; use log::{debug, error};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::Delay; use tokio::time::Delay;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
// TODO.async: 'Listener' and 'Connection' provide common enough functionality // TODO.async: 'Listener' and 'Connection' provide common enough functionality
@ -32,10 +32,10 @@ pub trait Connection: AsyncRead + AsyncWrite {
fn remote_addr(&self) -> Option<SocketAddr>; 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 /// 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 /// sockets. It does so by bridging the `Listener` trait to what hyper wants (an
/// (an Accept). This type is internal to Rocket. /// Accept). This type is internal to Rocket.
#[must_use = "streams do nothing unless polled"] #[must_use = "streams do nothing unless polled"]
pub struct Incoming<L> { pub struct Incoming<L> {
listener: L, listener: L,

View File

@ -1,70 +1,49 @@
use std::fs; use std::io;
use std::future::Future; use std::future::Future;
use std::io::{self, BufReader};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use rustls::internal::pemfile;
use rustls::{Certificate, PrivateKey, ServerConfig};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::{TlsAcceptor, Accept, server::TlsStream};
use tokio_rustls::{TlsAcceptor, server::TlsStream};
use tokio_rustls::rustls; use tokio_rustls::rustls;
pub use rustls::internal::pemfile;
pub use rustls::{Certificate, PrivateKey, ServerConfig};
use crate::listener::{Connection, Listener}; use crate::listener::{Connection, Listener};
#[derive(Debug)] fn load_certs(reader: &mut dyn io::BufRead) -> io::Result<Vec<Certificate>> {
pub enum Error { pemfile::certs(reader)
Io(io::Error), .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid certificate"))
BadCerts,
BadKeyCount,
BadKey,
} }
// TODO.async: consider using async fs operations fn load_private_key(reader: &mut dyn io::BufRead) -> io::Result<PrivateKey> {
pub fn load_certs<P: AsRef<Path>>(path: P) -> Result<Vec<rustls::Certificate>, Error> { use std::io::{Cursor, Error, Read, ErrorKind::Other};
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);
// "rsa" (PKCS1) PEM files have a different first-line header than PKCS8 // "rsa" (PKCS1) PEM files have a different first-line header than PKCS8
// PEM files, use that to determine the parse function to use. // PEM files, use that to determine the parse function to use.
let mut first_line = String::new(); let mut first_line = String::new();
reader.read_line(&mut first_line).map_err(Error::Io)?; reader.read_line(&mut first_line)?;
reader.seek(io::SeekFrom::Start(0)).map_err(Error::Io)?;
let private_keys_fn = match first_line.trim_end() { let private_keys_fn = match first_line.trim_end() {
"-----BEGIN RSA PRIVATE KEY-----" => pemfile::rsa_private_keys, "-----BEGIN RSA PRIVATE KEY-----" => pemfile::rsa_private_keys,
"-----BEGIN PRIVATE KEY-----" => pemfile::pkcs8_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) let key = private_keys_fn(&mut Cursor::new(first_line).chain(reader))
.map_err(|_| Error::BadKey) .map_err(|_| Error::new(Other, "invalid key file"))
.and_then(|mut keys| match keys.len() { .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)), 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. // Ensure we can use the key.
if rustls::sign::RSASigningKey::new(&key).is_err() { rustls::sign::RSASigningKey::new(&key)
Err(Error::BadKey) .map_err(|_| Error::new(Other, "key parsed but is unusable"))
} else { .map(|_| key)
Ok(key)
}
} }
pub struct TlsListener { pub struct TlsListener {
@ -75,7 +54,7 @@ pub struct TlsListener {
enum TlsListenerState { enum TlsListenerState {
Listening, Listening,
Accepting(Pin<Box<dyn Future<Output=Result<TlsStream<TcpStream>, io::Error>> + Send>>), Accepting(Accept<TcpStream>),
} }
impl Listener for TlsListener { impl Listener for TlsListener {
@ -85,22 +64,21 @@ impl Listener for TlsListener {
self.listener.local_addr().ok() 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 { loop {
match &mut self.state { match self.state {
TlsListenerState::Listening => { TlsListenerState::Listening => {
match self.listener.poll_accept(cx) { match self.listener.poll_accept(cx) {
Poll::Pending => return Poll::Pending, Poll::Pending => return Poll::Pending,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Ready(Ok((stream, _addr))) => { Poll::Ready(Ok((stream, _addr))) => {
self.state = TlsListenerState::Accepting(Box::pin(self.acceptor.accept(stream))); let fut = self.acceptor.accept(stream);
} self.state = TlsListenerState::Accepting(fut);
Poll::Ready(Err(e)) => {
return Poll::Ready(Err(e));
} }
} }
} }
TlsListenerState::Accepting(fut) => { TlsListenerState::Accepting(ref mut fut) => {
match fut.as_mut().poll(cx) { match Pin::new(fut).poll(cx) {
Poll::Pending => return Poll::Pending, Poll::Pending => return Poll::Pending,
Poll::Ready(result) => { Poll::Ready(result) => {
self.state = TlsListenerState::Listening; 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, address: SocketAddr,
cert_chain: Vec<Certificate>, mut cert_chain: C,
key: PrivateKey mut private_key: K,
) -> io::Result<TlsListener> { ) -> 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 listener = TcpListener::bind(address).await?;
let client_auth = rustls::NoClientAuth::new(); let client_auth = rustls::NoClientAuth::new();

View File

@ -19,7 +19,7 @@ edition = "2018"
all-features = true all-features = true
[features] [features]
default = ["secrets"] default = []
tls = ["rocket_http/tls"] tls = ["rocket_http/tls"]
secrets = ["rocket_http/private-cookies"] secrets = ["rocket_http/private-cookies"]
@ -29,7 +29,6 @@ rocket_http = { version = "0.5.0-dev", path = "../http" }
futures = "0.3.0" futures = "0.3.0"
yansi = "0.5" yansi = "0.5"
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
toml = "0.5"
num_cpus = "1.0" num_cpus = "1.0"
state = "0.4.1" state = "0.4.1"
time = "0.2.11" time = "0.2.11"
@ -39,12 +38,12 @@ atty = "0.2"
async-trait = "0.1" async-trait = "0.1"
ref-cast = "1.0" ref-cast = "1.0"
atomic = "0.5" atomic = "0.5"
ubyte = "0.10"
parking_lot = "0.11" parking_lot = "0.11"
ubyte = {version = "0.10", features = ["serde"] }
[dependencies.pear] serde = { version = "1.0", features = ["derive"] }
git = "https://github.com/SergioBenitez/Pear.git" figment = { version = "0.9.2", features = ["toml", "env"] }
rev = "4b68055" rand = "0.7"
either = "1"
[dependencies.tokio] [dependencies.tokio]
version = "0.2.9" version = "0.2.9"
@ -56,8 +55,7 @@ version_check = "0.9.1"
[dev-dependencies] [dev-dependencies]
bencher = "0.1" bencher = "0.1"
# TODO: Find a way to not depend on this. figment = { version = "0.9.2", features = ["test"] }
lazy_static = "1.0"
[[bench]] [[bench]]
name = "format-routing" name = "format-routing"

View File

@ -2,7 +2,6 @@
#[macro_use] extern crate bencher; #[macro_use] extern crate bencher;
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::config::{Environment, Config, LoggingLevel};
#[get("/", format = "application/json")] #[get("/", format = "application/json")]
fn get() -> &'static str { "get" } fn get() -> &'static str { "get" }
@ -11,8 +10,8 @@ fn get() -> &'static str { "get" }
fn post() -> &'static str { "post" } fn post() -> &'static str { "post" }
fn rocket() -> rocket::Rocket { fn rocket() -> rocket::Rocket {
let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); rocket::custom(rocket::Config::figment().merge(("log_level", "off")))
rocket::custom(config.unwrap()).mount("/", routes![get, post]) .mount("/", routes![get, post])
} }
use bencher::Bencher; use bencher::Bencher;

View File

@ -1,8 +1,6 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[macro_use] extern crate bencher; #[macro_use] extern crate bencher;
use rocket::config::{Environment, Config, LoggingLevel};
#[get("/", format = "application/json", rank = 1)] #[get("/", format = "application/json", rank = 1)]
fn get() -> &'static str { "json" } fn get() -> &'static str { "json" }
@ -22,8 +20,7 @@ fn post2() -> &'static str { "html" }
fn post3() -> &'static str { "plain" } fn post3() -> &'static str { "plain" }
fn rocket() -> rocket::Rocket { fn rocket() -> rocket::Rocket {
let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); rocket::custom(rocket::Config::figment().merge(("log_level", "off")))
rocket::custom(config.unwrap())
.mount("/", routes![get, get2, get3]) .mount("/", routes![get, get2, get3])
.mount("/", routes![post, post2, post3]) .mount("/", routes![post, post2, post3])
} }

View File

@ -1,15 +1,11 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[macro_use] extern crate bencher; #[macro_use] extern crate bencher;
use rocket::config::{Environment, Config, LoggingLevel};
use rocket::http::RawStr; use rocket::http::RawStr;
#[get("/")] #[get("/")]
fn hello_world() -> &'static str { "Hello, world!" } fn hello_world() -> &'static str { "Hello, world!" }
#[get("/")]
fn get_index() -> &'static str { "index" }
#[put("/")] #[put("/")]
fn put_index() -> &'static str { "index" } 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 index_dyn_a(_a: &RawStr) -> &'static str { "index" }
fn hello_world_rocket() -> rocket::Rocket { fn hello_world_rocket() -> rocket::Rocket {
let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); let config = rocket::Config::figment().merge(("log_level", "off"));
rocket::custom(config.unwrap()).mount("/", routes![hello_world]) rocket::custom(config).mount("/", routes![hello_world])
} }
fn rocket() -> rocket::Rocket { fn rocket() -> rocket::Rocket {
let config = Config::build(Environment::Production).log_level(LoggingLevel::Off); hello_world_rocket()
rocket::custom(config.unwrap()) .mount("/", routes![
.mount("/", routes![get_index, put_index, post_index, index_a, put_index, post_index, index_a, index_b, index_c, index_dyn_a
index_b, index_c, index_dyn_a]) ])
} }
use bencher::Bencher; 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::outcome::Outcome::*;
use crate::http::Status; use crate::http::Status;
use crate::request::Request; use crate::request::Request;
use crate::data::{Data, ByteUnit}; use crate::data::Data;
/// Type alias for the `Outcome` of a `FromTransformedData` conversion. /// Type alias for the `Outcome` of a `FromTransformedData` conversion.
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Data>; 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. /// transformations.
/// ///
/// When transformation of incoming data isn't required, data guards should /// 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. /// // 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, /// Ok(string) => string,
/// Err(e) => return Outcome::Failure((Status::InternalServerError, format!("{}", e))) /// 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)] #[cfg(debug_assertions)]
use crate::data::ByteUnit;
#[crate::async_trait] #[crate::async_trait]
#[cfg(debug_assertions)]
impl FromData for String { impl FromData for String {
type Error = std::io::Error; type Error = std::io::Error;
@ -615,8 +619,8 @@ impl FromData for String {
} }
} }
#[cfg(debug_assertions)]
#[crate::async_trait] #[crate::async_trait]
#[cfg(debug_assertions)]
impl FromData for Vec<u8> { impl FromData for Vec<u8> {
type Error = std::io::Error; type Error = std::io::Error;

View File

@ -1,8 +1,11 @@
use std::fmt; use std::fmt;
use serde::{Serialize, Deserialize};
use crate::request::{Request, FromRequest, Outcome};
use crate::data::{ByteUnit, ToByteUnit}; 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", /// 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 /// "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 /// # Defaults
/// ///
/// As documented in [`config`](crate::config), the default limits are as follows: /// The default limits are:
/// ///
/// * **forms**: 32KiB /// * **forms**: 32KiB
/// ///
@ -24,38 +27,82 @@ use crate::data::{ByteUnit, ToByteUnit};
/// use rocket::data::{Limits, ToByteUnit}; /// use rocket::data::{Limits, ToByteUnit};
/// ///
/// // Set a limit of 64KiB for forms and 3MiB for JSON. /// // Set a limit of 64KiB for forms and 3MiB for JSON.
/// let limits = Limits::new() /// let limits = Limits::default()
/// .limit("forms", 64.kibibytes()) /// .limit("forms", 64.kibibytes())
/// .limit("json", 3.mebibytes()); /// .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 { pub struct Limits {
// We cache this internally but don't share that fact in the API. // We cache this internally but don't share that fact in the API.
pub(crate) forms: ByteUnit, #[serde(with = "figment::util::vec_tuple_map")]
extra: Vec<(String, ByteUnit)> limits: Vec<(String, ByteUnit)>
} }
/// The default limits are:
///
/// * **forms**: 32KiB
impl Default for Limits { impl Default for Limits {
fn default() -> Limits { fn default() -> Limits {
// Default limit for forms is 32KiB. // Default limit for forms is 32KiB.
Limits { forms: 32.kibibytes(), extra: Vec::new() } Limits { limits: vec![("forms".into(), 32.kibibytes())] }
} }
} }
impl Limits { impl Limits {
/// Construct a new `Limits` structure with the default limits set. /// Construct a new `Limits` structure with no limits set.
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use rocket::data::{Limits, ToByteUnit}; /// use rocket::data::{Limits, ToByteUnit};
/// ///
/// let limits = Limits::new(); /// let limits = Limits::default();
/// assert_eq!(limits.get("forms"), Some(32.kibibytes())); /// assert_eq!(limits.get("forms"), Some(32.kibibytes()));
///
/// let limits = Limits::new();
/// assert_eq!(limits.get("forms"), None);
/// ``` /// ```
#[inline] #[inline]
pub fn new() -> Self { pub fn new() -> Self {
Limits::default() Limits { limits: vec![] }
} }
/// Adds or replaces a limit in `self`, consuming `self` and returning a new /// Adds or replaces a limit in `self`, consuming `self` and returning a new
@ -66,7 +113,7 @@ impl Limits {
/// ```rust /// ```rust
/// use rocket::data::{Limits, ToByteUnit}; /// 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("forms"), Some(32.kibibytes()));
/// assert_eq!(limits.get("json"), Some(1.mebibytes())); /// 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 { pub fn limit<S: Into<String>>(mut self, name: S, limit: ByteUnit) -> Self {
let name = name.into(); let name = name.into();
match name.as_str() { match self.limits.iter_mut().find(|(k, _)| *k == name) {
"forms" => self.forms = limit, Some((_, v)) => *v = limit,
_ => { None => self.limits.push((name, 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))
}
}
} }
self.limits.sort_by(|a, b| a.0.cmp(&b.0));
self self
} }
@ -104,35 +139,35 @@ impl Limits {
/// ```rust /// ```rust
/// use rocket::data::{Limits, ToByteUnit}; /// 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("forms"), Some(32.kibibytes()));
/// assert_eq!(limits.get("json"), Some(64.mebibytes())); /// assert_eq!(limits.get("json"), Some(64.mebibytes()));
/// assert!(limits.get("msgpack").is_none()); /// assert!(limits.get("msgpack").is_none());
/// ``` /// ```
pub fn get(&self, name: &str) -> Option<ByteUnit> { pub fn get(&self, name: &str) -> Option<ByteUnit> {
if name == "forms" { self.limits.iter()
return Some(self.forms); .find(|(k, _)| *k == name)
} .map(|(_, v)| *v)
for &(ref key, val) in &self.extra {
if key == name {
return Some(val);
}
}
None
} }
} }
impl fmt::Display for Limits { impl fmt::Display for Limits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "forms = {}", self.forms)?; for (i, (k, v)) in self.limits.iter().enumerate() {
for (key, val) in &self.extra { if i != 0 { f.write_str(", ")? }
write!(f, ", {}* = {}", key, val)?; write!(f, "{} = {}", k, v)?;
} }
Ok(()) 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; 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. /// An error that occurs during launch.
/// ///
/// A `LaunchError` is returned by [`launch()`](crate::Rocket::launch()) when /// An `Error` is returned by [`launch()`](crate::Rocket::launch()) when
/// launching an application fails. /// launching an application fails or, more rarely, when the runtime fails after
/// lauching.
/// ///
/// # Panics /// # Panics
/// ///
/// A value of this type panics if it is dropped without first being inspected. /// 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 /// An _inspection_ occurs when any method is called. For instance, if
/// `println!("Error: {}", e)` is called, where `e: LaunchError`, the /// `println!("Error: {}", e)` is called, where `e: Error`, the `Display::fmt`
/// `Display::fmt` method being called by `println!` results in `e` being marked /// method being called by `println!` results in `e` being marked as inspected;
/// as inspected; a subsequent `drop` of the value will _not_ result in a panic. /// a subsequent `drop` of the value will _not_ result in a panic. The following
/// The following snippet illustrates this: /// snippet illustrates this:
/// ///
/// ```rust /// ```rust
/// use rocket::error::Error;
///
/// # let _ = async { /// # let _ = async {
/// if let Err(error) = rocket::ignite().launch().await { /// if let Err(error) = rocket::ignite().launch().await {
/// match error { /// // This println "inspects" the error.
/// Error::Launch(error) => {
/// // This case is only reached if launching failed. This println "inspects" the error.
/// println!("Launch failed! Error: {}", error); /// println!("Launch failed! Error: {}", error);
/// ///
/// // This call to drop (explicit here for demonstration) will do nothing. /// // This call to drop (explicit here for demonstration) will do nothing.
/// drop(error); /// 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);
/// }
/// }
/// }
///
/// # }; /// # };
/// ``` /// ```
/// ///
@ -100,8 +49,8 @@ pub enum LaunchErrorKind {
/// ///
/// # Usage /// # Usage
/// ///
/// A `LaunchError` value should usually be allowed to `drop` without /// An `Error` value should usually be allowed to `drop` without inspection.
/// inspection. There are two exceptions to this suggestion. /// There are at least two exceptions:
/// ///
/// 1. If you are writing a library or high-level application on-top of /// 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 /// Rocket, you likely want to inspect the value before it drops to avoid a
@ -109,15 +58,42 @@ pub enum LaunchErrorKind {
/// value. /// value.
/// ///
/// 2. You want to display your own error messages. /// 2. You want to display your own error messages.
pub struct LaunchError { pub struct Error {
handled: AtomicBool, 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)] #[inline(always)]
pub(crate) fn new(kind: LaunchErrorKind) -> LaunchError { pub(crate) fn new(kind: ErrorKind) -> Error {
LaunchError { handled: AtomicBool::new(false), kind } Error { handled: AtomicBool::new(false), kind }
} }
#[inline(always)] #[inline(always)]
@ -135,43 +111,38 @@ impl LaunchError {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use rocket::error::Error; /// use rocket::error::ErrorKind;
///
/// # let _ = async { /// # let _ = async {
/// if let Err(error) = rocket::ignite().launch().await { /// if let Err(error) = rocket::ignite().launch().await {
/// match error { /// match error.kind() {
/// Error::Launch(err) => println!("Found a launch error: {}", err.kind()), /// ErrorKind::Io(e) => println!("found an i/o launch error: {}", e),
/// Error::Run(err) => println!("Error at runtime"), /// e => println!("something else happened: {}", e)
/// } /// }
/// } /// }
/// # }; /// # };
/// ``` /// ```
#[inline] #[inline]
pub fn kind(&self) -> &LaunchErrorKind { pub fn kind(&self) -> &ErrorKind {
self.mark_handled(); self.mark_handled();
&self.kind &self.kind
} }
} }
impl From<io::Error> for LaunchError { impl fmt::Display for ErrorKind {
#[inline]
fn from(error: io::Error) -> LaunchError {
LaunchError::new(LaunchErrorKind::Io(error))
}
}
impl fmt::Display for LaunchErrorKind {
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { match self {
LaunchErrorKind::Bind(ref e) => write!(f, "binding failed: {}", e), ErrorKind::Bind(e) => write!(f, "binding failed: {}", e),
LaunchErrorKind::Io(ref e) => write!(f, "I/O error: {}", e), ErrorKind::Io(e) => write!(f, "I/O error: {}", e),
LaunchErrorKind::Collision(_) => write!(f, "route collisions detected"), ErrorKind::Collision(_) => write!(f, "route collisions detected"),
LaunchErrorKind::FailedFairings(_) => write!(f, "a launch fairing failed"), 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] #[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.mark_handled(); self.mark_handled();
@ -179,7 +150,7 @@ impl fmt::Debug for LaunchError {
} }
} }
impl fmt::Display for LaunchError { impl fmt::Display for Error {
#[inline] #[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.mark_handled(); self.mark_handled();
@ -187,22 +158,24 @@ impl fmt::Display for LaunchError {
} }
} }
impl Drop for LaunchError { impl Drop for Error {
fn drop(&mut self) { fn drop(&mut self) {
if self.was_handled() { if self.was_handled() {
return return
} }
match *self.kind() { match *self.kind() {
LaunchErrorKind::Bind(ref e) => { ErrorKind::Bind(ref e) => {
error!("Rocket failed to bind network socket to given address/port."); 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."); 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:"); error!("Rocket failed to launch due to the following routing collisions:");
for &(ref a, ref b) in collisions { for &(ref a, ref b) in collisions {
info_!("{} {} {}", a, Paint::red("collides with").italic(), b) 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."); info_!("Note: Collisions can usually be resolved by ranking routes.");
panic!("route collisions detected"); panic!("route collisions detected");
} }
LaunchErrorKind::FailedFairings(ref failures) => { ErrorKind::FailedFairings(ref failures) => {
error!("Rocket failed to launch due to failing fairings:"); error!("Rocket failed to launch due to failing fairings:");
for fairing in failures { for fairing in failures {
info_!("{}", fairing); 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,8 +92,7 @@ impl AdHoc {
/// let fairing = AdHoc::on_attach("No-Op", |rocket| async { Ok(rocket) }); /// let fairing = AdHoc::on_attach("No-Op", |rocket| async { Ok(rocket) });
/// ``` /// ```
pub fn on_attach<F, Fut>(name: &'static str, f: F) -> AdHoc pub fn on_attach<F, Fut>(name: &'static str, f: F) -> AdHoc
where where F: FnOnce(Rocket) -> Fut + Send + 'static,
F: FnOnce(Rocket) -> Fut + Send + 'static,
Fut: Future<Output=Result<Rocket, Rocket>> + Send + 'static, Fut: Future<Output=Result<Rocket, Rocket>> + Send + 'static,
{ {
AdHoc { AdHoc {
@ -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 /// Constructs an `AdHoc` launch fairing named `name`. The function `f` will
/// be called by Rocket just prior to launching. /// be called by Rocket just prior to launching.
/// ///

View File

@ -55,24 +55,27 @@
//! } //! }
//! //!
//! #[launch] //! #[launch]
//! fn rocket() -> rocket::Rocket { //! fn rocket() -> _ {
//! rocket::ignite().mount("/", routes![hello]) //! rocket::ignite().mount("/", routes![hello])
//! } //! }
//! ``` //! ```
//! //!
//! ## Features //! ## Features
//! //!
//! The `secrets` feature, which enables [private cookies], is enabled by //! There are two optional, disabled-by-default features:
//! default. This necessitates pulling in additional dependencies. To avoid //!
//! these dependencies when your application does not use private cookies, //! * **secrets:** Enables support for [private cookies].
//! disable the `secrets` feature: //! * **tls:** Enables support for [TLS].
//!
//! The features can be enabled in `Cargo.toml`:
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [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 //! ## Configuration
//! //!
@ -98,10 +101,13 @@ pub use async_trait::*;
#[macro_use] extern crate log; #[macro_use] extern crate log;
/// These are public dependencies! Update docs if these are changed, especially
/// figment's version number in docs.
#[doc(hidden)] #[doc(hidden)]
pub use yansi; pub use yansi;
pub use futures; pub use futures;
pub use tokio; pub use tokio;
pub use figment;
#[doc(hidden)] #[macro_use] pub mod logger; #[doc(hidden)] #[macro_use] pub mod logger;
#[macro_use] pub mod outcome; #[macro_use] pub mod outcome;
@ -132,6 +138,7 @@ mod rocket;
mod codegen; mod codegen;
mod ext; mod ext;
#[doc(hidden)] pub use log::{info, warn, error, debug};
#[doc(inline)] pub use crate::response::Response; #[doc(inline)] pub use crate::response::Response;
#[doc(hidden)] pub use crate::codegen::{StaticRouteInfo, StaticCatcherInfo}; #[doc(hidden)] pub use crate::codegen::{StaticRouteInfo, StaticCatcherInfo};
#[doc(inline)] pub use crate::data::Data; #[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 /// Alias to [`Rocket::custom()`]. Creates a new instance of `Rocket` with a
/// custom configuration. /// custom configuration provider.
pub fn custom(config: Config) -> Rocket { pub fn custom<T: figment::Provider>(provider: T) -> Rocket {
Rocket::custom(config) Rocket::custom(provider)
} }
// TODO.async: More thoughtful plan for async tests // 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 { pub fn async_main<R>(fut: impl std::future::Future<Output = R> + Send) -> R {
tokio::runtime::Builder::new() tokio::runtime::Builder::new()
.threaded_scheduler() .threaded_scheduler()
.thread_name("rocket-worker-thread")
.enable_all() .enable_all()
.build() .build()
.expect("create tokio runtime") .expect("create tokio runtime")

View File

@ -5,7 +5,7 @@ use parking_lot::RwLock;
use crate::local::asynchronous::{LocalRequest, LocalResponse}; use crate::local::asynchronous::{LocalRequest, LocalResponse};
use crate::rocket::{Rocket, Cargo}; use crate::rocket::{Rocket, Cargo};
use crate::http::{private::cookie, Method}; use crate::http::{private::cookie, Method};
use crate::error::LaunchError; use crate::error::Error;
/// An `async` client to construct and dispatch local requests. /// An `async` client to construct and dispatch local requests.
/// ///
@ -57,7 +57,7 @@ impl Client {
pub(crate) async fn _new( pub(crate) async fn _new(
mut rocket: Rocket, mut rocket: Rocket,
tracked: bool tracked: bool
) -> Result<Client, LaunchError> { ) -> Result<Client, Error> {
rocket.prelaunch_check().await?; rocket.prelaunch_check().await?;
let cargo = rocket.into_cargo().await; let cargo = rocket.into_cargo().await;
let cookies = RwLock::new(cookie::CookieJar::new()); let cookies = RwLock::new(cookie::CookieJar::new());

View File

@ -89,7 +89,7 @@ impl<'c> LocalResponse<'c> {
async move { async move {
let response: Response<'c> = f(request).await; 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() { for cookie in response.cookies() {
cookies.add_original(cookie.into_owned()); cookies.add_original(cookie.into_owned());
} }

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use crate::error::LaunchError; use crate::error::Error;
use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}}; use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}};
use crate::rocket::{Rocket, Cargo}; use crate::rocket::{Rocket, Cargo};
use crate::http::Method; use crate::http::Method;
@ -31,7 +31,7 @@ pub struct Client {
} }
impl 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() let mut runtime = tokio::runtime::Builder::new()
.basic_scheduler() .basic_scheduler()
.enable_all() .enable_all()

View File

@ -58,7 +58,7 @@ macro_rules! pub_client_impl {
/// # Errors /// # Errors
/// ///
/// If launching the `Rocket` instance would fail, excepting network errors, /// If launching the `Rocket` instance would fail, excepting network errors,
/// the `LaunchError` is returned. /// the `Error` is returned.
/// ///
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
@ -67,7 +67,7 @@ macro_rules! pub_client_impl {
/// let client = Client::tracked(rocket); /// let client = Client::tracked(rocket);
/// ``` /// ```
#[inline(always)] #[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)? Self::_new(rocket, true) $(.$suffix)?
} }
@ -83,7 +83,7 @@ macro_rules! pub_client_impl {
/// # Errors /// # Errors
/// ///
/// If launching the `Rocket` instance would fail, excepting network /// If launching the `Rocket` instance would fail, excepting network
/// errors, the `LaunchError` is returned. /// errors, the `Error` is returned.
/// ///
/// ```rust,no_run /// ```rust,no_run
#[doc = $import] #[doc = $import]
@ -91,7 +91,7 @@ macro_rules! pub_client_impl {
/// let rocket = rocket::ignite(); /// let rocket = rocket::ignite();
/// let client = Client::untracked(rocket); /// 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)? Self::_new(rocket, false) $(.$suffix)?
} }
@ -100,7 +100,7 @@ macro_rules! pub_client_impl {
since = "0.5", since = "0.5",
note = "choose between `Client::untracked()` and `Client::tracked()`" 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)? Self::tracked(rocket) $(.$suffix)?
} }
@ -159,7 +159,7 @@ macro_rules! pub_client_impl {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn cookies(&self) -> crate::http::CookieJar<'_> { 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()); let jar = self._with_raw_cookies(|jar| jar.clone());
crate::http::CookieJar::from(jar, key) crate::http::CookieJar::from(jar, key)
} }

View File

@ -1,49 +1,58 @@
//! Rocket's logging infrastructure. //! Rocket's logging infrastructure.
use std::{fmt, env}; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use log; use log;
use yansi::Paint; 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 maximum level of log messages to show.
/// Defines the different levels for log messages.
#[derive(PartialEq, Eq, Debug, Clone, Copy)] #[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum LoggingLevel { pub enum LogLevel {
/// Only shows errors, warnings, and launch information. /// Only shows errors and warnings: `"critical"`.
Critical, Critical,
/// Shows everything except debug and trace information. /// Shows everything except debug and trace information: `"normal"`.
Normal, Normal,
/// Shows everything. /// Shows everything: `"debug"`.
Debug, Debug,
/// Shows nothing. /// Shows nothing: "`"off"`".
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)] #[inline(always)]
fn to_level_filter(self) -> log::LevelFilter { fn to_level_filter(self) -> log::LevelFilter {
match self { match self {
LoggingLevel::Critical => log::LevelFilter::Warn, LogLevel::Critical => log::LevelFilter::Warn,
LoggingLevel::Normal => log::LevelFilter::Info, LogLevel::Normal => log::LevelFilter::Info,
LoggingLevel::Debug => log::LevelFilter::Trace, LogLevel::Debug => log::LevelFilter::Trace,
LoggingLevel::Off => log::LevelFilter::Off LogLevel::Off => log::LevelFilter::Off
} }
} }
} }
impl FromStr for LoggingLevel { impl FromStr for LogLevel {
type Err = &'static str; type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let level = match s { let level = match &*s.to_ascii_lowercase() {
"critical" => LoggingLevel::Critical, "critical" => LogLevel::Critical,
"normal" => LoggingLevel::Normal, "normal" => LogLevel::Normal,
"debug" => LoggingLevel::Debug, "debug" => LogLevel::Debug,
"off" => LoggingLevel::Off, "off" => LogLevel::Off,
_ => return Err("a log level (off, debug, normal, critical)") _ => 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string = match *self { write!(f, "{}", self.as_str())
LoggingLevel::Critical => "critical", }
LoggingLevel::Normal => "normal", }
LoggingLevel::Debug => "debug",
LoggingLevel::Off => "off"
};
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 configged_level = self.0;
let from_hyper = record.module_path().map_or(false, |m| m.starts_with("hyper::")); 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::")); 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; return;
} }
// In Rocket, we abuse targets with suffix "_" to indicate indentation. // In Rocket, we abuse targets with suffix "_" to indicate indentation.
let is_launch = record.target().starts_with("launch"); let is_launch = record.target().starts_with("launch");
if record.target().ends_with('_') { if record.target().ends_with('_') {
if configged_level != LoggingLevel::Critical || is_launch { if configged_level != LogLevel::Critical || is_launch {
print!(" {} ", Paint::default("=>").bold()); print!(" {} ", Paint::default("=>").bold());
} }
} }
@ -145,89 +163,45 @@ impl log::Log for RocketLogger {
} }
} }
pub(crate) fn try_init(level: LoggingLevel, verbose: bool) -> bool { pub(crate) fn try_init(level: LogLevel, colors: bool, verbose: bool) -> bool {
if level == LoggingLevel::Off { if level == LogLevel::Off {
return false; return false;
} }
if !atty::is(atty::Stream::Stdout) if !atty::is(atty::Stream::Stdout)
|| (cfg!(windows) && !Paint::enable_windows_ascii()) || (cfg!(windows) && !Paint::enable_windows_ascii())
|| env::var_os(COLORS_ENV).map(|v| v == "0" || v == "off").unwrap_or(false) || !colors
{ {
Paint::disable(); Paint::disable();
} }
push_max_level(level);
if let Err(e) = log::set_boxed_logger(Box::new(RocketLogger(level))) { if let Err(e) = log::set_boxed_logger(Box::new(RocketLogger(level))) {
if verbose { if verbose {
eprintln!("Logger failed to initialize: {}", e); eprintln!("Logger failed to initialize: {}", e);
} }
pop_max_level();
return false; return false;
} }
log::set_max_level(level.to_level_filter());
true true
} }
use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering}; pub trait PaintExt {
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 {
fn emoji(item: &str) -> Paint<&str>; fn emoji(item: &str) -> Paint<&str>;
} }
impl PaintExt for Paint<&str> { impl PaintExt for Paint<&str> {
/// Paint::masked(), but hidden on Windows due to broken output. See #1122. /// Paint::masked(), but hidden on Windows due to broken output. See #1122.
fn emoji(item: &str) -> Paint<&str> { fn emoji(_item: &str) -> Paint<&str> {
if cfg!(windows) { #[cfg(windows)] { Paint::masked("") }
Paint::masked("") #[cfg(not(windows))] { Paint::masked(_item) }
} else {
Paint::masked(item)
}
} }
} }
#[doc(hidden)] #[doc(hidden)]
pub fn init(level: LoggingLevel) -> bool { pub fn init(level: LogLevel) -> bool {
try_init(level, true) try_init(level, true, true)
} }
// Expose logging macros as (hidden) funcions for use by core/contrib codegen. // 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::outcome::Outcome::*;
use crate::request::{Request, form::{FromForm, FormItems, FormDataError}}; 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}}; use crate::http::{Status, uri::{Query, FromUriParam}};
/// A data guard for parsing [`FromForm`] types strictly. /// A data guard for parsing [`FromForm`] types strictly.
/// ///
/// This type implements the [`FromTransformedData`] trait. It provides a generic means to /// This type implements the [`FromTransformedData`] trait. It provides a
/// parse arbitrary structures from incoming form data. /// generic means to parse arbitrary structures from incoming form data.
/// ///
/// # Strictness /// # Strictness
/// ///
@ -197,7 +198,8 @@ impl<'f, T: FromForm<'f> + Send + 'f> FromTransformedData<'f> for Form<T> {
return Transform::Borrowed(Forward(data)); 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)), Ok(form_string) => Transform::Borrowed(Success(form_string)),
Err(e) => { Err(e) => {
let err = (Status::InternalServerError, FormDataError::Io(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> { fn from_data(
Box::pin(futures::future::ready(o.borrowed().and_then(|data| { _: &'f Request<'_>,
<Form<T>>::from_data(data, true).map(Form) 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, managed: &rocket.managed_state,
shutdown: &rocket.shutdown_handle, shutdown: &rocket.shutdown_handle,
route: Atomic::new(None), route: Atomic::new(None),
cookies: CookieJar::new(rocket.config.secret_key()), cookies: CookieJar::new(&rocket.config.secret_key),
accept: Storage::new(), accept: Storage::new(),
content_type: Storage::new(), content_type: Storage::new(),
cache: Arc::new(Container::new()), cache: Arc::new(Container::new()),
@ -749,7 +749,7 @@ impl<'r> Request<'r> {
impl<'r> Request<'r> { impl<'r> Request<'r> {
// Only used by doc-tests! Needs to be `pub` because doc-test are external. // 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) { 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 uri = Origin::parse(uri).expect("invalid URI in example");
let mut request = Request::new(&rocket, method, uri); let mut request = Request::new(&rocket, method, uri);
f(&mut request); f(&mut request);

View File

@ -20,8 +20,7 @@ macro_rules! assert_headers {
$(expected.entry($key).or_insert(vec![]).append(&mut vec![$($value),+]);)+ $(expected.entry($key).or_insert(vec![]).append(&mut vec![$($value),+]);)+
// Dispatch the request and check that the headers are what we expect. // Dispatch the request and check that the headers are what we expect.
let config = Config::development(); let r = Rocket::custom(Config::default());
let r = Rocket::custom(config);
let req = Request::from_hyp(&r, h_method, h_headers, &h_uri, h_addr).unwrap(); let req = Request::from_hyp(&r, h_method, h_headers, &h_uri, h_addr).unwrap();
let actual_headers = req.headers(); let actual_headers = req.headers();
for (key, values) in expected.iter() { for (key, values) in expected.iter() {

View File

@ -11,16 +11,17 @@ use ref_cast::RefCast;
use yansi::Paint; use yansi::Paint;
use state::Container; use state::Container;
use figment::Figment;
use crate::{logger, handler}; use crate::{logger, handler};
use crate::config::{Config, FullConfig, ConfigError, LoggedValue}; use crate::config::Config;
use crate::request::{Request, FormItems}; use crate::request::{Request, FormItems};
use crate::data::Data; use crate::data::Data;
use crate::catcher::Catcher; use crate::catcher::Catcher;
use crate::response::{Body, Response}; use crate::response::{Body, Response};
use crate::router::{Router, Route}; use crate::router::{Router, Route};
use crate::outcome::Outcome; use crate::outcome::Outcome;
use crate::error::{LaunchError, LaunchErrorKind}; use crate::error::{Error, ErrorKind};
use crate::fairing::{Fairing, Fairings}; use crate::fairing::{Fairing, Fairings};
use crate::logger::PaintExt; use crate::logger::PaintExt;
use crate::ext::AsyncReadExt; use crate::ext::AsyncReadExt;
@ -35,6 +36,7 @@ use crate::http::uri::Origin;
/// application. /// application.
pub struct Rocket { pub struct Rocket {
pub(crate) config: Config, pub(crate) config: Config,
pub(crate) figment: Figment,
pub(crate) managed_state: Container, pub(crate) managed_state: Container,
manifest: Vec<PreLaunchOp>, manifest: Vec<PreLaunchOp>,
router: Router, router: Router,
@ -66,6 +68,9 @@ pub(crate) struct Token;
impl Rocket { impl Rocket {
#[inline] #[inline]
fn _mount(&mut self, base: Origin<'static>, routes: Vec<Route>) { 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!("{}{} {}{}", info!("{}{} {}{}",
Paint::emoji("🛰 "), Paint::emoji("🛰 "),
Paint::magenta("Mounting"), Paint::magenta("Mounting"),
@ -107,6 +112,7 @@ impl Rocket {
#[inline] #[inline]
async fn _attach(mut self, fairing: Box<dyn Fairing>) -> Self { async fn _attach(mut self, fairing: Box<dyn Fairing>) -> Self {
// Attach (and run attach-) fairings, which requires us to move `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()); let mut fairings = mem::replace(&mut self.fairings, Fairings::new());
self = fairings.attach(fairing, self).await; self = fairings.attach(fairing, self).await;
@ -119,8 +125,9 @@ impl Rocket {
// Create a "dummy" instance of `Rocket` to use while mem-swapping `self`. // Create a "dummy" instance of `Rocket` to use while mem-swapping `self`.
fn dummy() -> Rocket { fn dummy() -> Rocket {
Rocket { Rocket {
config: Config::debug_default(),
figment: Figment::from(Config::debug_default()),
manifest: vec![], manifest: vec![],
config: Config::development(),
router: Router::new(), router: Router::new(),
default_catcher: None, default_catcher: None,
catchers: HashMap::new(), catchers: HashMap::new(),
@ -145,7 +152,7 @@ impl Rocket {
// process them as a stack to maintain proper ordering. // process them as a stack to maintain proper ordering.
let mut manifest = mem::replace(&mut self.manifest, vec![]); let mut manifest = mem::replace(&mut self.manifest, vec![]);
while !manifest.is_empty() { while !manifest.is_empty() {
trace_!("[MANIEST PROGRESS]: {:?}", manifest); trace_!("[MANIEST PROGRESS ({} left)]: {:?}", manifest.len(), manifest);
match manifest.remove(0) { match manifest.remove(0) {
PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state), PreLaunchOp::Manage(_, callback) => callback(&mut self.managed_state),
PreLaunchOp::Mount(base, routes) => self._mount(base, routes), PreLaunchOp::Mount(base, routes) => self._mount(base, routes),
@ -183,10 +190,9 @@ async fn hyper_service_fn(
h_addr: std::net::SocketAddr, h_addr: std::net::SocketAddr,
hyp_req: hyper::Request<hyper::Body>, hyp_req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, io::Error> { ) -> Result<hyper::Response<hyper::Body>, io::Error> {
// This future must return a hyper::Response, but that's not easy // This future must return a hyper::Response, but the response body might
// because the response body might borrow from the request. Instead, // borrow from the request. Instead, write the body in another future that
// we do the body writing in another future that will send us // sends the response metadata (and a body channel) prior.
// the response metadata (and a body channel) beforehand.
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
tokio::spawn(async move { tokio::spawn(async move {
@ -194,7 +200,10 @@ async fn hyper_service_fn(
let (h_parts, h_body) = hyp_req.into_parts(); let (h_parts, h_body) = hyp_req.into_parts();
// Convert the Hyper request into a Rocket request. // 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 { let mut req = match req_res {
Ok(req) => req, Ok(req) => req,
Err(e) => { Err(e) => {
@ -218,6 +227,7 @@ async fn hyper_service_fn(
rocket.issue_response(r, tx).await; 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)) rx.await.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
} }
@ -228,14 +238,9 @@ impl Rocket {
response: Response<'_>, response: Response<'_>,
tx: oneshot::Sender<hyper::Response<hyper::Body>>, tx: oneshot::Sender<hyper::Response<hyper::Body>>,
) { ) {
let result = self.write_response(response, tx); match self.write_response(response, tx).await {
match result.await { Ok(()) => info_!("{}", Paint::green("Response succeeded.")),
Ok(()) => { Err(e) => error_!("Failed to write response: {:?}.", e),
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); hyp_res = hyp_res.header(name, value);
} }
let send_response = move |hyp_res: hyper::ResponseBuilder, body| -> io::Result<()> { let send_response = move |res: hyper::ResponseBuilder, body| -> io::Result<()> {
let response = hyp_res.body(body) let response = res.body(body)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
tx.send(response).map_err(|_| { 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) io::Error::new(io::ErrorKind::BrokenPipe, msg)
}) })
}; };
@ -488,14 +493,13 @@ impl Rocket {
} }
// TODO.async: Solidify the Listener APIs and make this function public // 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, where L: Listener + Send + Unpin + 'static,
<L as Listener>::Connection: Send + Unpin + 'static, <L as Listener>::Connection: Send + Unpin + 'static,
{ {
// Determine the address and port we actually binded to. // We do this twice if `listen_on` was called through `launch()` but
self.config.port = listener.local_addr().map(|a| a.port()).unwrap_or(0); // only once if `listen_on()` gets called directly.
let proto = self.config.tls.as_ref().map_or("http://", |_| "https://"); self.prelaunch_check().await?;
let full_addr = format!("{}:{}", self.config.address, self.config.port);
// Freeze managed state for synchronization-free accesses later. // Freeze managed state for synchronization-free accesses later.
self.managed_state.freeze(); self.managed_state.freeze();
@ -504,31 +508,35 @@ impl Rocket {
self.fairings.pretty_print_counts(); self.fairings.pretty_print_counts();
self.fairings.handle_launch(self.cargo()); 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!("{}{} {}{}", launch_info!("{}{} {}{}",
Paint::emoji("🚀 "), Paint::emoji("🚀 "),
Paint::default("Rocket has launched from").bold(), Paint::default("Rocket has launched from").bold(),
Paint::default(proto).bold().underline(), Paint::default(proto).bold().underline(),
Paint::default(&full_addr).bold().underline()); Paint::default(&full_addr).bold().underline());
// Restore the log level back to what it originally was. // Determine keep-alives.
logger::pop_max_level(); let http1_keepalive = self.config.keep_alive != 0;
let http2_keep_alive = match self.config.keep_alive {
// Set the keep-alive. 0 => None,
// TODO.async: implement keep-alive in Listener n => Some(std::time::Duration::from_secs(n as u64))
// let timeout = self.config.keep_alive.map(|s| Duration::from_secs(s as u64)); };
// listener.set_keepalive(timeout);
// We need to get this before moving `self` into an `Arc`. // We need to get this before moving `self` into an `Arc`.
let mut shutdown_receiver = self.shutdown_receiver let mut shutdown_receiver = self.shutdown_receiver.take()
.take().expect("shutdown receiver has already been used"); .expect("shutdown receiver has already been used");
let rocket = Arc::new(self); 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 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 { async move {
Ok::<_, std::convert::Infallible>(hyper::service_fn(move |req| { 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)) hyper::Server::builder(Incoming::from_listener(listener))
.http1_keepalive(http1_keepalive)
.http2_keep_alive_interval(http2_keep_alive)
.executor(TokioExecutor) .executor(TokioExecutor)
.serve(service) .serve(service)
.with_graceful_shutdown(async move { shutdown_receiver.recv().await; }) .with_graceful_shutdown(async move { shutdown_receiver.recv().await; })
.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 { pub fn ignite() -> Rocket {
Config::read() Rocket::custom(Config::figment())
.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)
})
} }
/// Creates a new `Rocket` application using the supplied custom /// Creates a new `Rocket` application using the supplied configuration
/// configuration. The `Rocket.toml` file, if present, is ignored. Any /// provider. This method is typically called through the
/// environment variables setting config parameters are ignored. /// [`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 /// # Examples
/// ///
/// ```rust /// ```rust,no_run
/// use rocket::config::{Config, Environment}; /// use figment::{Figment, providers::{Toml, Env, Format}};
/// # use rocket::config::ConfigError;
/// ///
/// # #[allow(dead_code)] /// #[rocket::launch]
/// # fn try_config() -> Result<(), ConfigError> { /// fn rocket() -> _ {
/// let config = Config::build(Environment::Staging) /// let figment = Figment::from(rocket::Config::default())
/// .address("1.2.3.4") /// .merge(Toml::file("MyApp.toml").nested())
/// .port(9234) /// .merge(Env::prefixed("MY_APP_"));
/// .finalize()?;
/// ///
/// # #[allow(unused_variables)] /// rocket::custom(figment)
/// let app = rocket::custom(config); /// }
/// # Ok(())
/// # }
/// ``` /// ```
#[inline] #[inline]
pub fn custom(config: Config) -> Rocket { pub fn custom<T: figment::Provider>(provider: T) -> Rocket {
Rocket::configured(config) let (config, figment) = (Config::from(&provider), Figment::from(provider));
} logger::try_init(config.log_level, config.cli_colors, false);
config.pretty_print(figment.profile());
#[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());
}
let managed_state = Container::new(); let managed_state = Container::new();
let (shutdown_sender, shutdown_receiver) = mpsc::channel(1); let (shutdown_sender, shutdown_receiver) = mpsc::channel(1);
Rocket { Rocket {
config, managed_state, config, figment,
managed_state,
shutdown_handle: Shutdown(shutdown_sender), shutdown_handle: Shutdown(shutdown_sender),
manifest: vec![], manifest: vec![],
router: Router::new(), router: Router::new(),
@ -887,7 +843,27 @@ impl Rocket {
self.inspect().await.state() 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 /// This function is equivalent to `.inspect().await.config()` and is
/// provided as a convenience. /// provided as a convenience.
@ -938,14 +914,14 @@ impl Rocket {
/// Perform "pre-launch" checks: verify that there are no routing colisions /// Perform "pre-launch" checks: verify that there are no routing colisions
/// and that there were no fairing failures. /// 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; self.actualize_manifest().await;
if let Err(e) = self.router.collisions() { 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() { 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(()) Ok(())
@ -963,8 +939,6 @@ impl Rocket {
/// first being inspected. See the [`Error`] documentation for more /// first being inspected. See the [`Error`] documentation for more
/// information. /// information.
/// ///
/// [`Error`]: crate::error::Error
///
/// # Example /// # Example
/// ///
/// ```rust /// ```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 std::net::ToSocketAddrs;
use futures::future::Either; 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 full_addr = format!("{}:{}", self.config.address, self.config.port);
let addr = match full_addr.to_socket_addrs() { let addr = full_addr.to_socket_addrs()
Ok(mut addrs) => addrs.next().expect(">= 1 socket addr"), .map(|mut addrs| addrs.next().expect(">= 1 socket addr"))
Err(e) => return Err(Launch(e.into())), .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 // If `ctrl-c` shutdown is enabled, we `select` on `the ctrl-c` signal
// and server. Otherwise, we only wait on the `server`, hence `pending`. // and server. Otherwise, we only wait on the `server`, hence `pending`.
let shutdown_handle = self.shutdown_handle.clone(); let shutdown_handle = self.shutdown_handle.clone();
let shutdown_signal = match self.config.get_bool("ctrlc") { let shutdown_signal = match self.config.ctrlc {
Ok(false) => futures::future::pending().boxed(), true => tokio::signal::ctrl_c().boxed(),
_ => tokio::signal::ctrl_c().boxed(), false => futures::future::pending().boxed(),
}; };
#[cfg(feature = "tls")]
let server = { let server = {
macro_rules! listen_on { use crate::http::tls::bind_tls;
($expr:expr) => {{
let listener = match $expr {
Ok(ok) => ok,
Err(err) => return Err(Launch(LaunchError::new(LaunchErrorKind::Bind(err))))
};
self.listen_on(listener)
}};
}
#[cfg(feature = "tls")] { if let Some(tls_config) = &self.config.tls {
if let Some(tls) = self.config.tls.clone() { let (certs, key) = tls_config.to_readers().map_err(ErrorKind::Io)?;
listen_on!(crate::http::tls::bind_tls(addr, tls.certs, tls.key).await).boxed() let l = bind_tls(addr, certs, key).await.map_err(ErrorKind::Bind)?;
self.listen_on(l).boxed()
} else { } else {
listen_on!(crate::http::private::bind_tcp(addr).await).boxed() let l = bind_tcp(addr).await.map_err(ErrorKind::Bind)?;
} self.listen_on(l).boxed()
}
#[cfg(not(feature = "tls"))] {
listen_on!(crate::http::private::bind_tcp(addr).await).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 { match futures::future::select(shutdown_signal, server).await {
Either::Left((Ok(()), server)) => { Either::Left((Ok(()), server)) => {
// Ctrl-was pressed. Signal shutdown, wait for the server. // Ctrl-was pressed. Signal shutdown, wait for the server.
@ -1029,7 +998,7 @@ impl Rocket {
} }
Either::Left((Err(err), server)) => { Either::Left((Err(err), server)) => {
// Error setting up ctrl-c signal. Let the user know. // 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); info_!("Error: {}", err);
server.await server.await
} }
@ -1128,6 +1097,21 @@ impl Cargo {
self.0.managed_state.try_get() 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. /// Returns the active configuration.
/// ///
/// # Example /// # Example

View File

@ -401,7 +401,7 @@ mod tests {
fn req_route_mt_collide<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool 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>> 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()); let mut req = Request::new(&rocket, m, Origin::dummy());
if let Some(mt_str) = mt1.into() { if let Some(mt_str) = mt1.into() {
if m.supports_payload() { if m.supports_payload() {
@ -468,7 +468,7 @@ mod tests {
} }
fn req_route_path_match(a: &'static str, b: &'static str) -> bool { 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 req = Request::new(&rocket, Get, Origin::parse(a).expect("valid URI"));
let route = Route::ranked(0, Get, b.to_string(), dummy); let route = Route::ranked(0, Get, b.to_string(), dummy);
route.matches(&req) route.matches(&req)

View File

@ -224,7 +224,7 @@ mod test {
} }
fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> { fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> {
let rocket = Rocket::custom(Config::development()); let rocket = Rocket::custom(Config::default());
let request = Request::new(&rocket, method, Origin::parse(uri).unwrap()); let request = Request::new(&rocket, method, Origin::parse(uri).unwrap());
let matches = router.route(&request); let matches = router.route(&request);
if matches.len() > 0 { if matches.len() > 0 {
@ -235,7 +235,7 @@ mod test {
} }
fn matches<'a>(router: &'a Router, method: Method, uri: &str) -> Vec<&'a Route> { 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()); let request = Request::new(&rocket, method, Origin::parse(uri).unwrap());
router.route(&request) router.route(&request)
} }

View File

@ -14,16 +14,13 @@ fn index(form: Form<Simple>) -> String {
mod limits_tests { mod limits_tests {
use rocket; use rocket;
use rocket::config::{Environment, Config};
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::{Status, ContentType}; use rocket::http::{Status, ContentType};
use rocket::data::Limits; use rocket::data::Limits;
fn rocket_with_forms_limit(limit: u64) -> rocket::Rocket { fn rocket_with_forms_limit(limit: u64) -> rocket::Rocket {
let config = Config::build(Environment::Development) let limits = Limits::default().limit("forms", limit.into());
.limits(Limits::default().limit("forms", limit.into())) let config = rocket::Config::figment().merge(("limits", limits));
.unwrap();
rocket::custom(config).mount("/", routes![super::index]) rocket::custom(config).mount("/", routes![super::index])
} }

View File

@ -2,33 +2,24 @@
# defaults. We show all of them here explicitly for demonstrative purposes. # defaults. We show all of them here explicitly for demonstrative purposes.
[global.limits] [global.limits]
forms = 32768 forms = "64 kB"
json = 1048576 # this is an extra used by the json contrib module json = "1 MiB"
msgpack = 1048576 # this is an extra used by the msgpack contrib module msgpack = "2 MiB"
[development] [debug]
address = "localhost" address = "127.0.0.1"
port = 8000 port = 8000
workers = 1 workers = 1
keep_alive = 5 keep_alive = 0
log = "normal" log_level = "normal"
hi = "Hello!" # this is an unused extra; maybe application specific? hi = "Hello!" # this is an unused extra; maybe application specific?
is_extra = true # this is an unused extra; maybe application specific? is_extra = true # this is an unused extra; maybe application specific?
[staging] [release]
address = "0.0.0.0" address = "127.0.0.1"
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"
port = 8000 port = 8000
workers = 12 workers = 12
keep_alive = 5 keep_alive = 5
log = "critical" log_level = "critical"
# don't use this key! generate your own and keep it private! # don't use this key! generate your own and keep it private!
secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk=" secret_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="

View File

@ -1,3 +1,5 @@
#[cfg(test)] mod tests;
// This example's illustration is the Rocket.toml file. // This example's illustration is the Rocket.toml file.
#[rocket::launch] #[rocket::launch]
fn rocket() -> rocket::Rocket { rocket::ignite() } 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 publish = false
[dependencies] [dependencies]
tokio = { version = "0.2.0", features = ["io-util"] }
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -68,8 +68,10 @@ fn rocket() -> rocket::Rocket {
.attach(Counter::default()) .attach(Counter::default())
.attach(AdHoc::on_attach("Token State", |mut rocket| async { .attach(AdHoc::on_attach("Token State", |mut rocket| async {
println!("Adding token managed state..."); println!("Adding token managed state...");
let token_val = rocket.config().await.get_int("token").unwrap_or(-1); match rocket.figment().await.extract_inner("token") {
Ok(rocket.manage(Token(token_val))) Ok(value) => Ok(rocket.manage(Token(value))),
Err(_) => Err(rocket)
}
})) }))
.attach(AdHoc::on_launch("Launch Message", |_| { .attach(AdHoc::on_launch("Launch Message", |_| {
println!("Rocket is about to launch!"); println!("Rocket is about to launch!");

View File

@ -6,7 +6,7 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib", features = ["secrets"] }
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" 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 export CARGO_INCREMENTAL=0
CARGO="cargo" 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 # Checks that the versions for Cargo projects $@ all match
function check_versions_match() { function check_versions_match() {
local last_version="" local last_version=""
@ -59,6 +62,7 @@ fi
echo ":: Preparing. Environment is..." echo ":: Preparing. Environment is..."
print_environment print_environment
echo " CARGO: $CARGO" echo " CARGO: $CARGO"
echo " RUSTFLAGS: $RUSTFLAGS"
echo ":: Ensuring all crate versions match..." echo ":: Ensuring all crate versions match..."
check_versions_match "${ALL_PROJECT_DIRS[@]}" 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 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 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 Support for private cookies must be manually enabled via the `secrets` crate
using authenticated encryption, a form of encryption which simultaneously feature:
provides confidentiality, integrity, and authenticity. This means that private
cookies cannot be inspected, tampered with, or manufactured by clients. If you ```toml
prefer, you can think of private cookies as being signed and encrypted. ## in Cargo.toml
rocket = { version = "0.5.0-dev", features = ["secrets"] }
```
The API for retrieving, adding, and removing private cookies is identical except The API for retrieving, adding, and removing private cookies is identical except
methods are suffixed with `_private`. These methods are: [`get_private`], 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 ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
@ -634,13 +642,11 @@ fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
### Secret Key ### Secret Key
To encrypt private cookies, Rocket uses the 256-bit key specified in the To encrypt private cookies, Rocket uses the 256-bit key specified in the
`secret_key` configuration parameter. If one is not specified, Rocket will `secret_key` configuration parameter. When compiled in debug mode, a fresh key
automatically generate a fresh key. Note, however, that a private cookie can is generated automatically. In release mode, Rocket requires you to set a secret
only be decrypted with the same key with which it was encrypted. As such, it is key if the `secrets` feature is enabled. Failure to do so results in a hard
important to set a `secret_key` configuration parameter when using private error at launch time. The value of the parameter may either be a 256-bit base64
cookies so that cookies decrypt properly after an application restart. Rocket or hex string or a 32-byte slice.
emits a warning if an application is run in production without a configured
`secret_key`.
Generating a string suitable for use as a `secret_key` configuration value is 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 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. section of the guide.
[`get_private`]: @api/rocket/http/struct.CookieJar.html#method.get_private [`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 [`add_private`]: @api/rocket/http/struct.CookieJar.html#method.add_private
[`remove_private`]: @api/rocket/http/struct.CookieJar.html#method.remove_private [`remove_private`]: @api/rocket/http/struct.CookieJar.html#method.remove_private

View File

@ -1,242 +1,180 @@
# Configuration # Configuration
Rocket aims to have a flexible and usable configuration system. Rocket Rocket's configuration system is flexible. Based on [Figment](@figment), it
applications can be configured via a configuration file, through environment allows you to configure your application the way _you_ want while also providing
variables, or both. Configurations are separated into three environments: with a sensible set of defaults.
development, staging, and production. The working environment is selected via an
environment variable.
## Environment ## Overview
At any point in time, a Rocket application is operating in a given Rocket's configuration system is based on Figment's [`Provider`]s, types which
_configuration environment_. There are three such environments: 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`) Rocket expects to be able to extract a [`Config`] structure from the provider it
* `staging` (short: `stage`) is configured with. This means that no matter which configuration provider
* `production` (short: `prod`) 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 | key | kind | description | debug/release default |
debug builds and the `production` environment for non-debug builds. The |----------------|-----------------|-------------------------------------------------|-----------------------|
environment can be changed via the `ROCKET_ENV` environment variable. For | `address` | `IpAddr` | IP address to serve on | `127.0.0.1` |
example, to launch an application in the `staging` environment, we can run: | `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 ### Profiles
ROCKET_ENV=stage cargo run
```
Note that you can use the short or long form of the environment name to specify Configurations can be arbitrarily namespaced by [`Profile`]s. Rocket's
the environment, `stage` _or_ `staging` here. Rocket tells us the environment we [`Config`] and [`Config::figment()`] providers automatically set the
have chosen and its configuration when it launches: 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 In addition to any profiles you declare, there are two meta-profiles, `default`
$ sudo ROCKET_ENV=staging cargo run 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. [`Provider`]: @figment/trait.Provider.html
=> address: 0.0.0.0 [`Profile`]: @figment/struct.Profile.html
=> port: 8000 [`Config`]: @api/rocket/struct.Config.html
=> log: normal [`Config::figment()`]: @api/struct.Config.html#method.figment
=> workers: [logical cores * 2] [`Toml`]: @figment/providers/struct.Toml.html
=> secret key: generated [`Json`]: @figment/providers/struct.Json.html
=> limits: forms = 32KiB [`Figment`]: @api/rocket/struct.Figment.html
=> keep-alive: 5s [`Deserialize`]: @serde/trait.Deserialize.html
=> tls: disabled [`Limits::default()`]: @api/rocket/data/struct.Limits.html#impl-Default
🛰 Mounting '/':
=> GET / (hello)
🚀 Rocket has launched from http://0.0.0.0:8000
```
## Rocket.toml ### Secret Key
An optional `Rocket.toml` file can be used to specify the configuration The `secret_key` parameter configures a cryptographic key to use when encrypting
parameters for each environment. If it is not present, the default configuration application values. In particular, the key is used to encrypt [private cookies],
parameters are used. Rocket searches for the file starting at the current which are available only when the `secrets` crate feature is enabled.
working directory. If it is not found there, Rocket checks the parent directory.
Rocket continues checking parent directories until the root is reached.
The file must be a series of TOML tables, at most one for each environment, and When compiled in debug mode, a fresh key is generated automatically. In release
an optional "global" table. Each table contains key-value pairs corresponding to mode, Rocket requires you to set a secret key if the `secrets` feature is
configuration parameters for that environment. If a configuration parameter is enabled. Failure to do so results in a hard error at launch time. The value of
missing, the default value is used. The following is a complete `Rocket.toml` the parameter may either be a 256-bit base64 or hex string or a slice of 32
file, where every standard configuration parameter is specified with the default bytes.
value:
```toml [private cookies]: ../requests/#private-cookies
[development]
address = "localhost"
port = 8000
workers = [number of cpus * 2]
keep_alive = 5
log = "normal"
secret_key = [randomly generated at launch]
limits = { forms = 32768 }
[staging] ### Limits
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
The `limits` parameter configures the maximum amount of data Rocket will accept 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 for a given data type. The value is expected to be a dictionary table where each
data type and each value corresponds to the maximum size in bytes Rocket key corresponds to a data type and each value corresponds to the maximum size in
should accept for that type. 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, By default, Rocket specifies a `32 KiB` limit for incoming forms. Since Rocket
simply set the `limits.forms` configuration parameter. For example, to increase requires specifying a read limit whenever data is read, external data guards may
the forms limit to 128KiB globally, we might write: 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 ```toml
[global.limits] [dependencies]
forms = 131072 rocket = { version = "0.5.0-dev", features = ["tls"] }
``` ```
The `limits` parameter can contain keys and values that are not endemic to TLS is configured through the `tls` configuration parameter. The value of `tls`
Rocket. For instance, the [`Json`] type reads the `json` limit value to cap is a dictionary with two keys: `certs` and `key`, described in the table above.
incoming JSON data. You should use the `limits` parameter for your application's Each key's value may be either a path to a file or raw bytes corresponding to
data limits as well. Data limits can be retrieved at runtime via the the expected value. When a path is configured in a file source, such as
[`Request::limits()`] method. `Rocket.toml`, relative paths are interpreted as being relative to the source
file's directory.
[`Request::limits()`]: @api/rocket/struct.Request.html#method.limits ! warning: Rocket's built-in TLS implements only TLS 1.2 and 1.3. As such, it
[`Json`]: @api/rocket_contrib/json/struct.Json.html#incoming-data-limits may not be suitable for production use.
## Extras ## Default Provider
In addition to overriding default configuration parameters, a configuration file Rocket's default configuration provider is [`Config::figment()`]; this is the
can also define values for any number of _extra_ configuration parameters. While provider that's used when calling [`rocket::ignite()`].
these parameters aren't used by Rocket directly, other libraries, or your own
application, can use them as they wish. As an example, the The default figment merges, at a per-key level, and reads from the following
[Template](@api/rocket_contrib/templates/struct.Template.html) type sources, in ascending priority order:
accepts a value for the `template_dir` configuration parameter. The parameter
can be set in `Rocket.toml` as follows: 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 ```toml
[development] ## defaults for _all_ profiles
template_dir = "dev_templates/" [default]
address = "0.0.0.0"
limits = { forms = "64 kB", json = "1 MiB" }
[production] ## set only when compiled in debug mode, i.e, `cargo build`
template_dir = "prod_templates/" [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/"` ### Environment Variables
when operating in the `development` environment and `"prod_templates/"` when
operating in the `production` environment. Rocket will prepend the `[extra]` tag
to extra configuration parameters when launching:
```sh Rocket reads all environment variable names prefixed with `ROCKET_` using the
🔧 Configured for development. 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
=> [extra] template_dir: "dev_templates/" in `Rocket.toml`. Values are parsed as loose form of TOML syntax. Consider the
```
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
following examples: following examples:
```sh ```sh
@ -249,80 +187,161 @@ ROCKET_ARRAY=[1,"b",3.14]
ROCKET_DICT={key="abc",val=123} ROCKET_DICT={key="abc",val=123}
``` ```
## Programmatic ## Extracting Values
In addition to using environment variables or a config file, Rocket can also be Your application can extract any configuration that implements [`Deserialize`]
configured using the [`rocket::custom()`] method and [`ConfigBuilder`]: 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 ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
use rocket::config::{Config, Environment}; use rocket::data::{Limits, ToByteUnit};
# fn build_config() -> rocket::config::Result<Config> { #[launch]
let config = Config::build(Environment::Staging) fn rocket() -> _ {
.address("1.2.3.4") let figment = rocket::Config::figment()
.port(9234) .merge(("port", 1111))
.finalize()?; .merge(("limits", Limits::new().limit("json", 2.mebibytes())));
# Ok(config)
# }
# let config = build_config().expect("config okay"); rocket::custom(figment).mount("/", routes![/* .. */])
# /* }
rocket::custom(config)
.mount("/", routes![/* .. */])
.launch()
.await;
# */
``` ```
Configuration via `rocket::custom()` replaces calls to `rocket::ignite()` and More involved, consider an application that wants to use Rocket's defaults for
all configuration from `Rocket.toml` or environment variables. In other words, [`Config`], but not its configuration sources, while allowing the application to
using `rocket::custom()` results in `Rocket.toml` and environment variables be configured via an `App.toml` file and `APP_` environment variables:
being ignored.
```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 [`rocket::custom()`]: @api/rocket/fn.custom.html
[`ConfigBuilder`]: @api/rocket/config/struct.ConfigBuilder.html [`rocket::ignite()`]: @api/rocket/fn.custom.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
```

View File

@ -6,8 +6,9 @@ edition = "2018"
publish = false publish = false
[dev-dependencies] [dev-dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib", features = ["secrets"] }
doc-comment = "0.3" doc-comment = "0.3"
rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] } rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
rand = "0.7" rand = "0.7"
figment = { version = "0.9.2", features = ["toml", "env"] }