Support configurable 'X-Forwarded-Proto'.

Co-authored-by: Sergio Benitez <sb@sergio.bz>
This commit is contained in:
Arjen 2023-04-11 23:43:12 +02:00 committed by Sergio Benitez
parent 5034ff0d1a
commit 5c85ea3db5
12 changed files with 421 additions and 96 deletions

View File

@ -4,10 +4,12 @@ mod media_type;
mod content_type; mod content_type;
mod accept; mod accept;
mod header; mod header;
mod proxy_proto;
pub use self::content_type::ContentType; pub use self::content_type::ContentType;
pub use self::accept::{Accept, QMediaType}; pub use self::accept::{Accept, QMediaType};
pub use self::media_type::MediaType; pub use self::media_type::MediaType;
pub use self::header::{Header, HeaderMap}; pub use self::header::{Header, HeaderMap};
pub use self::proxy_proto::ProxyProto;
pub(crate) use self::media_type::Source; pub(crate) use self::media_type::Source;

View File

@ -0,0 +1,51 @@
use std::fmt;
use uncased::{UncasedStr, AsUncased};
/// A protocol used to identify a specific protocol forwarded by an HTTP proxy.
/// Value are case-insensitive.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProxyProto<'a> {
/// `http` value, Hypertext Transfer Protocol.
Http,
/// `https` value, Hypertext Transfer Protocol Secure.
Https,
/// Any protocol name other than `http` or `https`.
Unknown(&'a UncasedStr),
}
impl ProxyProto<'_> {
/// Returns `true` if `self` is `ProxyProto::Https` and `false` otherwise.
///
/// # Example
///
/// ```rust
/// use rocket::http::ProxyProto;
///
/// assert!(ProxyProto::Https.is_https());
/// assert!(!ProxyProto::Http.is_https());
/// ```
pub fn is_https(&self) -> bool {
self == &ProxyProto::Https
}
}
impl<'a> From<&'a str> for ProxyProto<'a> {
fn from(value: &'a str) -> ProxyProto<'a> {
match value.as_uncased() {
v if v == "http" => ProxyProto::Http,
v if v == "https" => ProxyProto::Https,
v => ProxyProto::Unknown(v)
}
}
}
impl fmt::Display for ProxyProto<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
ProxyProto::Http => "http",
ProxyProto::Https => "https",
ProxyProto::Unknown(s) => s.as_str(),
})
}
}

View File

