Allow read/write timeouts to be configured.

Resolves #1472.
This commit is contained in:
Sergio Benitez 2020-11-09 14:46:07 -08:00
parent 3970783d06
commit 86bd7c1008
9 changed files with 215 additions and 23 deletions

View File

@ -16,6 +16,12 @@ pub struct ConfigBuilder {
pub workers: u16,
/// Keep-alive timeout in seconds or disabled if 0.
pub keep_alive: u32,
/// Number of seconds to wait without _receiving_ data before closing a
/// connection; disabled when `None`.
pub read_timeout: u32,
/// Number of seconds to wait without _sending_ data before closing a
/// connection; disabled when `None`.
pub write_timeout: u32,
/// How much information to log.
pub log_level: LoggingLevel,
/// The secret key.
@ -57,6 +63,8 @@ impl ConfigBuilder {
port: config.port,
workers: config.workers,
keep_alive: config.keep_alive.unwrap_or(0),
read_timeout: config.read_timeout.unwrap_or(0),
write_timeout: config.write_timeout.unwrap_or(0),
log_level: config.log_level,
secret_key: None,
tls: None,
@ -148,6 +156,58 @@ impl ConfigBuilder {
self
}
/// Sets the read timeout to `timeout` seconds. If `timeout` is `0`,
/// read timeouts are disabled.
///
/// # Example
///
/// ```rust
/// use rocket::config::{Config, Environment};
///
/// let config = Config::build(Environment::Staging)
/// .read_timeout(10)
/// .unwrap();
///
/// assert_eq!(config.read_timeout, Some(10));
///
/// let config = Config::build(Environment::Staging)
/// .read_timeout(0)
/// .unwrap();
///
/// assert_eq!(config.read_timeout, None);
/// ```
#[inline]
pub fn read_timeout(mut self, timeout: u32) -> Self {
self.read_timeout = timeout;
self
}
/// Sets the write timeout to `timeout` seconds. If `timeout` is `0`,
/// write timeouts are disabled.
///
/// # Example
///
/// ```rust
/// use rocket::config::{Config, Environment};
///
/// let config = Config::build(Environment::Staging)
/// .write_timeout(10)
/// .unwrap();
///
/// assert_eq!(config.write_timeout, Some(10));
///
/// let config = Config::build(Environment::Staging)
/// .write_timeout(0)
/// .unwrap();
///
/// assert_eq!(config.write_timeout, None);
/// ```
#[inline]
pub fn write_timeout(mut self, timeout: u32) -> Self {
self.write_timeout = timeout;
self
}
/// Sets the `log_level` in the configuration being built.
///
/// # Example
@ -318,6 +378,8 @@ impl ConfigBuilder {
config.set_port(self.port);
config.set_workers(self.workers);
config.set_keep_alive(self.keep_alive);
config.set_read_timeout(self.read_timeout);
config.set_write_timeout(self.write_timeout);
config.set_log_level(self.log_level);
config.set_extras(self.extras);
config.set_limits(self.limits);

View File

@ -46,6 +46,12 @@ pub struct Config {
pub workers: u16,
/// Keep-alive timeout in seconds or None if disabled.
pub keep_alive: Option<u32>,
/// Number of seconds to wait without _receiving_ data before closing a
/// connection; disabled when `None`.
pub read_timeout: Option<u32>,
/// Number of seconds to wait without _sending_ data before closing a
/// connection; disabled when `None`.
pub write_timeout: Option<u32>,
/// How much information to log.
pub log_level: LoggingLevel,
/// The secret key.
@ -229,6 +235,8 @@ impl Config {
port: 8000,
workers: default_workers,
keep_alive: Some(5),
read_timeout: Some(5),
write_timeout: Some(5),
log_level: LoggingLevel::Normal,
secret_key: key,
tls: None,
@ -245,6 +253,8 @@ impl Config {
port: 8000,
workers: default_workers,
keep_alive: Some(5),
read_timeout: Some(5),
write_timeout: Some(5),
log_level: LoggingLevel::Normal,
secret_key: key,
tls: None,
@ -261,6 +271,8 @@ impl Config {
port: 8000,
workers: default_workers,
keep_alive: Some(5),
read_timeout: Some(5),
write_timeout: Some(5),
log_level: LoggingLevel::Critical,
secret_key: key,
tls: None,
@ -307,6 +319,8 @@ impl Config {
port => (u16, set_port, ok),
workers => (u16, set_workers, ok),
keep_alive => (u32, set_keep_alive, ok),
read_timeout => (u32, set_read_timeout, ok),
write_timeout => (u32, set_write_timeout, ok),
log => (log_level, set_log_level, ok),
secret_key => (str, set_secret_key, id),
tls => (tls_config, set_raw_tls, id),
@ -422,6 +436,60 @@ impl Config {
}
}
/// Sets the read timeout to `timeout` seconds. If `timeout` is `0`, read
/// timeouts are disabled.
///
/// # Example
///
/// ```rust
/// use rocket::config::Config;
///
/// let mut config = Config::development();
///
/// // Set read timeout to 10 seconds.
/// config.set_read_timeout(10);
/// assert_eq!(config.read_timeout, Some(10));
///
/// // Disable read timeouts.
/// config.set_read_timeout(0);
/// assert_eq!(config.read_timeout, None);
/// ```
#[inline]
pub fn set_read_timeout(&mut self, timeout: u32) {
if timeout == 0 {
self.read_timeout = None;
} else {
self.read_timeout = Some(timeout);
}
}
/// Sets the write timeout to `timeout` seconds. If `timeout` is `0`, write
/// timeouts are disabled.
///
/// # Example
///
/// ```rust
/// use rocket::config::Config;
///
/// let mut config = Config::development();
///
/// // Set write timeout to 10 seconds.
/// config.set_write_timeout(10);
/// assert_eq!(config.write_timeout, Some(10));
///
/// // Disable write timeouts.
/// config.set_write_timeout(0);
/// assert_eq!(config.write_timeout, None);
/// ```
#[inline]
pub fn set_write_timeout(&mut self, timeout: u32) {
if timeout == 0 {
self.write_timeout = None;
} else {
self.write_timeout = Some(timeout);
}
}
/// Sets the `secret_key` in `self` to `key` which must be a 256-bit base64
/// encoded string.
///

View File

@ -31,18 +31,20 @@
//! not used by Rocket itself but can be used by external libraries. The
//! standard configuration parameters are:
//!
//! | name | type | description | examples |
//! |------------|----------------|-------------------------------------------------------------|----------------------------|
//! | address | string | ip address or host to listen on | `"localhost"`, `"1.2.3.4"` |
//! | port | integer | port number to listen on | `8000`, `80` |
//! | keep_alive | integer | keep-alive timeout in seconds | `0` (disable), `10` |
//! | workers | integer | number of concurrent thread workers | `36`, `512` |
//! | log | string | max log level: `"off"`, `"normal"`, `"debug"`, `"critical"` | `"off"`, `"normal"` |
//! | secret_key | 256-bit base64 | secret key for private cookies | `"8Xui8SI..."` (44 chars) |
//! | tls | table | tls config table with two keys (`certs`, `key`) | _see below_ |
//! | tls.certs | string | path to certificate chain in PEM format | `"private/cert.pem"` |
//! | tls.key | string | path to private key for `tls.certs` in PEM format | `"private/key.pem"` |
//! | limits | table | map from data type (string) to data limit (integer: bytes) | `{ forms = 65536 }` |
//! | name | type | description | examples |
//! |------------ |----------------|-------------------------------------------------------------|----------------------------|
//! | address | string | ip address or host to listen on | `"localhost"`, `"1.2.3.4"` |
//! | port | integer | port number to listen on | `8000`, `80` |
//! | keep_alive | integer | keep-alive timeout in seconds | `0` (disable), `10` |
//! | read_timeout | integer | data read timeout in seconds | `0` (disable), `5` |
//! | write_timeout | integer | data write timeout in seconds | `0` (disable), `5` |
//! | workers | integer | number of concurrent thread workers | `36`, `512` |
//! | log | string | max log level: `"off"`, `"normal"`, `"debug"`, `"critical"` | `"off"`, `"normal"` |
//! | secret_key | 256-bit base64 | secret key for private cookies | `"8Xui8SI..."` (44 chars) |
//! | tls | table | tls config table with two keys (`certs`, `key`) | _see below_ |
//! | tls.certs | string | path to certificate chain in PEM format | `"private/cert.pem"` |
//! | tls.key | string | path to private key for `tls.certs` in PEM format | `"private/key.pem"` |
//! | limits | table | map from data type (string) to data limit (integer: bytes) | `{ forms = 65536 }` |
//!
//! ### Rocket.toml
//!
@ -64,6 +66,8 @@
//! port = 8000
//! workers = [number_of_cpus * 2]
//! keep_alive = 5
//! read_timeout = 5
//! write_timeout = 5
//! log = "normal"
//! secret_key = [randomly generated at launch]
//! limits = { forms = 32768 }
@ -73,6 +77,8 @@
//! port = 8000
//! workers = [number_of_cpus * 2]
//! keep_alive = 5
//! read_timeout = 5
//! write_timeout = 5
//! log = "normal"
//! secret_key = [randomly generated at launch]
//! limits = { forms = 32768 }
@ -82,6 +88,8 @@
//! port = 8000
//! workers = [number_of_cpus * 2]
//! keep_alive = 5
//! read_timeout = 5
//! write_timeout = 5
//! log = "critical"
//! secret_key = [randomly generated at launch]
//! limits = { forms = 32768 }
@ -579,6 +587,8 @@ mod test {
workers = 21
log = "critical"
keep_alive = 0
read_timeout = 1
write_timeout = 0
secret_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
template_dir = "mine"
json = true
@ -591,6 +601,8 @@ mod test {
.workers(21)
.log_level(LoggingLevel::Critical)
.keep_alive(0)
.read_timeout(1)
.write_timeout(0)
.secret_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=")
.extra("template_dir", "mine")
.extra("json", true)
@ -866,7 +878,7 @@ mod test {
}
#[test]
fn test_good_keep_alives() {
fn test_good_keep_alives_and_timeouts() {
// Take the lock so changing the environment doesn't cause races.
let _env_lock = ENV_LOCK.lock().unwrap();
env::set_var(CONFIG_ENV, "stage");
@ -898,10 +910,24 @@ mod test {
"#.to_string(), TEST_CONFIG_FILENAME), {
default_config(Staging).keep_alive(0)
});
check_config!(RocketConfig::parse(r#"
[stage]
read_timeout = 10
"#.to_string(), TEST_CONFIG_FILENAME), {
default_config(Staging).read_timeout(10)
});
check_config!(RocketConfig::parse(r#"
[stage]
write_timeout = 4
"#.to_string(), TEST_CONFIG_FILENAME), {
default_config(Staging).write_timeout(4)
});
}
#[test]
fn test_bad_keep_alives() {
fn test_bad_keep_alives_and_timeouts() {
// Take the lock so changing the environment doesn't cause races.
let _env_lock = ENV_LOCK.lock().unwrap();
env::remove_var(CONFIG_ENV);
@ -925,6 +951,16 @@ mod test {
[dev]
keep_alive = 4294967296
"#.to_string(), TEST_CONFIG_FILENAME).is_err());
assert!(RocketConfig::parse(r#"
[dev]
read_timeout = true
"#.to_string(), TEST_CONFIG_FILENAME).is_err());
assert!(RocketConfig::parse(r#"
[dev]
write_timeout = None
"#.to_string(), TEST_CONFIG_FILENAME).is_err());
}
#[test]

View File

@ -91,7 +91,10 @@ impl Data {
}
// FIXME: This is absolutely terrible (downcasting!), thanks to Hyper.
crate fn from_hyp(mut body: HyperBodyReader) -> Result<Data, &'static str> {
crate fn from_hyp(
req: &crate::Request<'_>,
mut body: HyperBodyReader
) -> Result<Data, &'static str> {
#[inline(always)]
#[cfg(feature = "tls")]
fn concrete_stream(stream: &mut dyn NetworkStream) -> Option<NetStream> {
@ -117,7 +120,8 @@ impl Data {
};
// Set the read timeout to 5 seconds.
let _ = net_stream.set_read_timeout(Some(Duration::from_secs(5)));
let timeout = req.state.config.read_timeout.map(|s| Duration::from_secs(s as u64));
let _ = net_stream.set_read_timeout(timeout);
// Steal the internal, undecoded data buffer from Hyper.
let (mut hyper_buf, pos, cap) = body.get_mut().take_buf();

View File

@ -271,7 +271,6 @@ impl<'r, R: Responder<'r>> Responder<'r> for Option<R> {
/// If `self` is `Ok`, responds with the wrapped `Responder`. Otherwise prints
/// an error message with the `Err` value returns an `Err` of
/// `Status::InternalServerError`.
#[deprecated(since = "0.4.3")]
impl<'r, R: Responder<'r>, E: fmt::Debug> Responder<'r> for Result<R, E> {
default fn respond_to(self, req: &Request) -> response::Result<'r> {
self.map(|r| r.respond_to(req)).unwrap_or_else(|e| {

View File

@ -69,7 +69,7 @@ impl hyper::Handler for Rocket {
};
// Retrieve the data from the hyper body.
let data = match Data::from_hyp(h_body) {
let data = match Data::from_hyp(&req, h_body) {
Ok(data) => data,
Err(reason) => {
error_!("Bad data in request: {}", reason);
@ -414,11 +414,19 @@ impl Rocket {
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()),
fn log_timeout(name: &str, value: Option<u32>) {
let painted = match value {
Some(v) => Paint::default(format!("{}s", v)).bold(),
None => Paint::default("disabled".into()).bold()
};
launch_info_!("{}: {}", name, painted);
}
log_timeout("keep-alive", config.keep_alive);
log_timeout("read timeout", config.read_timeout);
log_timeout("write timeout", config.write_timeout);
let tls_configured = config.tls.is_some();
if tls_configured && cfg!(feature = "tls") {
launch_info_!("tls: {}", Paint::default("enabled").bold());
@ -717,8 +725,11 @@ impl Rocket {
server.keep_alive(timeout);
// Set sane timeouts.
server.set_read_timeout(Some(Duration::from_secs(10)));
server.set_write_timeout(Some(Duration::from_secs(10)));
let read_timeout = self.config.read_timeout.map(|s| Duration::from_secs(s as u64));
server.set_read_timeout(read_timeout);
let write_timeout = self.config.write_timeout.map(|s| Duration::from_secs(s as u64));
server.set_write_timeout(write_timeout);
// Freeze managed state for synchronization-free accesses later.
self.state.freeze();

View File

@ -87,6 +87,8 @@ run`. You should see the following:
=> secret key: generated
=> limits: forms = 32KiB
=> keep-alive: 5s
=> read timeout: 5s
=> write timeout: 5s
=> tls: disabled
🛰 Mounting '/':
=> GET / (index)

View File

@ -194,6 +194,8 @@ Running the application, the console shows:
=> secret key: generated
=> limits: forms = 32KiB
=> keep-alive: 5s
=> read timeout: 5s
=> write timeout: 5s
=> tls: disabled
🛰 Mounting '/hello':
=> GET /hello/world (world)

View File

@ -39,6 +39,8 @@ $ sudo ROCKET_ENV=staging cargo run
=> secret key: generated
=> limits: forms = 32KiB
=> keep-alive: 5s
=> read timeout: 5s
=> write timeout: 5s
=> tls: disabled
🛰 Mounting '/':
=> GET / (hello)
@ -66,6 +68,8 @@ address = "localhost"
port = 8000
workers = [number of cpus * 2]
keep_alive = 5
read_timeout = 5
write_timeout = 5
log = "normal"
secret_key = [randomly generated at launch]
limits = { forms = 32768 }
@ -75,6 +79,8 @@ address = "0.0.0.0"
port = 8000
workers = [number of cpus * 2]
keep_alive = 5
read_timeout = 5
write_timeout = 5
log = "normal"
secret_key = [randomly generated at launch]
limits = { forms = 32768 }
@ -84,6 +90,8 @@ address = "0.0.0.0"
port = 8000
workers = [number of cpus * 2]
keep_alive = 5
read_timeout = 5
write_timeout = 5
log = "critical"
secret_key = [randomly generated at launch]
limits = { forms = 32768 }