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};
/// 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),
}

View File

@ -93,18 +93,24 @@ pub struct Config {
#[serde(deserialize_with = "crate::config::http_header::deserialize")]
pub ip_header: Option<Uncased<'static>>,
/// 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<Uncased<'static>>,
/// Streaming read size limits. **(default: [`Limits::default()`])**

View File

@ -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>) {

View File

@ -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() {

View File

@ -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<S, E> = outcome::Outcome<S, (Status, E), Status>;
/// * **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<Self, Self::Error> {
match request.proxy_proto() {
Some(proto) => Success(proto),
None => Forward(Status::InternalServerError),
}
request.proxy_proto().or_forward(Status::InternalServerError)
}
}

View File

@ -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
///

View File

@ -1,22 +1,13 @@
use rocket::http::ProxyProto;
#[macro_use]
extern crate rocket;
#[macro_use] extern crate rocket;
#[get("/")]
fn inspect_proto(proto: Option<ProxyProto>) -> String {
proto
.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())
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(), "<none>");
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("<none>".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("<none>".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("<none>".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();

View File

@ -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