rocket_http/uri/host.rs
1use std::fmt::{self, Display};
2
3use crate::uncased::UncasedStr;
4use crate::uri::error::Error;
5use crate::uri::{Absolute, Authority};
6
7/// A domain and port identified by a client as the server being messaged.
8///
9/// For requests made via HTTP/1.1, a host is identified via the `HOST` header.
10/// In HTTP/2 and HTTP/3, this information is instead communicated via the
11/// `:authority` and `:port` pseudo-header request fields. It is a
12/// client-controlled value via which the client communicates to the server the
13/// domain name and port it is attempting to communicate with. The following
14/// diagram illustrates the syntactic structure of a `Host`:
15///
16/// ```text
17/// some.domain.foo:8088
18/// |-------------| |--|
19/// domain port
20/// ```
21///
22/// Only the domain part is required. Its value is case-insensitive.
23///
24/// # URI Construction
25///
26/// A `Host` is _not_ a [`Uri`](crate::uri::Uri), and none of Rocket's APIs will
27/// accept a `Host` value as such. This is because doing so would facilitate the
28/// construction of URIs to internal routes in a manner controllable by an
29/// attacker, inevitably leading to "HTTP Host header attacks".
30///
31/// Instead, a `Host` must be checked before being converted to a [`Uri`]
32/// value. The [`Host::to_authority`] and [`Host::to_absolute`] methods provide
33/// these mechanisms:
34///
35/// ```rust
36/// # #[macro_use] extern crate rocket;
37/// # type Token = String;
38/// use rocket::http::uri::Host;
39///
40/// // A sensitive URI we want to prefix with safe hosts.
41/// #[get("/token?<secret>")]
42/// fn token(secret: Token) { /* .. */ }
43///
44/// // Whitelist of known hosts. In a real setting, you might retrieve this
45/// // list from config at ignite-time using tools like `AdHoc::config()`.
46/// const WHITELIST: [Host<'static>; 4] = [
47/// Host::new(uri!("rocket.rs")),
48/// Host::new(uri!("rocket.rs:443")),
49/// Host::new(uri!("guide.rocket.rs")),
50/// Host::new(uri!("guide.rocket.rs:443")),
51/// ];
52///
53/// // Use `Host::to_absolute()` to case-insensitively check a host against a
54/// // whitelist, returning an `Absolute` usable as a `uri!()` prefix.
55/// let host = Host::new(uri!("guide.ROCKET.rs"));
56/// let prefix = host.to_absolute("https", &WHITELIST);
57///
58/// // Since `guide.rocket.rs` is in the whitelist, `prefix` is `Some`.
59/// assert!(prefix.is_some());
60/// if let Some(prefix) = prefix {
61/// // We can use this prefix to safely construct URIs.
62/// let uri = uri!(prefix, token("some-secret-token"));
63/// assert_eq!(uri, "https://guide.ROCKET.rs/token?secret=some-secret-token");
64/// }
65/// ```
66///
67/// # (De)serialization
68///
69/// `Host` is both `Serialize` and `Deserialize`:
70///
71/// ```rust
72/// # #[cfg(feature = "serde")] mod serde {
73/// # use serde_ as serde;
74/// use serde::{Serialize, Deserialize};
75/// use rocket::http::uri::Host;
76///
77/// #[derive(Deserialize, Serialize)]
78/// # #[serde(crate = "serde_")]
79/// struct UriOwned {
80/// uri: Host<'static>,
81/// }
82///
83/// #[derive(Deserialize, Serialize)]
84/// # #[serde(crate = "serde_")]
85/// struct UriBorrowed<'a> {
86/// uri: Host<'a>,
87/// }
88/// # }
89/// ```
90#[derive(Debug, Clone)]
91pub struct Host<'a>(Authority<'a>);
92
93impl<'a> Host<'a> {
94 /// Create a new `Host` from an `Authority`. Only the `host` and `port`
95 /// parts are preserved.
96 ///
97 /// ```rust
98 /// # #[macro_use] extern crate rocket;
99 /// use rocket::http::uri::Host;
100 ///
101 /// let host = Host::new(uri!("developer.mozilla.org"));
102 /// assert_eq!(host.to_string(), "developer.mozilla.org");
103 ///
104 /// let host = Host::new(uri!("foo:bar@developer.mozilla.org:1234"));
105 /// assert_eq!(host.to_string(), "developer.mozilla.org:1234");
106 ///
107 /// let host = Host::new(uri!("rocket.rs:443"));
108 /// assert_eq!(host.to_string(), "rocket.rs:443");
109 /// ```
110 pub const fn new(authority: Authority<'a>) -> Self {
111 Host(authority)
112 }
113
114 /// Parses the string `string` into a `Host`. Parsing will never allocate.
115 /// Returns an `Error` if `string` is not a valid authority URI, meaning
116 /// that this parser accepts a `user_info` part for compatibility but
117 /// discards it.
118 ///
119 /// # Example
120 ///
121 /// ```rust
122 /// # #[macro_use] extern crate rocket;
123 /// use rocket::http::uri::Host;
124 ///
125 /// // Parse from a valid authority URI.
126 /// let host = Host::parse("user:pass@domain").expect("valid host");
127 /// assert_eq!(host.domain(), "domain");
128 /// assert_eq!(host.port(), None);
129 ///
130 /// // Parse from a valid host.
131 /// let host = Host::parse("domain:311").expect("valid host");
132 /// assert_eq!(host.domain(), "doMaIN");
133 /// assert_eq!(host.port(), Some(311));
134 ///
135 /// // Invalid hosts fail to parse.
136 /// Host::parse("https://rocket.rs").expect_err("invalid host");
137 ///
138 /// // Prefer to use `uri!()` when the input is statically known:
139 /// let host = Host::new(uri!("domain"));
140 /// assert_eq!(host.domain(), "domain");
141 /// assert_eq!(host.port(), None);
142 /// ```
143 pub fn parse(string: &'a str) -> Result<Host<'a>, Error<'a>> {
144 Host::parse_bytes(string.as_bytes())
145 }
146
147 /// PRIVATE: Used by core.
148 #[doc(hidden)]
149 pub fn parse_bytes(bytes: &'a [u8]) -> Result<Host<'a>, Error<'a>> {
150 crate::parse::uri::authority_from_bytes(bytes).map(Host::new)
151 }
152
153 /// Parses the string `string` into an `Host`. Parsing never allocates
154 /// on success. May allocate on error.
155 ///
156 /// This method should be used instead of [`Host::parse()`] when the source
157 /// is already a `String`. Returns an `Error` if `string` is not a valid
158 /// authority URI, meaning that this parser accepts a `user_info` part for
159 /// compatibility but discards it.
160 ///
161 /// # Example
162 ///
163 /// ```rust
164 /// # extern crate rocket;
165 /// use rocket::http::uri::Host;
166 ///
167 /// let source = format!("rocket.rs:8000");
168 /// let host = Host::parse_owned(source).expect("valid host");
169 /// assert_eq!(host.domain(), "rocket.rs");
170 /// assert_eq!(host.port(), Some(8000));
171 /// ```
172 pub fn parse_owned(string: String) -> Result<Host<'static>, Error<'static>> {
173 Authority::parse_owned(string).map(Host::new)
174 }
175
176 /// Returns the case-insensitive domain part of the host.
177 ///
178 /// # Example
179 ///
180 /// ```rust
181 /// # #[macro_use] extern crate rocket;
182 /// use rocket::http::uri::Host;
183 ///
184 /// let host = Host::new(uri!("domain.com:123"));
185 /// assert_eq!(host.domain(), "domain.com");
186 ///
187 /// let host = Host::new(uri!("username:password@domain:123"));
188 /// assert_eq!(host.domain(), "domain");
189 ///
190 /// let host = Host::new(uri!("[1::2]:123"));
191 /// assert_eq!(host.domain(), "[1::2]");
192 /// ```
193 #[inline]
194 pub fn domain(&self) -> &UncasedStr {
195 self.0.host().into()
196 }
197
198 /// Returns the port part of the host, if there is one.
199 ///
200 /// # Example
201 ///
202 /// ```rust
203 /// # #[macro_use] extern crate rocket;
204 /// use rocket::http::uri::Host;
205 ///
206 /// // With a port.
207 /// let host = Host::new(uri!("domain:123"));
208 /// assert_eq!(host.port(), Some(123));
209 ///
210 /// let host = Host::new(uri!("domain.com:8181"));
211 /// assert_eq!(host.port(), Some(8181));
212 ///
213 /// // Without a port.
214 /// let host = Host::new(uri!("domain.foo.bar.tld"));
215 /// assert_eq!(host.port(), None);
216 /// ```
217 #[inline(always)]
218 pub fn port(&self) -> Option<u16> {
219 self.0.port()
220 }
221
222 /// Checks `self` against `whitelist`. If `self` is in `whitelist`, returns
223 /// an [`Authority`] URI representing self. Otherwise, returns `None`.
224 /// Domain comparison is case-insensitive.
225 ///
226 /// See [URI construction](Self#uri-construction) for more.
227 ///
228 /// # Example
229 ///
230 /// ```rust
231 /// # #[macro_use] extern crate rocket;
232 /// use rocket::http::uri::Host;
233 ///
234 /// let whitelist = &[Host::new(uri!("domain.tld"))];
235 ///
236 /// // A host in the whitelist returns `Some`.
237 /// let host = Host::new(uri!("domain.tld"));
238 /// let uri = host.to_authority(whitelist);
239 /// assert!(uri.is_some());
240 /// assert_eq!(uri.unwrap().to_string(), "domain.tld");
241 ///
242 /// let host = Host::new(uri!("foo:bar@doMaIN.tLd"));
243 /// let uri = host.to_authority(whitelist);
244 /// assert!(uri.is_some());
245 /// assert_eq!(uri.unwrap().to_string(), "doMaIN.tLd");
246 ///
247 /// // A host _not_ in the whitelist returns `None`.
248 /// let host = Host::new(uri!("domain.tld:1234"));
249 /// let uri = host.to_authority(whitelist);
250 /// assert!(uri.is_none());
251 /// ```
252 pub fn to_authority<'h, W>(&self, whitelist: W) -> Option<Authority<'a>>
253 where W: IntoIterator<Item = &'h Host<'h>>
254 {
255 let mut auth = whitelist.into_iter().any(|h| h == self).then(|| self.0.clone())?;
256 auth.user_info = None;
257 Some(auth)
258 }
259
260 /// Checks `self` against `whitelist`. If `self` is in `whitelist`, returns
261 /// an [`Absolute`] URI representing `self` with scheme `scheme`. Otherwise,
262 /// returns `None`. Domain comparison is case-insensitive.
263 ///
264 /// See [URI construction](Self#uri-construction) for more.
265 ///
266 /// # Example
267 ///
268 /// ```rust
269 /// # #[macro_use] extern crate rocket;
270 /// use rocket::http::uri::Host;
271 ///
272 /// let whitelist = &[Host::new(uri!("domain.tld:443"))];
273 ///
274 /// // A host in the whitelist returns `Some`.
275 /// let host = Host::new(uri!("user@domain.tld:443"));
276 /// let uri = host.to_absolute("http", whitelist);
277 /// assert!(uri.is_some());
278 /// assert_eq!(uri.unwrap().to_string(), "http://domain.tld:443");
279 ///
280 /// let host = Host::new(uri!("domain.TLD:443"));
281 /// let uri = host.to_absolute("https", whitelist);
282 /// assert!(uri.is_some());
283 /// assert_eq!(uri.unwrap().to_string(), "https://domain.TLD:443");
284 ///
285 /// // A host _not_ in the whitelist returns `None`.
286 /// let host = Host::new(uri!("domain.tld"));
287 /// let uri = host.to_absolute("http", whitelist);
288 /// assert!(uri.is_none());
289 /// ```
290 pub fn to_absolute<'h, W>(&self, scheme: &'a str, whitelist: W) -> Option<Absolute<'a>>
291 where W: IntoIterator<Item = &'h Host<'h>>
292 {
293 let scheme = crate::parse::uri::scheme_from_str(scheme).ok()?;
294 let authority = self.to_authority(whitelist)?;
295 Some(Absolute::const_new(scheme, Some(authority), "", None))
296 }
297}
298
299impl_serde!(Host<'a>, "an HTTP host");
300
301impl_base_traits!(Host, domain, port);
302
303impl crate::ext::IntoOwned for Host<'_> {
304 type Owned = Host<'static>;
305
306 fn into_owned(self) -> Host<'static> {
307 Host(self.0.into_owned())
308 }
309}
310
311impl<'a> From<Authority<'a>> for Host<'a> {
312 fn from(auth: Authority<'a>) -> Self {
313 Host::new(auth)
314 }
315}
316
317impl Display for Host<'_> {
318 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319 self.domain().fmt(f)?;
320 if let Some(port) = self.port() {
321 write!(f, ":{}", port)?;
322 }
323
324 Ok(())
325 }
326}