Fixup docs for 'proxy_proto_header'.

This commit is contained in:
Sergio Benitez 2024-01-22 18:18:06 -08:00 committed by Sergio Benitez
parent 5c85ea3db5
commit e9b568d9b2
8 changed files with 95 additions and 86 deletions

View File

@ -2,15 +2,30 @@ use std::fmt;
use uncased::{UncasedStr, AsUncased}; use uncased::{UncasedStr, AsUncased};
/// A protocol used to identify a specific protocol forwarded by an HTTP proxy. /// Parsed [`Config::proxy_proto_header`] value: identifies a forwarded HTTP
/// Value are case-insensitive. /// 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProxyProto<'a> { pub enum ProxyProto<'a> {
/// `http` value, Hypertext Transfer Protocol. /// `"http"`: Hypertext Transfer Protocol.
Http, Http,
/// `https` value, Hypertext Transfer Protocol Secure. /// `"https"`: Hypertext Transfer Protocol Secure.
Https, Https,
/// Any protocol name other than `http` or `https`. /// Any protocol name other than `"http"` or `"https"`.
Unknown(&'a UncasedStr), Unknown(&'a UncasedStr),
} }

View File

@ -93,18 +93,24 @@ pub struct Config {
#[serde(deserialize_with = "crate::config::http_header::deserialize")] #[serde(deserialize_with = "crate::config::http_header::deserialize")]
pub ip_header: Option<Uncased<'static>>, pub ip_header: Option<Uncased<'static>>,
/// The name of a header, whose value is typically set by an intermediary /// 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 /// server or proxy, which contains the protocol ("http" or "https") used by
/// connecting client. This should probably be [`X-Forwarded-Proto`], as /// the connecting client. This is usually [`"X-Forwarded-Proto"`], as that
/// that is the de facto standard. Used by [`Request::forwarded_proto()`] /// is the de-facto standard.
/// to determine the forwarded protocol and [`Request::forwarded_secure()`]
/// to determine whether a request is handled in a secure context.
/// ///
/// [`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` /// 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`)** /// **(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")] #[serde(deserialize_with = "crate::config::http_header::deserialize")]
pub proxy_proto_header: Option<Uncased<'static>>, pub proxy_proto_header: Option<Uncased<'static>>,
/// Streaming read size limits. **(default: [`Limits::default()`])** /// Streaming read size limits. **(default: [`Limits::default()`])**

View File

@ -278,12 +278,13 @@ impl<'a> CookieJar<'a> {
/// ///
/// * `path`: `"/"` /// * `path`: `"/"`
/// * `SameSite`: `Strict` /// * `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 /// 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.
/// ///
/// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure()
///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
@ -321,11 +322,13 @@ impl<'a> CookieJar<'a> {
/// * `SameSite`: `Strict` /// * `SameSite`: `Strict`
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `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 /// 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 /// # Example
/// ///
@ -510,9 +513,7 @@ impl<'a> CookieJar<'a> {
/// ///
/// * `path`: `"/"` /// * `path`: `"/"`
/// * `SameSite`: `Strict` /// * `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.
fn set_defaults(&self, cookie: &mut Cookie<'static>) { fn set_defaults(&self, cookie: &mut Cookie<'static>) {
if cookie.path().is_none() { if cookie.path().is_none() {
cookie.set_path("/"); cookie.set_path("/");
@ -550,9 +551,7 @@ impl<'a> CookieJar<'a> {
/// * `SameSite`: `Strict` /// * `SameSite`: `Strict`
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `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.
#[cfg(feature = "secrets")] #[cfg(feature = "secrets")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
fn set_private_defaults(&self, cookie: &mut Cookie<'static>) { fn set_private_defaults(&self, cookie: &mut Cookie<'static>) {

View File

@ -87,9 +87,10 @@ impl<'c> LocalResponse<'c> {
let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) };
async move { async move {
// NOTE: The new `secure` cookie jar state will not reflect the last // NOTE: The cookie jar `secure` state will not reflect the last
// known value in `request.cookies()`. This is okay as new cookies // known value in `request.cookies()`. This is okay: new cookies
// should never be added to the resulting jar. // 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 response: Response<'c> = f(request).await;
let mut cookies = CookieJar::new(None, request.rocket()); let mut cookies = CookieJar::new(None, request.rocket());
for cookie in response.cookies() { for cookie in response.cookies() {

View File

@ -3,7 +3,7 @@ use std::fmt::Debug;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use crate::{Request, Route}; use crate::{Request, Route};
use crate::outcome::{self, Outcome::*}; use crate::outcome::{self, IntoOutcome, Outcome::*};
use crate::http::uri::{Host, Origin}; use crate::http::uri::{Host, Origin};
use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar}; use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar};
@ -163,8 +163,8 @@ pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>;
/// * **ProxyProto** /// * **ProxyProto**
/// ///
/// Extracts the protocol of the incoming request as a [`ProxyProto`] via /// Extracts the protocol of the incoming request as a [`ProxyProto`] via
/// [`Request::proxy_proto()`] (HTTP or HTTPS). If value of the header is /// [`Request::proxy_proto()`]. If no such header is present, the request is
/// not known, the request is forwarded with a 404 Not Found status. /// forwarded with a 500 Internal Server Error status.
/// ///
/// * **SocketAddr** /// * **SocketAddr**
/// ///
@ -481,10 +481,7 @@ impl<'r> FromRequest<'r> for ProxyProto<'r> {
type Error = std::convert::Infallible; type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.proxy_proto() { request.proxy_proto().or_forward(Status::InternalServerError)
Some(proto) => Success(proto),
None => Forward(Status::InternalServerError),
}
} }
} }

View File

@ -389,8 +389,9 @@ impl<'r> Request<'r> {
/// ///
/// The value is determined by inspecting the header named /// The value is determined by inspecting the header named
/// [`proxy_proto_header`](crate::Config::proxy_proto_header), if /// [`proxy_proto_header`](crate::Config::proxy_proto_header), if
/// configured. If parameter isn't configured or the request doesn't contain /// configured, and parsing it case-insensitivity. If the parameter isn't
/// a header named as indicated, this method returns `None`. /// configured or the request doesn't contain a header named as indicated,
/// this method returns `None`.
/// ///
/// # Example /// # Example
/// ///
@ -413,7 +414,7 @@ impl<'r> Request<'r> {
/// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https)); /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https));
/// ///
/// # let req = c.get("/"); /// # 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)); /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Http));
/// ///
/// # let req = c.get("/"); /// # 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 /// 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 /// for the header to indicate that the connection is being proxied via
/// HTTPS while reality differs. As such, this value should not be trusted /// HTTPS while reality differs. As such, this value should not be trusted
/// when security is a concern. /// when 100% confidence is a necessity.
/// ///
/// # Example /// # Example
/// ///

View File

@ -1,22 +1,13 @@
use rocket::http::ProxyProto; #[macro_use] extern crate rocket;
#[macro_use]
extern crate rocket;
#[get("/")] #[get("/")]
fn inspect_proto(proto: Option<ProxyProto>) -> String { fn inspect_proto(proto: rocket::http::ProxyProto) -> String {
proto proto.to_string()
.map(|proto| match proto {
ProxyProto::Http => "http".to_owned(),
ProxyProto::Https => "https".to_owned(),
ProxyProto::Unknown(s) => s.to_string(),
})
.unwrap_or("<none>".to_owned())
} }
mod tests { mod tests {
use rocket::{Rocket, Build, Route}; use rocket::{Rocket, Build, Route};
use rocket::http::Header; use rocket::http::{Header, Status};
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::figment::Figment; use rocket::figment::Figment;
@ -32,8 +23,7 @@ mod tests {
#[test] #[test]
fn check_proxy_proto_header_works() { fn check_proxy_proto_header_works() {
let rocket = rocket_with_proto_header(Some("X-Url-Scheme")); let client = Client::debug(rocket_with_proto_header(Some("X-Url-Scheme"))).unwrap();
let client = Client::debug(rocket).unwrap();
let response = client.get("/") let response = client.get("/")
.header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Forwarded-Proto", "https"))
.header(Header::new("X-Url-Scheme", "http")) .header(Header::new("X-Url-Scheme", "http"))
@ -41,22 +31,18 @@ mod tests {
assert_eq!(response.into_string().unwrap(), "http"); assert_eq!(response.into_string().unwrap(), "http");
let response = client.get("/") let response = client.get("/").header(Header::new("X-Url-Scheme", "https")).dispatch();
.header(Header::new("X-Url-Scheme", "https"))
.dispatch();
assert_eq!(response.into_string().unwrap(), "https"); assert_eq!(response.into_string().unwrap(), "https");
let response = client.get("/").dispatch(); let response = client.get("/").dispatch();
assert_eq!(response.into_string().unwrap(), "<none>"); assert_eq!(response.status(), Status::InternalServerError);
} }
#[test] #[test]
fn check_proxy_proto_header_works_again() { fn check_proxy_proto_header_works_again() {
let client = Client::debug(rocket_with_proto_header(Some("x-url-scheme"))).unwrap(); let client = Client::debug(rocket_with_proto_header(Some("x-url-scheme"))).unwrap();
let response = client let response = client.get("/")
.get("/") .header(Header::new("X-Url-Scheme", "hTTpS"))
.header(Header::new("X-Url-Scheme", "https"))
.dispatch(); .dispatch();
assert_eq!(response.into_string().unwrap(), "https"); assert_eq!(response.into_string().unwrap(), "https");
@ -65,9 +51,8 @@ mod tests {
.merge(("proxy_proto_header", "x-url-scheme")); .merge(("proxy_proto_header", "x-url-scheme"));
let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap();
let response = client let response = client.get("/")
.get("/") .header(Header::new("X-url-Scheme", "HTTPS"))
.header(Header::new("X-url-Scheme", "https"))
.dispatch(); .dispatch();
assert_eq!(response.into_string().unwrap(), "https"); assert_eq!(response.into_string().unwrap(), "https");
@ -76,12 +61,11 @@ mod tests {
#[test] #[test]
fn check_default_proxy_proto_header_works() { fn check_default_proxy_proto_header_works() {
let client = Client::debug_with(routes()).unwrap(); let client = Client::debug_with(routes()).unwrap();
let response = client let response = client.get("/")
.get("/")
.header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Forwarded-Proto", "https"))
.dispatch(); .dispatch();
assert_eq!(response.into_string(), Some("<none>".into())); assert_eq!(response.status(), Status::InternalServerError);
} }
#[test] #[test]
@ -91,25 +75,23 @@ mod tests {
.header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Forwarded-Proto", "https"))
.dispatch(); .dispatch();
assert_eq!(response.into_string(), Some("<none>".into())); assert_eq!(response.status(), Status::InternalServerError);
let config = let config =
Figment::from(rocket::Config::debug_default()).merge(("proxy_proto_header", false)); Figment::from(rocket::Config::debug_default()).merge(("proxy_proto_header", false));
let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap();
let response = client let response = client.get("/")
.get("/")
.header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Forwarded-Proto", "https"))
.dispatch(); .dispatch();
assert_eq!(response.into_string(), Some("<none>".into())); assert_eq!(response.status(), Status::InternalServerError);
let config = Figment::from(rocket::Config::debug_default()) let config = Figment::from(rocket::Config::debug_default())
.merge(("proxy_proto_header", "x-forwarded-proto")); .merge(("proxy_proto_header", "x-forwarded-proto"));
let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap();
let response = client let response = client.get("/")
.get("/")
.header(Header::new("x-Forwarded-Proto", "https")) .header(Header::new("x-Forwarded-Proto", "https"))
.dispatch(); .dispatch();

View File

@ -152,8 +152,8 @@ workers = 16
max_blocking = 512 max_blocking = 512
keep_alive = 5 keep_alive = 5
ident = "Rocket" ident = "Rocket"
ip_header = "X-Real-IP" # 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` or `None` to disable proxy_proto_header = `false` # set to `false` (the default) to disable
log_level = "normal" log_level = "normal"
temp_dir = "/tmp" temp_dir = "/tmp"
cli_colors = true cli_colors = true
@ -365,22 +365,30 @@ mutual TLS.
### Proxied TLS ### Proxied TLS
If Rocket is running behind a reverse proxy that terminates TLS, it is useful to The `proxy_proto_header` configuration parameter allows Rocket applications to
know whether the original connection was made securely. Therefore, Rocket offers determine when and if a client's initial connection was likely made in a secure
the option to configure a `proxy_proto_header` that is used to determine if the context by examining the header with the configured name. The header's value is
request is handled in a secure context. The outcome is available via parsed into a [`ProxyProto`], retrievable via [`Request::proxy_proto()`].
[`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 That value is in-turn inspected to determine if the initial connection was
proxy. For example: 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 ```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 [`Request::proxy_proto()`]: @api/rocket/request/struct.Request.html#method.proxy_proto
explicitly. This setting also provides the [request guard `ProxyProto`]. [`ProxyProto`]: @api/rocket/http/enum.ProxyProto.html
[`CookieJar`]: @api/rocket/http/struct.CookieJar.html
[`ProxyProto`]: @api/rocket/request/trait.FromRequest.html#impl-FromRequest%3C'r%3E-for-%26ProxyProto [`Request::context_is_likely_secure()`]: @api/rocket/request/struct.Request.html#method.context_is_likely_secure
### Workers ### Workers