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 accept;
mod header;
mod proxy_proto;
pub use self::content_type::ContentType;
pub use self::accept::{Accept, QMediaType};
pub use self::media_type::MediaType;
pub use self::header::{Header, HeaderMap};
pub use self::proxy_proto::ProxyProto;
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
/// [`Request::real_ip()`].
///
/// To disable using any header for this purpose, set this value to `false`.
/// Deserialization semantics are identical to those of [`Ident`] except
/// that the value must syntactically be a valid HTTP header name.
/// To disable using any header for this purpose, set this value to `false`
/// or `None`. Deserialization semantics are identical to those of [`Ident`]
/// except that the value must syntactically be a valid HTTP header name.
///
/// **(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>>,
/// 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()`])**
pub limits: Limits,
/// Directory to store temporary files in. **(default:
@ -189,6 +204,7 @@ impl Config {
max_blocking: 512,
ident: Ident::default(),
ip_header: Some(Uncased::from_borrowed("X-Real-IP")),
proxy_proto_header: None,
limits: Limits::default(),
temp_dir: std::env::temp_dir().into(),
keep_alive: 5,
@ -409,6 +425,11 @@ impl Config {
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_!("temp dir: {}", self.temp_dir.relative().display().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`].
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`].
pub const LIMITS: &'static str = "limits";
@ -536,10 +560,9 @@ impl Config {
/// An array of all of the stringy parameter names.
pub const PARAMETERS: &'static [&'static str] = &[
Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING,
Self::KEEP_ALIVE, Self::IDENT, Self::IP_HEADER, Self::LIMITS, Self::TLS,
Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN,
Self::CLI_COLORS,
Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, Self::KEEP_ALIVE,
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::CLI_COLORS,
];
}

View File

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

View File

@ -2,8 +2,8 @@ use std::fmt;
use parking_lot::Mutex;
use crate::Config;
use crate::http::private::cookie;
use crate::{Rocket, Orbit};
#[doc(inline)]
pub use self::cookie::{Cookie, SameSite, Iter};
@ -154,17 +154,14 @@ pub use self::cookie::{Cookie, SameSite, Iter};
pub struct CookieJar<'a> {
jar: cookie::CookieJar,
ops: Mutex<Vec<Op>>,
config: &'a Config,
pub(crate) state: CookieState<'a>,
}
impl<'a> Clone for CookieJar<'a> {
fn clone(&self) -> Self {
CookieJar {
jar: self.jar.clone(),
ops: Mutex::new(self.ops.lock().clone()),
config: self.config,
}
}
#[derive(Copy, Clone)]
pub(crate) struct CookieState<'a> {
pub secure: bool,
#[cfg_attr(not(feature = "secrets"), allow(unused))]
pub config: &'a crate::Config,
}
#[derive(Clone)]
@ -173,22 +170,17 @@ enum Op {
Remove(Cookie<'static>, bool),
}
impl Op {
fn cookie(&self) -> &Cookie<'static> {
match self {
Op::Add(c, _) | Op::Remove(c, _) => c
}
}
}
impl<'a> CookieJar<'a> {
#[inline(always)]
pub(crate) fn new(config: &'a Config) -> Self {
CookieJar::from(cookie::CookieJar::new(), config)
}
pub(crate) fn from(jar: cookie::CookieJar, config: &'a Config) -> Self {
CookieJar { jar, config, ops: Mutex::new(Vec::new()) }
pub(crate) fn new(base: Option<cookie::CookieJar>, rocket: &'a Rocket<Orbit>) -> Self {
CookieJar {
jar: base.unwrap_or_default(),
ops: Mutex::new(Vec::new()),
state: CookieState {
// This is updated dynamically when headers are received.
secure: rocket.config().tls_enabled(),
config: rocket.config(),
}
}
}
/// Returns a reference to the _original_ `Cookie` inside this container
@ -236,7 +228,7 @@ impl<'a> CookieJar<'a> {
#[cfg(feature = "secrets")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
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
@ -287,7 +279,10 @@ impl<'a> CookieJar<'a> {
/// * `path`: `"/"`
/// * `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
///
@ -309,7 +304,7 @@ impl<'a> CookieJar<'a> {
/// ```
pub fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
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));
}
@ -327,9 +322,10 @@ impl<'a> CookieJar<'a> {
/// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now
///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. These
/// defaults ensure maximum usability and security. For additional security,
/// you may wish to set the `secure` flag.
/// 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
///
@ -346,7 +342,7 @@ impl<'a> CookieJar<'a> {
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn add_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
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));
}
@ -476,7 +472,7 @@ impl<'a> CookieJar<'a> {
Op::Add(c, false) => jar.add(c),
#[cfg(feature = "secrets")]
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, _) => {
if self.jar.get(c.name()).is_some() {
@ -505,7 +501,7 @@ impl<'a> CookieJar<'a> {
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
#[inline(always)]
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
@ -515,8 +511,9 @@ impl<'a> CookieJar<'a> {
/// * `path`: `"/"`
/// * `SameSite`: `Strict`
///
/// Furthermore, if TLS is enabled, the `Secure` cookie flag is set.
fn set_defaults(config: &Config, cookie: &mut Cookie<'static>) {
/// 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>) {
if cookie.path().is_none() {
cookie.set_path("/");
}
@ -525,7 +522,7 @@ impl<'a> CookieJar<'a> {
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);
}
}
@ -554,17 +551,12 @@ impl<'a> CookieJar<'a> {
/// * `HttpOnly`: `true`
/// * `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_attr(nightly, doc(cfg(feature = "secrets")))]
fn set_private_defaults(config: &Config, cookie: &mut Cookie<'static>) {
if cookie.path().is_none() {
cookie.set_path("/");
}
if cookie.same_site().is_none() {
cookie.set_same_site(SameSite::Strict);
}
fn set_private_defaults(&self, cookie: &mut Cookie<'static>) {
self.set_defaults(cookie);
if cookie.http_only().is_none() {
cookie.set_http_only(true);
@ -573,10 +565,6 @@ impl<'a> CookieJar<'a> {
if cookie.expires().is_none() {
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)
.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 _) };
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 mut cookies = CookieJar::new(request.rocket().config());
let mut cookies = CookieJar::new(None, request.rocket());
for cookie in response.cookies() {
cookies.add_original(cookie.into_owned());
}

View File

@ -181,9 +181,8 @@ macro_rules! pub_client_impl {
/// ```
#[inline(always)]
pub fn cookies(&self) -> crate::http::CookieJar<'_> {
let config = &self.rocket().config();
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);

View File

@ -6,7 +6,7 @@ use crate::{Request, Route};
use crate::outcome::{self, Outcome::*};
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.
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,
/// 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**
///
/// 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]
impl<'r> FromRequest<'r> for SocketAddr {
type Error = Infallible;

View File

@ -13,9 +13,8 @@ use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::form::{self, ValueField, FromForm};
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::uncased::UncasedStr;
use crate::http::private::Certificates;
use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority};
@ -97,7 +96,7 @@ impl<'r> Request<'r> {
state: RequestState {
rocket,
route: Atomic::new(None),
cookies: CookieJar::new(rocket.config()),
cookies: CookieJar::new(None, rocket),
accept: InitCell::new(),
content_type: InitCell::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
/// [`ip_header`](crate::Config::ip_header) and then using the remote
/// connection's IP address. Note that the built-in `IpAddr` request guard
@ -497,7 +575,7 @@ impl<'r> Request<'r> {
#[inline]
pub fn add_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) {
let header = header.into();
self.bust_header_cache(header.name(), false);
self.bust_header_cache(&header, false);
self.headers.add(header);
}
@ -526,7 +604,7 @@ impl<'r> Request<'r> {
#[inline]
pub fn replace_header<'h: 'r, H: Into<Header<'h>>>(&mut self, header: H) {
let header = header.into();
self.bust_header_cache(header.name(), true);
self.bust_header_cache(&header, true);
self.headers.replace(header);
}
@ -547,9 +625,9 @@ impl<'r> Request<'r> {
/// ```
#[inline]
pub fn content_type(&self) -> Option<&ContentType> {
self.state.content_type.get_or_init(|| {
self.headers().get_one("Content-Type").and_then(|v| v.parse().ok())
}).as_ref()
self.state.content_type
.get_or_init(|| self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()))
.as_ref()
}
/// Returns the Accept header of `self`. If the header is not present,
@ -567,9 +645,9 @@ impl<'r> Request<'r> {
/// ```
#[inline]
pub fn accept(&self) -> Option<&Accept> {
self.state.accept.get_or_init(|| {
self.headers().get_one("Accept").and_then(|v| v.parse().ok())
}).as_ref()
self.state.accept
.get_or_init(|| self.headers().get_one("Accept").and_then(|v| v.parse().ok()))
.as_ref()
}
/// Returns the media type "format" of the request.
@ -925,15 +1003,19 @@ impl<'r> Request<'r> {
#[doc(hidden)]
impl<'r> Request<'r> {
/// Resets the cached value (if any) for the header with name `name`.
fn bust_header_cache(&mut self, name: &UncasedStr, replace: bool) {
if name == "Content-Type" {
fn bust_header_cache(&mut self, header: &Header<'_>, replace: bool) {
if header.name() == "Content-Type" {
if self.content_type().is_none() || replace {
self.state.content_type = InitCell::new();
}
} else if name == "Accept" {
} else if header.name() == "Accept" {
if self.accept().is_none() || replace {
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
values:
| key | kind | description | debug/release default |
|-----------------|-------------------|-------------------------------------------------|-------------------------|
| `address` | `IpAddr` | IP address to serve on | `127.0.0.1` |
| `port` | `u16` | Port to serve on. | `8000` |
| `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` |
| `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"` |
| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` |
| `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` |
| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` |
| `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` |
| `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` |
| `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] |
| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" |
| `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` |
| `shutdown`* | [`Shutdown`] | Graceful shutdown configuration. | [`Shutdown::default()`] |
| key | kind | description | debug/release default |
|----------------------|-------------------|-------------------------------------------------|-------------------------|
| `address` | `IpAddr` | IP address to serve on. | `127.0.0.1` |
| `port` | `u16` | Port to serve on. | `8000` |
| `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` |
| `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"` |
| `proxy_proto_header` | `string`, `false` | Header identifying [client to proxy protocol]. | `None` |
| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` |
| `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` |
| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` |
| `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` |
| `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` |
| `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] |
| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" |
| `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
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 to proxy protocol]: @api/rocket/request/struct.Request.html#method.proxy_proto
### Profiles
@ -130,6 +133,7 @@ port = 9001
[release]
port = 9999
ip_header = false
proxy_proto_header = "X-Forwarded-Proto"
# NOTE: Don't (!) use this key! Generate your own and keep it private!
# e.g. via `head -c64 /dev/urandom | base64`
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
@ -148,7 +152,8 @@ workers = 16
max_blocking = 512
keep_alive = 5
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"
temp_dir = "/tmp"
cli_colors = true
@ -358,6 +363,25 @@ mutual TLS.
[`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
The `workers` parameter sets the number of threads used for parallel task