Add type-safe 'Host' type, 'Request::host()'.

Closes #1699.
This commit is contained in:
Sergio Benitez 2021-07-02 06:48:40 -07:00
parent f49ee7da00
commit c58b43700c
9 changed files with 498 additions and 24 deletions

View File

@ -4,7 +4,7 @@
//! These types will, with certainty, be removed with time, but they reside here
//! while necessary.
#[doc(hidden)] pub use hyper::{Body, Error, Request, Response};
#[doc(hidden)] pub use hyper::{Body, Error, Request, Response, Version};
#[doc(hidden)] pub use hyper::body::{Bytes, HttpBody, Sender as BodySender};
#[doc(hidden)] pub use hyper::rt::Executor;
#[doc(hidden)] pub use hyper::server::Server;

View File

@ -27,6 +27,17 @@ pub fn authority_from_str(s: &str) -> Result<Authority<'_>, Error<'_>> {
Ok(parse!(authority: RawInput::new(s.as_bytes()))?)
}
#[inline]
pub fn authority_from_bytes(s: &[u8]) -> Result<Authority<'_>, Error<'_>> {
Ok(parse!(authority: RawInput::new(s))?)
}
#[inline]
pub fn scheme_from_str(s: &str) -> Result<&str, Error<'_>> {
let _validated = parse!(scheme: RawInput::new(s.as_bytes()))?;
Ok(s)
}
#[inline]
pub fn absolute_from_str(s: &str) -> Result<Absolute<'_>, Error<'_>> {
Ok(parse!(absolute: RawInput::new(s.as_bytes()))?)

View File

@ -81,12 +81,18 @@ pub fn authority<'a>(input: &mut RawInput<'a>) -> Result<'a, Authority<'a>> {
}
#[parser]
pub fn absolute<'a>(input: &mut RawInput<'a>) -> Result<'a, Absolute<'a>> {
pub fn scheme<'a>(input: &mut RawInput<'a>) -> Result<'a, Extent<&'a [u8]>> {
let scheme = take_some_while(is_scheme_char)?;
if !scheme.get(0).map_or(false, |b| b.is_ascii_alphabetic()) {
parse_error!("invalid scheme")?;
}
scheme
}
#[parser]
pub fn absolute<'a>(input: &mut RawInput<'a>) -> Result<'a, Absolute<'a>> {
let scheme = scheme()?;
let (_, (authority, path), query) = (eat(b':')?, hier_part()?, query()?);
unsafe { Absolute::raw(input.start.into(), scheme, authority, path, query) }
}

View File