@ -85,13 +85,28 @@ pub struct Config {
/// client. Used internally and by [`Request::client_ip()`] and /// client. Used internally and by [`Request::client_ip()`] and
/// [`Request::real_ip()`]. /// [`Request::real_ip()`].
/// ///
/// 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`
/// Deserialization semantics are identical to those of [`Ident`] except /// or `None`. Deserialization semantics are identical to those of [`Ident`]
/// that the value must syntactically be a valid HTTP header name. /// except that the value must syntactically be a valid HTTP header name.
/// ///
/// **(default: `"X-Real-IP"`)** /// **(default: `"X-Real-IP"`)**
#[serde(deserialize_with = "crate::config::ip_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
/// 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.
///
/// [`X-Forwarded-Proto`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
///
/// To disable using any header for this purpose, set this value to `false`
/// or `None`. Deserialization semantics are identical to those of [`ip_header`].
///
/// **(default: `None`)**
#[serde(deserialize_with = "crate::config::http_header::deserialize")]
pub proxy_proto_header: Option<Uncased<'static>>,
/// Streaming read size limits. **(default: [`Limits::default()`])** /// Streaming read size limits. **(default: [`Limits::default()`])**
pub limits: Limits, pub limits: Limits,
/// Directory to store temporary files in. **(default: /// Directory to store temporary files in. **(default:
@ -189,6 +204,7 @@ impl Config {
max_blocking: 512, max_blocking: 512,
ident: Ident::default(), ident: Ident::default(),
ip_header: Some(Uncased::from_borrowed("X-Real-IP")), ip_header: Some(Uncased::from_borrowed("X-Real-IP")),
proxy_proto_header: None,
limits: Limits::default(), limits: Limits::default(),
temp_dir: std::env::temp_dir().into(), temp_dir: std::env::temp_dir().into(),
keep_alive: 5, keep_alive: 5,
@ -409,6 +425,11 @@ impl Config {
None => launch_meta_!("IP header: {}", "disabled".paint(VAL)) None => launch_meta_!("IP header: {}", "disabled".paint(VAL))
} }
match self.proxy_proto_header.as_ref() {
Some(name) => launch_meta_!("Proxy-Proto header: {}", name.paint(VAL)),
None => launch_meta_!("Proxy-Proto header: {}", "disabled".paint(VAL))
}
launch_meta_!("limits: {}", (&self.limits).paint(VAL)); launch_meta_!("limits: {}", (&self.limits).paint(VAL));
launch_meta_!("temp dir: {}", self.temp_dir.relative().display().paint(VAL)); launch_meta_!("temp dir: {}", self.temp_dir.relative().display().paint(VAL));
launch_meta_!("http/2: {}", (cfg!(feature = "http2").paint(VAL))); launch_meta_!("http/2: {}", (cfg!(feature = "http2").paint(VAL)));
@ -513,6 +534,9 @@ impl Config {
/// The stringy parameter name for setting/extracting [`Config::ip_header`]. /// The stringy parameter name for setting/extracting [`Config::ip_header`].
pub const IP_HEADER: &'static str = "ip_header"; pub const IP_HEADER: &'static str = "ip_header";
/// The stringy parameter name for setting/extracting [`Config::proxy_proto_header`].
pub const PROXY_PROTO_HEADER: &'static str = "proxy_proto_header";
/// The stringy parameter name for setting/extracting [`Config::limits`]. /// The stringy parameter name for setting/extracting [`Config::limits`].
pub const LIMITS: &'static str = "limits"; pub const LIMITS: &'static str = "limits";
@ -536,10 +560,9 @@ impl Config {
/// An array of all of the stringy parameter names. /// An array of all of the stringy parameter names.
pub const PARAMETERS: &'static [&'static str] = &[ pub const PARAMETERS: &'static [&'static str] = &[
Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, Self::KEEP_ALIVE,
Self::KEEP_ALIVE, Self::IDENT, Self::IP_HEADER, Self::LIMITS, Self::TLS, Self::IDENT, Self::IP_HEADER, Self::PROXY_PROTO_HEADER, Self::LIMITS, Self::TLS,
Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, Self::CLI_COLORS,
Self::CLI_COLORS,
]; ];
} }

View File

@ -114,8 +114,8 @@
mod ident; mod ident;
mod config; mod config;
mod shutdown; mod shutdown;
mod ip_header;
mod cli_colors; mod cli_colors;
mod http_header;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
mod tls; mod tls;

View File

