actix_web/guard/
host.rs

1use actix_http::{header, uri::Uri, RequestHead};
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        .or_else(|| req.uri.host())
70        .and_then(|host| host.parse().ok())
71}
72
73#[doc(hidden)]
74pub struct HostGuard {
75    host: String,
76    scheme: Option<String>,
77}
78
79impl HostGuard {
80    /// Set request scheme to match
81    pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
82        self.scheme = Some(scheme.as_ref().to_string());
83        self
84    }
85}
86
87impl Guard for HostGuard {
88    fn check(&self, ctx: &GuardContext<'_>) -> bool {
89        // parse host URI from header or request target
90        let req_host_uri = match get_host_uri(ctx.head()) {
91            Some(uri) => uri,
92
93            // no match if host cannot be determined
94            None => return false,
95        };
96
97        match req_host_uri.host() {
98            // fall through to scheme checks
99            Some(uri_host) if self.host == uri_host => {}
100
101            // Either:
102            // - request's host does not match guard's host;
103            // - It was possible that the parsed URI from request target did not contain a host.
104            _ => return false,
105        }
106
107        if let Some(ref scheme) = self.scheme {
108            if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
109                return scheme == req_host_uri_scheme;
110            }
111
112            // TODO: is this the correct behavior?
113            // falls through if scheme cannot be determined
114        }
115
116        // all conditions passed
117        true
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::test::TestRequest;
125
126    #[test]
127    fn host_from_header() {
128        let req = TestRequest::default()
129            .insert_header((
130                header::HOST,
131                header::HeaderValue::from_static("www.rust-lang.org"),
132            ))
133            .to_srv_request();
134
135        let host = Host("www.rust-lang.org");
136        assert!(host.check(&req.guard_ctx()));
137
138        let host = Host("www.rust-lang.org").scheme("https");
139        assert!(host.check(&req.guard_ctx()));
140
141        let host = Host("blog.rust-lang.org");
142        assert!(!host.check(&req.guard_ctx()));
143
144        let host = Host("blog.rust-lang.org").scheme("https");
145        assert!(!host.check(&req.guard_ctx()));
146
147        let host = Host("crates.io");
148        assert!(!host.check(&req.guard_ctx()));
149
150        let host = Host("localhost");
151        assert!(!host.check(&req.guard_ctx()));
152    }
153
154    #[test]
155    fn host_without_header() {
156        let req = TestRequest::default()
157            .uri("www.rust-lang.org")
158            .to_srv_request();
159
160        let host = Host("www.rust-lang.org");
161        assert!(host.check(&req.guard_ctx()));
162
163        let host = Host("www.rust-lang.org").scheme("https");
164        assert!(host.check(&req.guard_ctx()));
165
166        let host = Host("blog.rust-lang.org");
167        assert!(!host.check(&req.guard_ctx()));
168
169        let host = Host("blog.rust-lang.org").scheme("https");
170        assert!(!host.check(&req.guard_ctx()));
171
172        let host = Host("crates.io");
173        assert!(!host.check(&req.guard_ctx()));
174
175        let host = Host("localhost");
176        assert!(!host.check(&req.guard_ctx()));
177    }
178
179    #[test]
180    fn host_scheme() {
181        let req = TestRequest::default()
182            .insert_header((
183                header::HOST,
184                header::HeaderValue::from_static("https://www.rust-lang.org"),
185            ))
186            .to_srv_request();
187
188        let host = Host("www.rust-lang.org").scheme("https");
189        assert!(host.check(&req.guard_ctx()));
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("http");
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").scheme("https");
204        assert!(!host.check(&req.guard_ctx()));
205
206        let host = Host("localhost");
207        assert!(!host.check(&req.guard_ctx()));
208    }
209}