@ -426,7 +426,7 @@ impl<'a> Absolute<'a> {
Absolute::const_new(scheme, authority.into(), path, query.into())
}
/// PRIVATE. Used by codegen.
/// PRIVATE. Used by codegen and `Host`.
#[doc(hidden)]
pub const fn const_new(
scheme: &'a str,

View File

@ -46,7 +46,7 @@ use crate::uri::{as_utf8_unchecked, error::Error};
#[derive(Debug, Clone)]
pub struct Authority<'a> {
pub(crate) source: Option<Cow<'a, str>>,
user_info: Option<IndexedStr<'a>>,
pub(crate) user_info: Option<IndexedStr<'a>>,
host: IndexedStr<'a>,
port: Option<u16>,
}
@ -68,7 +68,8 @@ impl<'a> Authority<'a> {
}
}
#[cfg(test)]
/// PRIVATE. Used by core.
#[doc(hidden)]
pub fn new(
user_info: impl Into<Option<&'a str>>,
host: &'a str,
@ -166,14 +167,10 @@ impl<'a> Authority<'a> {
/// Returns the host part of the authority URI.
///
///
/// If the host was provided in brackets (such as for IPv6 addresses), the
/// brackets will not be part of the returned string.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
///
/// let uri = uri!("domain.com:123");
/// assert_eq!(uri.host(), "domain.com");
///

326
core/http/src/uri/host.rs Normal file
View File

@ -0,0 +1,326 @@
use std::fmt::{self, Display};
use crate::uncased::UncasedStr;
use crate::uri::error::Error;
use crate::uri::{Absolute, Authority};
/// A domain and port identified by a client as the server being messaged.
///
/// For requests made via HTTP/1.1, a host is identified via the `HOST` header.
/// In HTTP/2 and HTTP/3, this information is instead communicated via the
/// `:authority` and `:port` pseudo-header request fields. It is a
/// client-controlled value via which the client communicates to the server the
/// domain name and port it is attemping to communicate with. The following
/// diagram illustrates the syntactic structure of a `Host`:
///
/// ```text
/// some.domain.foo:8088
/// |-----------| |--|
/// domain port
/// ```
///
/// Only the domain part is required. Its value is case-insensitive.
///
/// # URI Construction
///
/// A `Host` is _not_ a [`Uri`](crate::uri::Uri), and none of Rocket's APIs will
/// accept a `Host` value as such. This is because doing so would facilitate the
/// construction of URIs to internal routes in a manner controllable by an
/// attacker, inevitably leading to "HTTP Host header attacks".
///
/// Instead, a `Host` must be checked before being converted to a [`Uri`]
/// value. The [`Host::to_authority`] and [`Host::to_absolute`] methods provide
/// these mechanisms:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # type Token = String;
/// use rocket::http::uri::Host;
///
/// // A sensitive URI we want to prefix with safe hosts.
/// #[get("/token?<secret>")]
/// fn token(secret: Token) { /* .. */ }
///
/// // Whitelist of known hosts. In a real setting, you might retrieve this
/// // list from config at ignite-time using tools like `AdHoc::config()`.
/// const WHITELIST: [Host<'static>; 4] = [
/// Host::new(uri!("rocket.rs")),
/// Host::new(uri!("rocket.rs:443")),
/// Host::new(uri!("guide.rocket.rs")),
/// Host::new(uri!("guide.rocket.rs:443")),
/// ];
///
/// // Use `Host::to_absolute()` to case-insensitively check a host against a
/// // whitelist, returning an `Absolute` usable as a `uri!()` prefix.
/// let host = Host::new(uri!("guide.ROCKET.rs"));
/// let prefix = host.to_absolute("https", &WHITELIST);
///
/// // Since `guide.rocket.rs` is in the whitelist, `prefix` is `Some`.
/// assert!(prefix.is_some());
/// if let Some(prefix) = prefix {
/// // We can use this prefix to safely construct URIs.
/// let uri = uri!(prefix, token("some-secret-token"));
/// assert_eq!(uri, "https://guide.ROCKET.rs/token?secret=some-secret-token");
/// }
/// ```
///
/// # (De)serialization
///
/// `Host` is both `Serialize` and `Deserialize`:
///
/// ```rust
/// # #[cfg(feature = "serde")] mod serde {
/// # use serde_ as serde;
/// use serde::{Serialize, Deserialize};
/// use rocket::http::uri::Host;
///
/// #[derive(Deserialize, Serialize)]
/// # #[serde(crate = "serde_")]
/// struct UriOwned {
/// uri: Host<'static>,
/// }
///
/// #[derive(Deserialize, Serialize)]
/// # #[serde(crate = "serde_")]
/// struct UriBorrowed<'a> {
/// uri: Host<'a>,
/// }
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct Host<'a>(Authority<'a>);
impl<'a> Host<'a> {
/// Create a new `Host` from an `Authority`. Only the `host` and `port`
/// parts are preserved.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// let host = Host::new(uri!("developer.mozilla.org"));
/// assert_eq!(host.to_string(), "developer.mozilla.org");
///
/// let host = Host::new(uri!("foo:bar@developer.mozilla.org:1234"));
/// assert_eq!(host.to_string(), "developer.mozilla.org:1234");
///
/// let host = Host::new(uri!("rocket.rs:443"));
/// assert_eq!(host.to_string(), "rocket.rs:443");
/// ```
pub const fn new(authority: Authority<'a>) -> Self {
Host(authority)
}
/// Parses the string `string` into a `Host`. Parsing will never allocate.
/// Returns an `Error` if `string` is not a valid authority URI, meaning
/// that this parser accepts a `user_info` part for compatability but
/// discards it.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// // Parse from a valid authority URI.
/// let host = Host::parse("user:pass@domain").expect("valid host");
/// assert_eq!(host.domain(), "domain");
/// assert_eq!(host.port(), None);
///
/// // Parse from a valid host.
/// let host = Host::parse("domain:311").expect("valid host");
/// assert_eq!(host.domain(), "doMaIN");
/// assert_eq!(host.port(), Some(311));
///
/// // Invalid hosts fail to parse.
/// Host::parse("https://rocket.rs").expect_err("invalid host");
///
/// // Prefer to use `uri!()` when the input is statically known:
/// let host = Host::new(uri!("domain"));
/// assert_eq!(host.domain(), "domain");
/// assert_eq!(host.port(), None);
/// ```
pub fn parse(string: &'a str) -> Result<Host<'a>, Error<'a>> {
Host::parse_bytes(string.as_bytes())
}
/// PRIVATE: Used by core.
#[doc(hidden)]
pub fn parse_bytes(bytes: &'a [u8]) -> Result<Host<'a>, Error<'a>> {
crate::parse::uri::authority_from_bytes(bytes).map(Host::new)
}
/// Parses the string `string` into an `Host`. Parsing never allocates
/// on success. May allocate on error.
///
/// This method should be used instead of [`Host::parse()`] when the source
/// is already a `String`. Returns an `Error` if `string` is not a valid
/// authority URI, meaning that this parser accepts a `user_info` part for
/// compatability but discards it.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::uri::Host;
///
/// let source = format!("rocket.rs:8000");
/// let host = Host::parse_owned(source).expect("valid host");
/// assert_eq!(host.domain(), "rocket.rs");
/// assert_eq!(host.port(), Some(8000));
/// ```
pub fn parse_owned(string: String) -> Result<Host<'static>, Error<'static>> {
Authority::parse_owned(string).map(Host::new)
}
/// Returns the case-insensitive domain part of the host.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// let host = Host::new(uri!("domain.com:123"));
/// assert_eq!(host.domain(), "domain.com");
///
/// let host = Host::new(uri!("username:password@domain:123"));
/// assert_eq!(host.domain(), "domain");
///
/// let host = Host::new(uri!("[1::2]:123"));
/// assert_eq!(host.domain(), "[1::2]");
/// ```
#[inline]
pub fn domain(&self) -> &UncasedStr {
self.0.host().into()
}
/// Returns the port part of the host, if there is one.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// // With a port.
/// let host = Host::new(uri!("domain:123"));
/// assert_eq!(host.port(), Some(123));
///
/// let host = Host::new(uri!("domain.com:8181"));
/// assert_eq!(host.port(), Some(8181));
///
/// // Without a port.
/// let host = Host::new(uri!("domain.foo.bar.tld"));
/// assert_eq!(host.port(), None);
/// ```
#[inline(always)]
pub fn port(&self) -> Option<u16> {
self.0.port()
}
/// Checks `self` against `whitelist`. If `self` is in `whitelist`, returns
/// an [`Authority`] URI representing self. Otherwise, returns `None`.
/// Domain comparison is case-insensitive.
///
/// See [URI construction](Self#uri-construction) for more.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// let whitelist = &[Host::new(uri!("domain.tld"))];
///
/// // A host in the whitelist returns `Some`.
/// let host = Host::new(uri!("domain.tld"));
/// let uri = host.to_authority(whitelist);
/// assert!(uri.is_some());
/// assert_eq!(uri.unwrap().to_string(), "domain.tld");
///
/// let host = Host::new(uri!("foo:bar@doMaIN.tLd"));
/// let uri = host.to_authority(whitelist);
/// assert!(uri.is_some());
/// assert_eq!(uri.unwrap().to_string(), "doMaIN.tLd");
///
/// // A host _not_ in the whitelist returns `None`.
/// let host = Host::new(uri!("domain.tld:1234"));
/// let uri = host.to_authority(whitelist);
/// assert!(uri.is_none());
/// ```
pub fn to_authority<'h, W>(&self, whitelist: W) -> Option<Authority<'a>>
where W: IntoIterator<Item = &'h Host<'h>>
{
let mut auth = whitelist.into_iter().any(|h| h == self).then(|| self.0.clone())?;
auth.user_info = None;
Some(auth)
}
/// Checks `self` against `whitelist`. If `self` is in `whitelist`, returns
/// an [`Absolute`] URI representing `self` with scheme `scheme`. Otherwise,
/// returns `None`. Domain comparison is case-insensitive.
///
/// See [URI construction](Self#uri-construction) for more.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::uri::Host;
///
/// let whitelist = &[Host::new(uri!("domain.tld:443"))];
///
/// // A host in the whitelist returns `Some`.
/// let host = Host::new(uri!("user@domain.tld:443"));
/// let uri = host.to_absolute("http", whitelist);
/// assert!(uri.is_some());
/// assert_eq!(uri.unwrap().to_string(), "http://domain.tld:443");
///
/// let host = Host::new(uri!("domain.TLD:443"));
/// let uri = host.to_absolute("https", whitelist);
/// assert!(uri.is_some());
/// assert_eq!(uri.unwrap().to_string(), "https://domain.TLD:443");
///
/// // A host _not_ in the whitelist returns `None`.
/// let host = Host::new(uri!("domain.tld"));
/// let uri = host.to_absolute("http", whitelist);
/// assert!(uri.is_none());
/// ```
pub fn to_absolute<'h, W>(&self, scheme: &'a str, whitelist: W) -> Option<Absolute<'a>>
where W: IntoIterator<Item = &'h Host<'h>>
{
let scheme = crate::parse::uri::scheme_from_str(scheme).ok()?;
let authority = self.to_authority(whitelist)?;
Some(Absolute::const_new(scheme, Some(authority), "", None))
}
}
impl_serde!(Host<'a>, "an HTTP host");
impl_base_traits!(Host, domain, port);
impl crate::ext::IntoOwned for Host<'_> {
type Owned = Host<'static>;
fn into_owned(self) -> Host<'static> {
Host(self.0.into_owned())
}
}
impl<'a> From<Authority<'a>> for Host<'a> {
fn from(auth: Authority<'a>) -> Self {
Host::new(auth)
}
}
impl Display for Host<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.domain().fmt(f)?;
if let Some(port) = self.port() {
write!(f, ":{}", port)?;
}
Ok(())
}
}

