Skip to main content

actix_web_lab/
redirect_host.rs

1use std::collections::BTreeSet;
2
3use actix_web::{HttpResponse, http::StatusCode};
4
5/// Host allowlist for redirect middleware.
6///
7/// Pass this type to redirect middleware setter methods such as
8/// [`crate::middleware::RedirectHttps::allow_hosts`] to require request hosts to match a known
9/// allowlisted value before constructing an absolute redirect target.
10///
11/// Host matching is case-insensitive. Include ports in allowlist entries when your deployment
12/// expects them.
13#[derive(Debug, Clone, Default)]
14pub struct HostAllowlist {
15    hosts: BTreeSet<String>,
16}
17
18impl HostAllowlist {
19    /// Creates a new host allowlist.
20    pub fn new<I, S>(hosts: I) -> Self
21    where
22        I: IntoIterator<Item = S>,
23        S: Into<String>,
24    {
25        Self {
26            hosts: hosts
27                .into_iter()
28                .map(|host| normalize_host(host.into()))
29                .collect(),
30        }
31    }
32
33    /// Returns true if the host is contained in the allowlist.
34    pub fn contains(&self, host: &str) -> bool {
35        self.hosts.contains(&normalize_host(host))
36    }
37}
38
39pub(crate) fn reject_untrusted_host(
40    configured_allowlist: Option<&HostAllowlist>,
41    host: &str,
42) -> Option<HttpResponse<()>> {
43    if configured_allowlist.is_some_and(|allowlist| !allowlist.contains(host)) {
44        return Some(HttpResponse::with_body(StatusCode::BAD_REQUEST, ()));
45    }
46
47    None
48}
49
50fn normalize_host(host: impl AsRef<str>) -> String {
51    host.as_ref().trim().to_ascii_lowercase()
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn host_matching_is_case_insensitive() {
60        let allowlist = HostAllowlist::new(["Example.COM:8443"]);
61
62        assert!(allowlist.contains("example.com:8443"));
63        assert!(allowlist.contains("EXAMPLE.COM:8443"));
64        assert!(!allowlist.contains("example.com"));
65    }
66}