diff --git a/core/http/src/header/proxy_proto.rs b/core/http/src/header/proxy_proto.rs index 6c475263..9b7a4ce3 100644 --- a/core/http/src/header/proxy_proto.rs +++ b/core/http/src/header/proxy_proto.rs @@ -2,15 +2,30 @@ use std::fmt; use uncased::{UncasedStr, AsUncased}; -/// A protocol used to identify a specific protocol forwarded by an HTTP proxy. -/// Value are case-insensitive. +/// Parsed [`Config::proxy_proto_header`] value: identifies a forwarded HTTP +/// protocol (aka [X-Forwarded-Proto]). +/// +/// The value of the header with name [`Config::proxy_proto_header`] is parsed +/// case-insensitively into this `enum`. For a given request, the parsed value, +/// if the header was present, can be retrieved via [`Request::proxy_proto()`] +/// or directly as a [request guard]. That value is used to determine whether a +/// request's context is likely secure ([`Request::context_is_likely_secure()`]) +/// which in-turn is used to determine whether the `Secure` cookie flag is set +/// by default when [cookies are added] to a `CookieJar`. +/// +/// [X-Forwarded-Proto]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto +/// [`Config::proxy_proto_header`]: ../../rocket/struct.Config.html#structfield.proxy_proto_header +/// [`Request::proxy_proto()`]: ../../rocket/request/struct.Request.html#method.proxy_proto +/// [`Request::context_is_likely_secure()`]: ../../rocket/request/struct.Request.html#method.context_is_likely_secure +/// [cookies are added]: ../..//rocket/http/struct.CookieJar.html#method.add +/// [request guard]: ../../rocket/request/trait.FromRequest.html#provided-implementations #[derive(Clone, Debug, Eq, PartialEq)] pub enum ProxyProto<'a> { - /// `http` value, Hypertext Transfer Protocol. + /// `"http"`: Hypertext Transfer Protocol. Http, - /// `https` value, Hypertext Transfer Protocol Secure. + /// `"https"`: Hypertext Transfer Protocol Secure. Https, - /// Any protocol name other than `http` or `https`. + /// Any protocol name other than `"http"` or `"https"`. Unknown(&'a UncasedStr), } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index aaa62c41..197b6a2f 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -93,18 +93,24 @@ pub struct Config { #[serde(deserialize_with = "crate::config::http_header::deserialize")] pub ip_header: Option>, /// The name of a header, whose value is typically set by an intermediary - /// server or proxy, which contains the protocol (HTTP or HTTPS) used by the - /// connecting client. This should probably be [`X-Forwarded-Proto`], as - /// that is the de facto standard. Used by [`Request::forwarded_proto()`] - /// to determine the forwarded protocol and [`Request::forwarded_secure()`] - /// to determine whether a request is handled in a secure context. + /// server or proxy, which contains the protocol ("http" or "https") used by + /// the connecting client. This is usually [`"X-Forwarded-Proto"`], as that + /// is the de-facto standard. /// - /// [`X-Forwarded-Proto`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + /// The header value is parsed into a [`ProxyProto`], accessible via + /// [`Request::proxy_proto()`]. The value influences + /// [`Request::context_is_likely_secure()`] and the default value for the + /// `Secure` flag in cookies added to [`CookieJar`]s. /// /// To disable using any header for this purpose, set this value to `false` - /// or `None`. Deserialization semantics are identical to those of [`ip_header`]. + /// or `None`. Deserialization semantics are identical to those of + /// [`Config::ip_header`] (the value must be a valid HTTP header name). /// /// **(default: `None`)** + /// + /// [`CookieJar`]: crate::http::CookieJar + /// [`ProxyProto`]: crate::http::ProxyProto + /// [`"X-Forwarded-Proto"`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto #[serde(deserialize_with = "crate::config::http_header::deserialize")] pub proxy_proto_header: Option>, /// Streaming read size limits. **(default: [`Limits::default()`])** diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 926241fd..b64441f0 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -278,12 +278,13 @@ impl<'a> CookieJar<'a> { /// /// * `path`: `"/"` /// * `SameSite`: `Strict` + /// * `Secure`: `true` if [`Request::context_is_likely_secure()`] /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. /// These defaults ensure maximum usability and security. For additional /// security, you may wish to set the `secure` flag explicitly. /// + /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure() + /// /// # Example /// /// ```rust @@ -321,11 +322,13 @@ impl<'a> CookieJar<'a> { /// * `SameSite`: `Strict` /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now + /// * `Secure`: `true` if [`Request::context_is_likely_secure()`] /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. /// These defaults ensure maximum usability and security. For additional - /// security, you may wish to set the `secure` flag explicitly. + /// security, you may wish to set the `secure` flag explicitly and + /// unconditionally. + /// + /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure() /// /// # Example /// @@ -510,9 +513,7 @@ impl<'a> CookieJar<'a> { /// /// * `path`: `"/"` /// * `SameSite`: `Strict` - /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// * `Secure`: `true` if `Request::context_is_likely_secure()` fn set_defaults(&self, cookie: &mut Cookie<'static>) { if cookie.path().is_none() { cookie.set_path("/"); @@ -550,9 +551,7 @@ impl<'a> CookieJar<'a> { /// * `SameSite`: `Strict` /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now - /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// * `Secure`: `true` if `Request::context_is_likely_secure()` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] fn set_private_defaults(&self, cookie: &mut Cookie<'static>) { diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index e0fbd836..f91afeb2 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -87,9 +87,10 @@ impl<'c> LocalResponse<'c> { let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; async move { - // NOTE: The new `secure` cookie jar state will not reflect the last - // known value in `request.cookies()`. This is okay as new cookies - // should never be added to the resulting jar. + // NOTE: The cookie jar `secure` state will not reflect the last + // known value in `request.cookies()`. This is okay: new cookies + // should never be added to the resulting jar which is the only time + // the value is used to set cookie defaults. let response: Response<'c> = f(request).await; let mut cookies = CookieJar::new(None, request.rocket()); for cookie in response.cookies() { diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index e183d4e4..c95a427f 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use std::net::{IpAddr, SocketAddr}; use crate::{Request, Route}; -use crate::outcome::{self, Outcome::*}; +use crate::outcome::{self, IntoOutcome, Outcome::*}; use crate::http::uri::{Host, Origin}; use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar}; @@ -163,8 +163,8 @@ pub type Outcome = outcome::Outcome; /// * **ProxyProto** /// /// Extracts the protocol of the incoming request as a [`ProxyProto`] via -/// [`Request::proxy_proto()`] (HTTP or HTTPS). If value of the header is -/// not known, the request is forwarded with a 404 Not Found status. +/// [`Request::proxy_proto()`]. If no such header is present, the request is +/// forwarded with a 500 Internal Server Error status. /// /// * **SocketAddr** /// @@ -481,10 +481,7 @@ impl<'r> FromRequest<'r> for ProxyProto<'r> { type Error = std::convert::Infallible; async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.proxy_proto() { - Some(proto) => Success(proto), - None => Forward(Status::InternalServerError), - } + request.proxy_proto().or_forward(Status::InternalServerError) } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 44630b26..1d380a97 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -389,8 +389,9 @@ impl<'r> Request<'r> { /// /// The value is determined by inspecting the header named /// [`proxy_proto_header`](crate::Config::proxy_proto_header), if - /// configured. If parameter isn't configured or the request doesn't contain - /// a header named as indicated, this method returns `None`. + /// configured, and parsing it case-insensitivity. If the parameter isn't + /// configured or the request doesn't contain a header named as indicated, + /// this method returns `None`. /// /// # Example /// @@ -413,7 +414,7 @@ impl<'r> Request<'r> { /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https)); /// /// # let req = c.get("/"); - /// let req = req.header(Header::new("x-forwarded-proto", "http")); + /// let req = req.header(Header::new("x-forwarded-proto", "HTTP")); /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Http)); /// /// # let req = c.get("/"); @@ -438,7 +439,7 @@ impl<'r> Request<'r> { /// be in a secure context. We say _likely_ because it is entirely possible /// for the header to indicate that the connection is being proxied via /// HTTPS while reality differs. As such, this value should not be trusted - /// when security is a concern. + /// when 100% confidence is a necessity. /// /// # Example /// diff --git a/core/lib/tests/config-proxy-proto-header.rs b/core/lib/tests/config-proxy-proto-header.rs index 48dc5af3..b0ddc014 100644 --- a/core/lib/tests/config-proxy-proto-header.rs +++ b/core/lib/tests/config-proxy-proto-header.rs @@ -1,22 +1,13 @@ -use rocket::http::ProxyProto; - -#[macro_use] -extern crate rocket; +#[macro_use] extern crate rocket; #[get("/")] -fn inspect_proto(proto: Option) -> String { - proto - .map(|proto| match proto { - ProxyProto::Http => "http".to_owned(), - ProxyProto::Https => "https".to_owned(), - ProxyProto::Unknown(s) => s.to_string(), - }) - .unwrap_or("".to_owned()) +fn inspect_proto(proto: rocket::http::ProxyProto) -> String { + proto.to_string() } mod tests { use rocket::{Rocket, Build, Route}; - use rocket::http::Header; + use rocket::http::{Header, Status}; use rocket::local::blocking::Client; use rocket::figment::Figment; @@ -32,8 +23,7 @@ mod tests { #[test] fn check_proxy_proto_header_works() { - let rocket = rocket_with_proto_header(Some("X-Url-Scheme")); - let client = Client::debug(rocket).unwrap(); + let client = Client::debug(rocket_with_proto_header(Some("X-Url-Scheme"))).unwrap(); let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Url-Scheme", "http")) @@ -41,22 +31,18 @@ mod tests { assert_eq!(response.into_string().unwrap(), "http"); - let response = client.get("/") - .header(Header::new("X-Url-Scheme", "https")) - .dispatch(); - + let response = client.get("/").header(Header::new("X-Url-Scheme", "https")).dispatch(); assert_eq!(response.into_string().unwrap(), "https"); let response = client.get("/").dispatch(); - assert_eq!(response.into_string().unwrap(), ""); + assert_eq!(response.status(), Status::InternalServerError); } #[test] fn check_proxy_proto_header_works_again() { let client = Client::debug(rocket_with_proto_header(Some("x-url-scheme"))).unwrap(); - let response = client - .get("/") - .header(Header::new("X-Url-Scheme", "https")) + let response = client.get("/") + .header(Header::new("X-Url-Scheme", "hTTpS")) .dispatch(); assert_eq!(response.into_string().unwrap(), "https"); @@ -65,9 +51,8 @@ mod tests { .merge(("proxy_proto_header", "x-url-scheme")); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") - .header(Header::new("X-url-Scheme", "https")) + let response = client.get("/") + .header(Header::new("X-url-Scheme", "HTTPS")) .dispatch(); assert_eq!(response.into_string().unwrap(), "https"); @@ -76,12 +61,11 @@ mod tests { #[test] fn check_default_proxy_proto_header_works() { let client = Client::debug_with(routes()).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); } #[test] @@ -91,25 +75,23 @@ mod tests { .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); let config = Figment::from(rocket::Config::debug_default()).merge(("proxy_proto_header", false)); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); let config = Figment::from(rocket::Config::debug_default()) .merge(("proxy_proto_header", "x-forwarded-proto")); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("x-Forwarded-Proto", "https")) .dispatch(); diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 5cd2c51e..0c4b680a 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -152,8 +152,8 @@ workers = 16 max_blocking = 512 keep_alive = 5 ident = "Rocket" -ip_header = "X-Real-IP" # set to `false` or `None` to disable -proxy_proto_header = `false` # set to `false` or `None` to disable +ip_header = "X-Real-IP" # set to `false` (the default) to disable +proxy_proto_header = `false` # set to `false` (the default) to disable log_level = "normal" temp_dir = "/tmp" cli_colors = true @@ -365,22 +365,30 @@ mutual TLS. ### Proxied TLS -If Rocket is running behind a reverse proxy that terminates TLS, it is useful to -know whether the original connection was made securely. Therefore, Rocket offers -the option to configure a `proxy_proto_header` that is used to determine if the -request is handled in a secure context. The outcome is available via -[`Request::context_is_likely_secure()`] and used to set cookies' secure flag by -default. To enable this behaviour, configure the header as set by your reverse -proxy. For example: +The `proxy_proto_header` configuration parameter allows Rocket applications to +determine when and if a client's initial connection was likely made in a secure +context by examining the header with the configured name. The header's value is +parsed into a [`ProxyProto`], retrievable via [`Request::proxy_proto()`]. + +That value is in-turn inspected to determine if the initial connection was +secure (i.e, made over TLS) and the outcome made available via +[`Request::context_is_likely_secure()`]. The value returned by this method +influences cookie defaults. In particular, if the method returns `true` (i.e, +the request context is likely secure), the `Secure` cookie flag is set by +default when a cookie is added to a [`CookieJar`]. + +To enable this behaviour, configure the header as set by your reverse proxy or +forwarding entity. For example, to set the header name to `X-Forwarded-Proto` +via a TOML file: ```toml,ignore -proxy_proto_header = 'X-Forwarded-Proto' +proxy_proto_header = "X-Forwarded-Proto" ``` -Note that this only sets the cookies' secure flag when not configured -explicitly. This setting also provides the [request guard `ProxyProto`]. - -[`ProxyProto`]: @api/rocket/request/trait.FromRequest.html#impl-FromRequest%3C'r%3E-for-%26ProxyProto +[`Request::proxy_proto()`]: @api/rocket/request/struct.Request.html#method.proxy_proto +[`ProxyProto`]: @api/rocket/http/enum.ProxyProto.html +[`CookieJar`]: @api/rocket/http/struct.CookieJar.html +[`Request::context_is_likely_secure()`]: @api/rocket/request/struct.Request.html#method.context_is_likely_secure ### Workers