View File

@ -9,6 +9,7 @@ mod absolute;
mod segments;
mod path_query;
mod asterisk;
mod host;
pub mod error;
pub mod fmt;
@ -24,3 +25,4 @@ pub use self::segments::*;
pub use self::reference::*;
pub use self::path_query::*;
pub use self::asterisk::*;
pub use self::host::*;

View File

@ -389,8 +389,26 @@ macro_rules! impl_serde {
};
}
/// Implements PartialEq, Eq, Hash, TryFrom, and IntoOwned for a URI.
/// Implements traits from `impl_base_traits` and IntoOwned for a URI.
macro_rules! impl_traits {
($T:ident, $($field:ident),* $(,)?) => {
impl_base_traits!($T, $($field),*);
impl crate::ext::IntoOwned for $T<'_> {
type Owned = $T<'static>;
fn into_owned(self) -> $T<'static> {
$T {
source: self.source.into_owned(),
$($field: self.$field.into_owned()),*
}
}
}
}
}
/// Implements PartialEq, Eq, Hash, and TryFrom.
macro_rules! impl_base_traits {
($T:ident, $($field:ident),* $(,)?) => {
impl std::convert::TryFrom<String> for $T<'static> {
type Error = Error<'static>;
@ -448,16 +466,5 @@ macro_rules! impl_traits {
$(self.$field().hash(state);)*
}
}
impl crate::ext::IntoOwned for $T<'_> {
type Owned = $T<'static>;
fn into_owned(self) -> $T<'static> {
$T {
source: self.source.into_owned(),
$($field: self.$field.into_owned()),*
}
}
}
}
}

