From 86bd7c1008a7ae8fa48fbc61c9fde72f79f4d5ea Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 9 Nov 2020 14:46:07 -0800 Subject: [PATCH] Allow read/write timeouts to be configured. Resolves #1472. --- core/lib/src/config/builder.rs | 62 +++++++++++++++++++++++++++ core/lib/src/config/config.rs | 68 ++++++++++++++++++++++++++++++ core/lib/src/config/mod.rs | 64 ++++++++++++++++++++++------ core/lib/src/data/data.rs | 8 +++- core/lib/src/response/responder.rs | 1 - core/lib/src/rocket.rs | 23 +++++++--- site/guide/2-getting-started.md | 2 + site/guide/3-overview.md | 2 + site/guide/9-configuration.md | 8 ++++ 9 files changed, 215 insertions(+), 23 deletions(-) diff --git a/core/lib/src/config/builder.rs b/core/lib/src/config/builder.rs index 536e773cf3..1d110ac80a 100644 --- a/core/lib/src/config/builder.rs +++ b/core/lib/src/config/builder.rs @@ -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); diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 690ba1fc12..5b44c1c9a2 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -46,6 +46,12 @@ pub struct Config { pub workers: u16, /// Keep-alive timeout in seconds or None if disabled. pub keep_alive: Option, + /// Number of seconds to wait without _receiving_ data before closing a + /// connection; disabled when `None`. + pub read_timeout: Option, + /// Number of seconds to wait without _sending_ data before closing a + /// connection; disabled when `None`. + pub write_timeout: Option, /// 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. /// diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 1e71af6366..cf92c87cf6 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -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] diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs index 85aab8c8fa..d5b4003736 100644 --- a/core/lib/src/data/data.rs +++ b/core/lib/src/data/data.rs @@ -91,7 +91,10 @@ impl Data { } // FIXME: This is absolutely terrible (downcasting!), thanks to Hyper. - crate fn from_hyp(mut body: HyperBodyReader) -> Result { + crate fn from_hyp( + req: &crate::Request<'_>, + mut body: HyperBodyReader + ) -> Result { #[inline(always)] #[cfg(feature = "tls")] fn concrete_stream(stream: &mut dyn NetworkStream) -> Option { @@ -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(); diff --git a/core/lib/src/response/responder.rs b/core/lib/src/response/responder.rs index 1e33d4de21..1fb21061a4 100644 --- a/core/lib/src/response/responder.rs +++ b/core/lib/src/response/responder.rs @@ -271,7 +271,6 @@ impl<'r, R: Responder<'r>> Responder<'r> for Option { /// 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 { default fn respond_to(self, req: &Request) -> response::Result<'r> { self.map(|r| r.respond_to(req)).unwrap_or_else(|e| { diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index d5d65e2a93..32d045f5d3 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -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) { + 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(); diff --git a/site/guide/2-getting-started.md b/site/guide/2-getting-started.md index eed4ece917..45fead15c3 100644 --- a/site/guide/2-getting-started.md +++ b/site/guide/2-getting-started.md @@ -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) diff --git a/site/guide/3-overview.md b/site/guide/3-overview.md index 33db0fc02b..7b9b691e6f 100644 --- a/site/guide/3-overview.md +++ b/site/guide/3-overview.md @@ -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) diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 13107fd708..1e6e8e90c1 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -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 }