@ -2,8 +2,8 @@ use std::fmt;
use parking_lot::Mutex; use parking_lot::Mutex;
use crate::Config;
use crate::http::private::cookie; use crate::http::private::cookie;
use crate::{Rocket, Orbit};
#[doc(inline)] #[doc(inline)]
pub use self::cookie::{Cookie, SameSite, Iter}; pub use self::cookie::{Cookie, SameSite, Iter};
@ -154,17 +154,14 @@ pub use self::cookie::{Cookie, SameSite, Iter};
pub struct CookieJar<'a> { pub struct CookieJar<'a> {
jar: cookie::CookieJar, jar: cookie::CookieJar,
ops: Mutex<Vec<Op>>, ops: Mutex<Vec<Op>>,
config: &'a Config, pub(crate) state: CookieState<'a>,
} }
impl<'a> Clone for CookieJar<'a> { #[derive(Copy, Clone)]
fn clone(&self) -> Self { pub(crate) struct CookieState<'a> {
CookieJar { pub secure: bool,
jar: self.jar.clone(), #[cfg_attr(not(feature = "secrets"), allow(unused))]
ops: Mutex::new(self.ops.lock().clone()), pub config: &'a crate::Config,
config: self.config,
}
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -173,22 +170,17 @@ enum Op {
Remove(Cookie<'static>, bool), Remove(Cookie<'static>, bool),
} }
impl Op {
fn cookie(&self) -> &Cookie<'static> {
match self {
Op::Add(c, _) | Op::Remove(c, _) => c
}
}
}
impl<'a> CookieJar<'a> { impl<'a> CookieJar<'a> {
#[inline(always)] pub(crate) fn new(base: Option<cookie::CookieJar>, rocket: &'a Rocket<Orbit>) -> Self {
pub(crate) fn new(config: &'a Config) -> Self { CookieJar {
CookieJar::from(cookie::CookieJar::new(), config) jar: base.unwrap_or_default(),
} ops: Mutex::new(Vec::new()),
state: CookieState {
pub(crate) fn from(jar: cookie::CookieJar, config: &'a Config) -> Self { // This is updated dynamically when headers are received.
CookieJar { jar, config, ops: Mutex::new(Vec::new()) } secure: rocket.config().tls_enabled(),
config: rocket.config(),
}
}
} }
/// Returns a reference to the _original_ `Cookie` inside this container /// Returns a reference to the _original_ `Cookie` inside this container
@ -236,7 +228,7 @@ impl<'a> CookieJar<'a> {
#[cfg(feature = "secrets")] #[cfg(feature = "secrets")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn get_private(&self, name: &str) -> Option<Cookie<'static>> { pub fn get_private(&self, name: &str) -> Option<Cookie<'static>> {
self.jar.private(&self.config.secret_key.key).get(name) self.jar.private(&self.state.config.secret_key.key).get(name)
} }
/// Returns a reference to the _original or pending_ `Cookie` inside this /// Returns a reference to the _original or pending_ `Cookie` inside this
@ -287,7 +279,10 @@ impl<'a> CookieJar<'a> {
/// * `path`: `"/"` /// * `path`: `"/"`
/// * `SameSite`: `Strict` /// * `SameSite`: `Strict`
/// ///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. /// 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.
/// ///
/// # Example /// # Example
/// ///
@ -309,7 +304,7 @@ impl<'a> CookieJar<'a> {
/// ``` /// ```
pub fn add<C: Into<Cookie<'static>>>(&self, cookie: C) { pub fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
let mut cookie = cookie.into(); let mut cookie = cookie.into();
Self::set_defaults(self.config, &mut cookie); self.set_defaults(&mut cookie);
self.ops.lock().push(Op::Add(cookie, false)); self.ops.lock().push(Op::Add(cookie, false));
} }
@ -327,9 +322,10 @@ impl<'a> CookieJar<'a> {
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `Expires`: 1 week from now
/// ///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. These /// Furthermore, if TLS is enabled or handled by a proxy (as determined by
/// defaults ensure maximum usability and security. For additional security, /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set.
/// you may wish to set the `secure` flag. /// These defaults ensure maximum usability and security. For additional
/// security, you may wish to set the `secure` flag explicitly.
/// ///
/// # Example /// # Example
/// ///
@ -346,7 +342,7 @@ impl<'a> CookieJar<'a> {
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn add_private<C: Into<Cookie<'static>>>(&self, cookie: C) { pub fn add_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
let mut cookie = cookie.into(); let mut cookie = cookie.into();
Self::set_private_defaults(self.config, &mut cookie); self.set_private_defaults(&mut cookie);
self.ops.lock().push(Op::Add(cookie, true)); self.ops.lock().push(Op::Add(cookie, true));
} }
@ -476,7 +472,7 @@ impl<'a> CookieJar<'a> {
Op::Add(c, false) => jar.add(c), Op::Add(c, false) => jar.add(c),
#[cfg(feature = "secrets")] #[cfg(feature = "secrets")]
Op::Add(c, true) => { Op::Add(c, true) => {
jar.private_mut(&self.config.secret_key.key).add(c); jar.private_mut(&self.state.config.secret_key.key).add(c);
} }
Op::Remove(mut c, _) => { Op::Remove(mut c, _) => {
if self.jar.get(c.name()).is_some() { if self.jar.get(c.name()).is_some() {
@ -505,7 +501,7 @@ impl<'a> CookieJar<'a> {
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
#[inline(always)] #[inline(always)]
pub(crate) fn add_original_private(&mut self, cookie: Cookie<'static>) { pub(crate) fn add_original_private(&mut self, cookie: Cookie<'static>) {
self.jar.private_mut(&self.config.secret_key.key).add_original(cookie); self.jar.private_mut(&self.state.config.secret_key.key).add_original(cookie);
} }
/// For each property mentioned below, this method checks if there is a /// For each property mentioned below, this method checks if there is a
@ -515,8 +511,9 @@ impl<'a> CookieJar<'a> {
/// * `path`: `"/"` /// * `path`: `"/"`
/// * `SameSite`: `Strict` /// * `SameSite`: `Strict`
/// ///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. /// Furthermore, if TLS is enabled or handled by a proxy (as determined by
fn set_defaults(config: &Config, cookie: &mut Cookie<'static>) { /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set.
fn set_defaults(&self, cookie: &mut Cookie<'static>) {
if cookie.path().is_none() { if cookie.path().is_none() {
cookie.set_path("/"); cookie.set_path("/");
} }
@ -525,7 +522,7 @@ impl<'a> CookieJar<'a> {
cookie.set_same_site(SameSite::Strict); cookie.set_same_site(SameSite::Strict);
} }
if cookie.secure().is_none() && config.tls_enabled() { if cookie.secure().is_none() && self.state.secure {
cookie.set_secure(true); cookie.set_secure(true);
} }
} }
@ -554,17 +551,12 @@ impl<'a> CookieJar<'a> {
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `Expires`: 1 week from now
/// ///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. /// 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(config: &Config, cookie: &mut Cookie<'static>) { fn set_private_defaults(&self, cookie: &mut Cookie<'static>) {
if cookie.path().is_none() { self.set_defaults(cookie);
cookie.set_path("/");
}
if cookie.same_site().is_none() {
cookie.set_same_site(SameSite::Strict);
}
if cookie.http_only().is_none() { if cookie.http_only().is_none() {
cookie.set_http_only(true); cookie.set_http_only(true);
@ -573,10 +565,6 @@ impl<'a> CookieJar<'a> {
if cookie.expires().is_none() { if cookie.expires().is_none() {
cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1)); cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1));
} }
if cookie.secure().is_none() && config.tls_enabled() {
cookie.set_secure(true);
}
} }
} }
@ -593,5 +581,22 @@ impl fmt::Debug for CookieJar<'_> {
.field("pending", &pending) .field("pending", &pending)
.finish() .finish()
} }
}
impl<'a> Clone for CookieJar<'a> {
fn clone(&self) -> Self {
CookieJar {
jar: self.jar.clone(),
ops: Mutex::new(self.ops.lock().clone()),
state: self.state,
}
}
}
impl Op {
fn cookie(&self) -> &Cookie<'static> {
match self {
Op::Add(c, _) | Op::Remove(c, _) => c
}
}
} }