View File

@ -13,9 +13,10 @@ use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::form::{self, ValueField, FromForm};
use crate::{Rocket, Route, Orbit};
use crate::http::{hyper, uri::{Origin, Segments, fmt::Path}, uncased::UncasedStr};
use crate::http::{Method, Header, HeaderMap};
use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority};
use crate::http::{hyper, Method, Header, HeaderMap};
use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie};
use crate::http::uncased::UncasedStr;
use crate::data::Limits;
/// The type of an incoming web request.
@ -27,6 +28,7 @@ use crate::data::Limits;
pub struct Request<'r> {
method: Atomic<Method>,
uri: Origin<'r>,
host: Option<Host<'r>>,
headers: HeaderMap<'r>,
remote: Option<SocketAddr>,
pub(crate) state: RequestState<'r>,
@ -46,6 +48,7 @@ impl Request<'_> {
Request {
method: Atomic::new(self.method()),
uri: self.uri.clone(),
host: self.host.clone(),
headers: self.headers.clone(),
remote: self.remote,
state: self.state.clone(),
@ -76,6 +79,7 @@ impl<'r> Request<'r> {
) -> Request<'r> {
Request {
uri,
host: None,
method: Atomic::new(method),
headers: HeaderMap::new(),
remote: None,
@ -168,6 +172,119 @@ impl<'r> Request<'r> {
self.uri = uri;
}
/// Returns the [`Host`] identified in the request, if any.
///
/// If the request is made via HTTP/1.1 (or earlier), this method returns
/// the value in the `HOST` header without the deprecated `user_info`
/// component. Otherwise, this method returns the contents of the
/// `:authority` pseudo-header request field.
///
/// # ⚠️ DANGER ⚠️
///
/// Using the user-controlled `host` to construct URLs is a security hazard!
/// _Never_ do so without first validating the host against a whitelist. For
/// this reason, Rocket disallows constructing host-prefixed URIs with
/// [`uri!`]. _Always_ use [`uri!`] to construct URIs.
///
/// [`uri!`]: crate::uri!
///
/// # Example
///
/// Retrieve the raw host, unusable to construct safe URIs:
///
/// ```rust
/// use rocket::http::uri::Host;
/// # use rocket::uri;
/// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap();
/// # let mut req = c.get("/");
/// # let request = req.inner_mut();
///
/// assert_eq!(request.host(), None);
///
/// request.set_host(Host::from(uri!("rocket.rs")));
/// let host = request.host().unwrap();
/// assert_eq!(host.domain(), "rocket.rs");
/// assert_eq!(host.port(), None);
///
/// request.set_host(Host::from(uri!("rocket.rs:2392")));
/// let host = request.host().unwrap();
/// assert_eq!(host.domain(), "rocket.rs");
/// assert_eq!(host.port(), Some(2392));
/// ```
///
/// Retrieve the raw host, check it against a whitelist, and construct a
/// URI:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # type Token = String;
/// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap();
/// # let mut req = c.get("/");
/// # let request = req.inner_mut();
/// use rocket::http::uri::Host;
///
/// // A sensitive URI we want to prefix with safe hosts.
/// #[get("/token?<secret>")]
/// fn token(secret: Token) { /* .. */ }
///
/// // Whitelist of known hosts. In a real setting, you might retrieve this
/// // list from config at ignite-time using tools like `AdHoc::config()`.
/// const WHITELIST: [Host<'static>; 3] = [
/// Host::new(uri!("rocket.rs")),
/// Host::new(uri!("rocket.rs:443")),
/// Host::new(uri!("guide.rocket.rs:443")),
/// ];
///
/// // A request with a host of "rocket.rs". Note the case-insensitivity.
/// request.set_host(Host::from(uri!("ROCKET.rs")));
/// let prefix = request.host().and_then(|h| h.to_absolute("https", &WHITELIST));
///
/// // `rocket.rs` is in the whitelist, so we'll get back a `Some`.
/// assert!(prefix.is_some());
/// if let Some(prefix) = prefix {
/// // We can use this prefix to safely construct URIs.
/// let uri = uri!(prefix, token("some-secret-token"));
/// assert_eq!(uri, "https://ROCKET.rs/token?secret=some-secret-token");
/// }
///
/// // A request with a host of "attacker-controlled.com".
/// request.set_host(Host::from(uri!("attacker-controlled.com")));
/// let prefix = request.host().and_then(|h| h.to_absolute("https", &WHITELIST));
///
/// // `attacker-controlled.come` is _not_ on the whitelist.
/// assert!(prefix.is_none());
/// assert!(request.host().is_some());
/// ```
#[inline(always)]
pub fn host(&self) -> Option<&Host<'r>> {
self.host.as_ref()
}
/// Sets the host of `self` to `host`.
///
/// # Example
///
/// Set the host to `rocket.rs:443`.
///
/// ```rust
/// use rocket::http::uri::Host;
/// # use rocket::uri;
/// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap();
/// # let mut req = c.get("/");
/// # let request = req.inner_mut();
///
/// assert_eq!(request.host(), None);
///
/// request.set_host(Host::from(uri!("rocket.rs:443")));
/// let host = request.host().unwrap();
/// assert_eq!(host.domain(), "rocket.rs");
/// assert_eq!(host.port(), Some(443));
/// ```
#[inline(always)]
pub fn set_host(&mut self, host: Host<'r>) {
self.host = Some(host);
}
/// Returns the raw address of the remote connection that initiated this
/// request if the address is known. If the address is not known, `None` is
/// returned.
@ -850,6 +967,14 @@ impl<'r> Request<'r> {
let mut request = Request::new(rocket, method, uri);
request.set_remote(addr);
// Determine the host. On HTTP < 2, use the `HOST` header. Otherwise,
// use the `:authority` pseudo-header which hyper makes part of the URI.
request.host = if hyper.version < hyper::Version::HTTP_2 {
hyper.headers.get("host").and_then(|h| Host::parse_bytes(h.as_bytes()).ok())
} else {
hyper.uri.host().map(|h| Host::new(Authority::new(None, h, hyper.uri.port_u16())))
};
// Set the request cookies, if they exist.
for header in hyper.headers.get_all("Cookie") {
let raw_str = match std::str::from_utf8(header.as_bytes()) {