actix_web/guard/
host.rs

1use actix_http::{header, uri::Uri, RequestHead, Version};
2
3use super::{Guard, GuardContext};
4
5/// Creates a guard that matches requests targeting a specific host.
6///
7/// # Matching Host
8/// This guard will:
9/// - match against the `Host` header, if present;
10/// - fall-back to matching against the request target's host, if present;
11/// - return false if host cannot be determined;
12///
13/// # Matching Scheme
14/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
15/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
16/// the guard from matching successfully.
17///
18/// # Examples
19/// The `Host` guard can be used to set up a form of [virtual hosting] within a single app.
20/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
21/// definitions they become safe to use in this way. Without these host guards, only routes under
22/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
23/// and `localhost` as the `Host` guards.
24/// ```
25/// use actix_web::{web, http::Method, guard, App, HttpResponse};
26///
27/// App::new()
28///     .service(
29///         web::scope("")
30///             .guard(guard::Host("www.rust-lang.org"))
31///             .default_service(web::to(|| async {
32///                 HttpResponse::Ok().body("marketing site")
33///             })),
34///     )
35///     .service(
36///         web::scope("")
37///             .guard(guard::Host("play.rust-lang.org"))
38///             .default_service(web::to(|| async {
39///                 HttpResponse::Ok().body("playground frontend")
40///             })),
41///     );
42/// ```
43///
44/// The example below additionally guards on the host URI's scheme. This could allow routing to
45/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
46/// ```
47/// use actix_web::{web, guard::Host, HttpResponse};
48///
49/// web::scope("/admin")
50///     .guard(Host("admin.rust-lang.org").scheme("https"))
51///     .default_service(web::to(|| async {
52///         HttpResponse::Ok().body("admin connection is secure")
53///     }));
54/// ```
55///
56/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
57#[allow(non_snake_case)]
58pub fn Host(host: impl AsRef<str>) -> HostGuard {
59    HostGuard {
60        host: host.as_ref().to_string(),
61        scheme: None,
62    }
63}
64
65fn get_host_uri(req: &RequestHead) -> Option<Uri> {
66    req.headers
67        .get(header::HOST)
68        .and_then(|host_value| host_value.to_str().ok())
69        .filter(|_| req.version < Version::HTTP_2)
70        .or_else(|| req.uri.host())
71        .and_then(|host| host.parse().ok())
72}
73
74#[doc(hidden)]
75pub struct HostGuard {
76    host: String,
77    scheme: Option<String>,
78}
79
80impl HostGuard {
81    /// Set request scheme to match
82    pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
83        self.scheme = Some(scheme.as_ref().to_string());
84        self
85    }
86}
87
88impl Guard for HostGuard {
89    fn check(&self, ctx: &GuardContext<'_>) -> bool {
90        // parse host URI from header or request target
91        let req_host_uri = match get_host_uri(ctx.head()) {
92            Some(uri) => uri,
93
94            // no match if host cannot be determined
95            None => return false,
96        };
97
98        match req_host_uri.host() {
99            // fall through to scheme checks
100            Some(uri_host) if self.host == uri_host => {}
101
102            // Either:
103            // - request's host does not match guard's host;
104            // - It was possible that the parsed URI from request target did not contain a host.
105            _ => return false,
106        }
107
108        if let Some(ref scheme) = self.scheme {
109            if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
110                return scheme == req_host_uri_scheme;
111            }
112
113            // TODO: is this the correct behavior?
114            // falls through if scheme cannot be determined
115        }
116
117        // all conditions passed
118        true
119    }
120
121    #[cfg(feature = "experimental-introspection")]
122    fn name(&self) -> String {
123        if let Some(ref scheme) = self.scheme {
124            format!("Host({}, scheme={})", self.host, scheme)
125        } else {
126            format!("Host({})", self.host)
127        }
128    }
129
130    #[cfg(feature = "experimental-introspection")]
131    fn details(&self) -> Option<Vec<super::GuardDetail>> {
132        let mut details = vec![super::GuardDetail::Headers(vec![(
133            "host".to_string(),
134            self.host.clone(),
135        )])];
136
137        if let Some(ref scheme) = self.scheme {
138            details.push(super::GuardDetail::Generic(format!("scheme={scheme}")));
139        }
140
141        Some(details)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::test::TestRequest;
149
150    #[test]
151    fn host_not_from_header_if_http2() {
152        let req = TestRequest::default()
153            .uri("www.rust-lang.org")
154            .insert_header((
155                header::HOST,
156                header::HeaderValue::from_static("www.example.com"),
157            ))
158            .to_srv_request();
159
160        let host = Host("www.example.com");
161        assert!(host.check(&req.guard_ctx()));
162
163        let host = Host("www.rust-lang.org");
164        assert!(!host.check(&req.guard_ctx()));
165
166        let req = TestRequest::default()
167            .version(actix_http::Version::HTTP_2)
168            .uri("www.rust-lang.org")
169            .insert_header((
170                header::HOST,
171                header::HeaderValue::from_static("www.example.com"),
172            ))
173            .to_srv_request();
174
175        let host = Host("www.example.com");
176        assert!(!host.check(&req.guard_ctx()));
177
178        let host = Host("www.rust-lang.org");
179        assert!(host.check(&req.guard_ctx()));
180    }
181
182    #[test]
183    fn host_from_header() {
184        let req = TestRequest::default()
185            .insert_header((
186                header::HOST,
187                header::HeaderValue::from_static("www.rust-lang.org"),
188            ))
189            .to_srv_request();
190
191        let host = Host("www.rust-lang.org");
192        assert!(host.check(&req.guard_ctx()));
193
194        let host = Host("www.rust-lang.org").scheme("https");
195        assert!(host.check(&req.guard_ctx()));
196
197        let host = Host("blog.rust-lang.org");
198        assert!(!host.check(&req.guard_ctx()));
199
200        let host = Host("blog.rust-lang.org").scheme("https");
201        assert!(!host.check(&req.guard_ctx()));
202
203        let host = Host("crates.io");
204        assert!(!host.check(&req.guard_ctx()));
205
206        let host = Host("localhost");
207        assert!(!host.check(&req.guard_ctx()));
208    }
209
210    #[test]
211    fn host_without_header() {
212        let req = TestRequest::default()
213            .uri("www.rust-lang.org")
214            .to_srv_request();
215
216        let host = Host("www.rust-lang.org");
217        assert!(host.check(&req.guard_ctx()));
218
219        let host = Host("www.rust-lang.org").scheme("https");
220        assert!(host.check(&req.guard_ctx()));
221
222        let host = Host("blog.rust-lang.org");
223        assert!(!host.check(&req.guard_ctx()));
224
225        let host = Host("blog.rust-lang.org").scheme("https");
226        assert!(!host.check(&req.guard_ctx()));
227
228        let host = Host("crates.io");
229        assert!(!host.check(&req.guard_ctx()));
230
231        let host = Host("localhost");
232        assert!(!host.check(&req.guard_ctx()));
233    }
234
235    #[test]
236    fn host_scheme() {
237        let req = TestRequest::default()
238            .insert_header((
239                header::HOST,
240                header::HeaderValue::from_static("https://www.rust-lang.org"),
241            ))
242            .to_srv_request();
243
244        let host = Host("www.rust-lang.org").scheme("https");
245        assert!(host.check(&req.guard_ctx()));
246
247        let host = Host("www.rust-lang.org");
248        assert!(host.check(&req.guard_ctx()));
249
250        let host = Host("www.rust-lang.org").scheme("http");
251        assert!(!host.check(&req.guard_ctx()));
252
253        let host = Host("blog.rust-lang.org");
254        assert!(!host.check(&req.guard_ctx()));
255
256        let host = Host("blog.rust-lang.org").scheme("https");
257        assert!(!host.check(&req.guard_ctx()));
258
259        let host = Host("crates.io").scheme("https");
260        assert!(!host.check(&req.guard_ctx()));
261
262        let host = Host("localhost");
263        assert!(!host.check(&req.guard_ctx()));
264    }
265
266    #[cfg(feature = "experimental-introspection")]
267    #[test]
268    fn host_guard_details_include_host_and_scheme() {
269        let host = Host("example.com").scheme("https");
270        let details = host.details().expect("missing guard details");
271
272        assert!(details.iter().any(|detail| match detail {
273            crate::guard::GuardDetail::Headers(headers) => headers
274                .iter()
275                .any(|(name, value)| name == "host" && value == "example.com"),
276            _ => false,
277        }));
278        assert!(details.iter().any(|detail| match detail {
279            crate::guard::GuardDetail::Generic(value) => value == "scheme=https",
280            _ => false,
281        }));
282        assert_eq!(host.name(), "Host(example.com, scheme=https)");
283    }
284}