View File

@ -87,8 +87,11 @@ 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
// known value in `request.cookies()`. This is okay as new cookies
// should never be added to the resulting jar.
let response: Response<'c> = f(request).await; let response: Response<'c> = f(request).await;
let mut cookies = CookieJar::new(request.rocket().config()); let mut cookies = CookieJar::new(None, request.rocket());
for cookie in response.cookies() { for cookie in response.cookies() {
cookies.add_original(cookie.into_owned()); cookies.add_original(cookie.into_owned());
} }

View File

@ -181,9 +181,8 @@ macro_rules! pub_client_impl {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn cookies(&self) -> crate::http::CookieJar<'_> { pub fn cookies(&self) -> crate::http::CookieJar<'_> {
let config = &self.rocket().config();
let jar = self._with_raw_cookies(|jar| jar.clone()); let jar = self._with_raw_cookies(|jar| jar.clone());
crate::http::CookieJar::from(jar, config) crate::http::CookieJar::new(Some(jar), self.rocket())
} }
req_method!($import, "GET", get, Method::Get); req_method!($import, "GET", get, Method::Get);

View File

@ -6,7 +6,7 @@ use crate::{Request, Route};
use crate::outcome::{self, Outcome::*}; use crate::outcome::{self, Outcome::*};
use crate::http::uri::{Host, Origin}; use crate::http::uri::{Host, Origin};
use crate::http::{Status, ContentType, Accept, Method, CookieJar}; use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar};
/// Type alias for the `Outcome` of a `FromRequest` conversion. /// Type alias for the `Outcome` of a `FromRequest` conversion.
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>; pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>;
@ -160,6 +160,12 @@ pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Status>;
/// via [`Request::client_ip()`]. If the client's IP address is not known, /// via [`Request::client_ip()`]. If the client's IP address is not known,
/// the request is forwarded with a 500 Internal Server Error status. /// the request is forwarded with a 500 Internal Server Error 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.
///
/// * **SocketAddr** /// * **SocketAddr**
/// ///
/// Extracts the remote address of the incoming request as a [`SocketAddr`] /// Extracts the remote address of the incoming request as a [`SocketAddr`]
@ -470,6 +476,18 @@ impl<'r> FromRequest<'r> for IpAddr {
} }
} }
#[crate::async_trait]
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),
}
}
}
#[crate::async_trait] #[crate::async_trait]
impl<'r> FromRequest<'r> for SocketAddr { impl<'r> FromRequest<'r> for SocketAddr {
type Error = Infallible; type Error = Infallible;

View File

@ -13,9 +13,8 @@ use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::form::{self, ValueField, FromForm}; use crate::form::{self, ValueField, FromForm};
use crate::data::Limits; use crate::data::Limits;
use crate::http::{hyper, Method, Header, HeaderMap}; use crate::http::{hyper, Method, Header, HeaderMap, ProxyProto};
use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie}; use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie};
use crate::http::uncased::UncasedStr;
use crate::http::private::Certificates; use crate::http::private::Certificates;
use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority}; use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority};
@ -97,7 +96,7 @@ impl<'r> Request<'r> {
state: RequestState { state: RequestState {
rocket, rocket,
route: Atomic::new(None), route: Atomic::new(None),
cookies: CookieJar::new(rocket.config()), cookies: CookieJar::new(None, rocket),
accept: InitCell::new(), accept: InitCell::new(),
content_type: InitCell::new(), content_type: InitCell::new(),
cache: Arc::new(<TypeMap![Send + Sync]>::new()), cache: Arc::new(<TypeMap![Send + Sync]>::new()),
@ -386,6 +385,85 @@ impl<'r> Request<'r> {
}) })
} }
/// Returns the [`ProxyProto`] associated with the current request.
///
/// 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`.
///
/// # Example
///
/// ```rust
/// use rocket::http::{Header, ProxyProto};
///
/// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap();
/// # let req = c.get("/");
/// // By default, no `proxy_proto_header` is configured.
/// let req = req.header(Header::new("x-forwarded-proto", "https"));
/// assert_eq!(req.proxy_proto(), None);
///
/// // We can configure one by setting the `proxy_proto_header` parameter.
/// // Here we set it to `x-forwarded-proto`, considered de-facto standard.
/// # let figment = rocket::figment::Figment::from(rocket::Config::debug_default());
/// let figment = figment.merge(("proxy_proto_header", "x-forwarded-proto"));
/// # let c = rocket::local::blocking::Client::debug(rocket::custom(figment)).unwrap();
/// # let req = c.get("/");
/// let req = req.header(Header::new("x-forwarded-proto", "https"));
/// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https));
///
/// # let req = c.get("/");
/// let req = req.header(Header::new("x-forwarded-proto", "http"));
/// assert_eq!(req.proxy_proto(), Some(ProxyProto::Http));
///
/// # let req = c.get("/");
/// let req = req.header(Header::new("x-forwarded-proto", "xproto"));
/// assert_eq!(req.proxy_proto(), Some(ProxyProto::Unknown("xproto".into())));
/// ```
pub fn proxy_proto(&self) -> Option<ProxyProto<'_>> {
self.rocket()
.config
.proxy_proto_header
.as_ref()
.and_then(|header| self.headers().get_one(header.as_str()))
.map(ProxyProto::from)
}
/// Returns whether we are *likely* in a secure context.
///
/// A request is in a "secure context" if it was initially sent over a
/// secure (TLS, via HTTPS) connection. If TLS is configured and enabled,
/// then the request is guaranteed to be in a secure context. Otherwise, if
/// [`Request::proxy_proto()`] evaluates to `Https`, then we are _likely_ to
/// 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.
///
/// # Example
///
/// ```rust
/// use rocket::http::{Header, ProxyProto};
///
/// # let client = rocket::local::blocking::Client::debug_with(vec![]).unwrap();
/// # let req = client.get("/");
/// // If TLS and proxy_proto are disabled, we are not in a secure context.
/// assert_eq!(req.context_is_likely_secure(), false);
///
/// // Configuring proxy_proto and receiving a header value of `https` is
/// // interpreted as likely being in a secure context.
/// // Here we set it to `x-forwarded-proto`, considered de-facto standard.
/// # let figment = rocket::figment::Figment::from(rocket::Config::debug_default());
/// let figment = figment.merge(("proxy_proto_header", "x-forwarded-proto"));
/// # let c = rocket::local::blocking::Client::debug(rocket::custom(figment)).unwrap();
/// # let req = c.get("/");
/// let req = req.header(Header::new("x-forwarded-proto", "https"));
/// assert_eq!(req.context_is_likely_secure(), true);
/// ```
pub fn context_is_likely_secure(&self) -> bool {
self.cookies().state.secure
}
/// Attempts to return the client's IP address by first inspecting the /// Attempts to return the client's IP address by first inspecting the
/// [`ip_header`](crate::Config::ip_header) and then using the remote /// [`ip_header`](crate::Config::ip_header) and then using the remote
/// connection's IP address. Note that the built-in `IpAddr` request guard /// connection's IP address. Note that the built-in `IpAddr` request guard
@ -497,7 +575,7 @@ impl<'r> Request<'r> {
#[inline] #[inline]
pub fn add_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) { pub fn add_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) {
let header = header.into(); let header = header.into();
self.bust_header_cache(header.name(), false); self.bust_header_cache(&header, false);
self.headers.add(header); self.headers.add(header);
} }
@ -526,7 +604,7 @@ impl<'r> Request<'r> {
#[inline] #[inline]
pub fn replace_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) { pub fn replace_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) {
let header = header.into(); let header = header.into();
self.bust_header_cache(header.name(), true); self.bust_header_cache(&header, true);
self.headers.replace(header); self.headers.replace(header);
} }
@ -547,9 +625,9 @@ impl<'r> Request<'r> {
/// ``` /// ```
#[inline] #[inline]
pub fn content_type(&self) -> Option<&ContentType> { pub fn content_type(&self) -> Option<&ContentType> {
self.state.content_type.get_or_init(|| { self.state.content_type
self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()) .get_or_init(|| self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()))
}).as_ref() .as_ref()
} }
/// Returns the Accept header of `self`. If the header is not present, /// Returns the Accept header of `self`. If the header is not present,
@ -567,9 +645,9 @@ impl<'r> Request<'r> {
/// ``` /// ```
#[inline] #[inline]
pub fn accept(&self) -> Option<&Accept> { pub fn accept(&self) -> Option<&Accept> {
self.state.accept.get_or_init(|| { self.state.accept
self.headers().get_one("Accept").and_then(|v| v.parse().ok()) .get_or_init(|| self.headers().get_one("Accept").and_then(|v| v.parse().ok()))
}).as_ref() .as_ref()
} }
/// Returns the media type "format" of the request. /// Returns the media type "format" of the request.
@ -925,15 +1003,19 @@ impl<'r> Request<'r> {
#[doc(hidden)] #[doc(hidden)]
impl<'r> Request<'r> { impl<'r> Request<'r> {
/// Resets the cached value (if any) for the header with name `name`. /// Resets the cached value (if any) for the header with name `name`.
fn bust_header_cache(&mut self, name: &UncasedStr, replace: bool) { fn bust_header_cache(&mut self, header: &Header<'_>, replace: bool) {
if name == "Content-Type" { if header.name() == "Content-Type" {
if self.content_type().is_none() || replace { if self.content_type().is_none() || replace {
self.state.content_type = InitCell::new(); self.state.content_type = InitCell::new();
} }
} else if name == "Accept" { } else if header.name() == "Accept" {
if self.accept().is_none() || replace { if self.accept().is_none() || replace {
self.state.accept = InitCell::new(); self.state.accept = InitCell::new();
} }
} else if Some(header.name()) == self.rocket().config.proxy_proto_header.as_deref() {
if !self.cookies().state.secure || replace {
self.cookies_mut().state.secure |= ProxyProto::from(header.value()).is_https();
}
} }
} }

View File

@ -0,0 +1,118 @@
use rocket::http::ProxyProto;
#[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())
}
mod tests {
use rocket::{Rocket, Build, Route};
use rocket::http::Header;
use rocket::local::blocking::Client;
use rocket::figment::Figment;
fn routes() -> Vec<Route> {
routes![super::inspect_proto]
}
fn rocket_with_proto_header(header: Option<&'static str>) -> Rocket<Build> {
let mut config = rocket::Config::debug_default();
config.proxy_proto_header = header.map(|h| h.into());
rocket::custom(config).mount("/", routes())
}
#[test]
fn check_proxy_proto_header_works() {
let rocket = rocket_with_proto_header(Some("X-Url-Scheme"));
let client = Client::debug(rocket).unwrap();
let response = client.get("/")
.header(Header::new("X-Forwarded-Proto", "https"))
.header(Header::new("X-Url-Scheme", "http"))
.dispatch();
assert_eq!(response.into_string().unwrap(), "http");
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>");
}
#[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"))
.dispatch();
assert_eq!(response.into_string().unwrap(), "https");
let config = Figment::from(rocket::Config::debug_default())
.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"))
.dispatch();
assert_eq!(response.into_string().unwrap(), "https");
}
#[test]
fn check_default_proxy_proto_header_works() {
let client = Client::debug_with(routes()).unwrap();
let response = client
.get("/")
.header(Header::new("X-Forwarded-Proto", "https"))
.dispatch();
assert_eq!(response.into_string(), Some("<none>".into()));
}
#[test]
fn check_no_proxy_proto_header_works() {
let client = Client::debug(rocket_with_proto_header(None)).unwrap();
let response = client.get("/")
.header(Header::new("X-Forwarded-Proto", "https"))
.dispatch();
assert_eq!(response.into_string(), Some("<none>".into()));
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("/")
.header(Header::new("X-Forwarded-Proto", "https"))
.dispatch();
assert_eq!(response.into_string(), Some("<none>".into()));
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("/")
.header(Header::new("x-Forwarded-Proto", "https"))
.dispatch();
assert_eq!(response.into_string(), Some("https".into()));
}
}

View File

@ -17,28 +17,31 @@ is configured with. This means that no matter which configuration provider
Rocket is asked to use, it must be able to read the following configuration Rocket is asked to use, it must be able to read the following configuration
values: values:
| key | kind | description | debug/release default | | key | kind | description | debug/release default |
|-----------------|-------------------|-------------------------------------------------|-------------------------| |----------------------|-------------------|-------------------------------------------------|-------------------------|
| `address` | `IpAddr` | IP address to serve on | `127.0.0.1` | | `address` | `IpAddr` | IP address to serve on. | `127.0.0.1` |
| `port` | `u16` | Port to serve on. | `8000` | | `port` | `u16` | Port to serve on. | `8000` |
| `workers`* | `usize` | Number of threads to use for executing futures. | cpu core count | | `workers`* | `usize` | Number of threads to use for executing futures. | cpu core count |
| `max_blocking`* | `usize` | Limit on threads to start for blocking tasks. | `512` | | `max_blocking`* | `usize` | Limit on threads to start for blocking tasks. | `512` |
| `ident` | `string`, `false` | If and how to identify via the `Server` header. | `"Rocket"` | | `ident` | `string`, `false` | If and how to identify via the `Server` header. | `"Rocket"` |
| `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` | | `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` |
| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | | `proxy_proto_header` | `string`, `false` | Header identifying [client to proxy protocol]. | `None` |
| `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | | `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` |
| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` | | `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` |
| `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` | | `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` |
| `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` | | `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` |
| `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] | | `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` |
| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" | | `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] |
| `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` | | `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" |
| `shutdown`* | [`Shutdown`] | Graceful shutdown configuration. | [`Shutdown::default()`] | | `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` |
| `shutdown`* | [`Shutdown`] | Graceful shutdown configuration. | [`Shutdown::default()`] |
<small>* Note: the `workers`, `max_blocking`, and `shutdown.force` configuration <small>* Note: the `workers`, `max_blocking`, and `shutdown.force` configuration
parameters are only read from the [default provider](#default-provider).</small> parameters are only read from the [default provider](#default-provider).</small>
[client's real IP]: @api/rocket/request/struct.Request.html#method.real_ip [client's real IP]: @api/rocket/request/struct.Request.html#method.real_ip
[client to proxy protocol]: @api/rocket/request/struct.Request.html#method.proxy_proto
### Profiles ### Profiles
@ -130,6 +133,7 @@ port = 9001
[release] [release]
port = 9999 port = 9999
ip_header = false ip_header = false
proxy_proto_header = "X-Forwarded-Proto"
# NOTE: Don't (!) use this key! Generate your own and keep it private! # NOTE: Don't (!) use this key! Generate your own and keep it private!
# e.g. via `head -c64 /dev/urandom | base64` # e.g. via `head -c64 /dev/urandom | base64`
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk=" secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
@ -148,7 +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` to disable ip_header = "X-Real-IP" # set to `false` or `None` to disable
proxy_proto_header = `false` # set to `false` or `None` to disable
log_level = "normal" log_level = "normal"
temp_dir = "/tmp" temp_dir = "/tmp"
cli_colors = true cli_colors = true
@ -358,6 +363,25 @@ mutual TLS.
[`mtls::Certificate`]: @api/rocket/mtls/struct.Certificate.html [`mtls::Certificate`]: @api/rocket/mtls/struct.Certificate.html
### 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:
```toml,ignore
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
### Workers ### Workers
The `workers` parameter sets the number of threads used for parallel task The `workers` parameter sets the number of threads used for parallel task