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
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::test::TestRequest;
126
127    #[test]
128    fn host_not_from_header_if_http2() {
129        let req = TestRequest::default()
130            .uri("www.rust-lang.org")
131            .insert_header((
132                header::HOST,
133                header::HeaderValue::from_static("www.example.com"),
134            ))
135            .to_srv_request();
136
137        let host = Host("www.example.com");
138        assert!(host.check(&req.guard_ctx()));
139
140        let host = Host("www.rust-lang.org");
141        assert!(!host.check(&req.guard_ctx()));
142
143        let req = TestRequest::default()
144            .version(actix_http::Version::HTTP_2)
145            .uri("www.rust-lang.org")
146            .insert_header((
147                header::HOST,
148                header::HeaderValue::from_static("www.example.com"),
149            ))
150            .to_srv_request();
151
152        let host = Host("www.example.com");
153        assert!(!host.check(&req.guard_ctx()));
154
155        let host = Host("www.rust-lang.org");
156        assert!(host.check(&req.guard_ctx()));
157    }
158
159    #[test]
160    fn host_from_header() {
161        let req = TestRequest::default()
162            .insert_header((
163                header::HOST,
164                header::HeaderValue::from_static("www.rust-lang.org"),
165            ))
166            .to_srv_request();
167
168        let host = Host("www.rust-lang.org");
169        assert!(host.check(&req.guard_ctx()));
170
171        let host = Host("www.rust-lang.org").scheme("https");
172        assert!(host.check(&req.guard_ctx()));
173
174        let host = Host("blog.rust-lang.org");
175        assert!(!host.check(&req.guard_ctx()));
176
177        let host = Host("blog.rust-lang.org").scheme("https");
178        assert!(!host.check(&req.guard_ctx()));
179
180        let host = Host("crates.io");
181        assert!(!host.check(&req.guard_ctx()));
182
183        let host = Host("localhost");
184        assert!(!host.check(&req.guard_ctx()));
185    }
186
187    #[test]
188    fn host_without_header() {
189        let req = TestRequest::default()
190            .uri("www.rust-lang.org")
191            .to_srv_request();
192
193        let host = Host("www.rust-lang.org");
194        assert!(host.check(&req.guard_ctx()));
195
196        let host = Host("www.rust-lang.org").scheme("https");
197        assert!(host.check(&req.guard_ctx()));
198
199        let host = Host("blog.rust-lang.org");
200        assert!(!host.check(&req.guard_ctx()));
201
202        let host = Host("blog.rust-lang.org").scheme("https");
203        assert!(!host.check(&req.guard_ctx()));
204
205        let host = Host("crates.io");
206        assert!(!host.check(&req.guard_ctx()));
207
208        let host = Host("localhost");
209        assert!(!host.check(&req.guard_ctx()));
210    }
211
212    #[test]
213    fn host_scheme() {
214        let req = TestRequest::default()
215            .insert_header((
216                header::HOST,
217                header::HeaderValue::from_static("https://www.rust-lang.org"),
218            ))
219            .to_srv_request();
220
221        let host = Host("www.rust-lang.org").scheme("https");
222        assert!(host.check(&req.guard_ctx()));
223
224        let host = Host("www.rust-lang.org");
225        assert!(host.check(&req.guard_ctx()));
226
227        let host = Host("www.rust-lang.org").scheme("http");
228        assert!(!host.check(&req.guard_ctx()));
229
230        let host = Host("blog.rust-lang.org");
231        assert!(!host.check(&req.guard_ctx()));
232
233        let host = Host("blog.rust-lang.org").scheme("https");
234        assert!(!host.check(&req.guard_ctx()));
235
236        let host = Host("crates.io").scheme("https");
237        assert!(!host.check(&req.guard_ctx()));
238
239        let host = Host("localhost");
240        assert!(!host.check(&req.guard_ctx()));
241    }
242}