Skip to main content

siderite_core/middleware/
hosts.rs

1//! Host validation and HTTPS redirection.
2
3use super::TrustedProxies;
4use super::adapt::{BoxService, Next, from_fn, impl_layer};
5use crate::body::Body;
6use crate::error::ApiError;
7use crate::extract::Request;
8use crate::response::{IntoResponse, Response};
9use http::uri::Authority;
10use http::{HeaderValue, StatusCode, header};
11use std::sync::Arc;
12use tower::Layer;
13
14fn bad_request(message: &str) -> Response {
15    ApiError::bad_request(message).into_response()
16}
17
18/// Parse one request authority, excluding user info and invalid ports.
19fn request_authority(req: &Request) -> Option<Authority> {
20    let mut hosts = req.headers().get_all(header::HOST).iter();
21    let raw = match hosts.next() {
22        Some(value) => value.to_str().ok()?,
23        None => req.uri().authority()?.as_str(),
24    };
25    if hosts.next().is_some() {
26        return None;
27    }
28    let authority = raw.parse::<Authority>().ok()?;
29    valid_authority(&authority).then_some(authority)
30}
31
32fn valid_authority(authority: &Authority) -> bool {
33    let raw = authority.as_str();
34    let host = authority.host();
35    if raw.contains('@') || host.is_empty() {
36        return false;
37    }
38    raw.strip_prefix(host).is_some_and(|suffix| {
39        suffix.is_empty()
40            || suffix
41                .strip_prefix(':')
42                .is_some_and(|port| port.parse::<u16>().is_ok())
43    })
44}
45
46fn request_host(req: &Request) -> Option<String> {
47    let authority = request_authority(req)?;
48    Some(
49        authority
50            .host()
51            .trim_start_matches('[')
52            .trim_end_matches(']')
53            .to_ascii_lowercase(),
54    )
55}
56
57/// Rejects requests whose `Host` is not on an allow-list (400 problem).
58///
59/// Patterns are exact hosts (`example.com`), subdomain wildcards
60/// (`*.example.com`, which does not match the apex) or `*`. Matching is
61/// case-insensitive and ignores the port.
62#[derive(Debug, Clone)]
63pub struct TrustedHosts {
64    patterns: Arc<[String]>,
65}
66
67impl TrustedHosts {
68    /// Allow the given host patterns.
69    pub fn new<I, S>(patterns: I) -> Self
70    where
71        I: IntoIterator<Item = S>,
72        S: AsRef<str>,
73    {
74        let patterns: Vec<String> = patterns
75            .into_iter()
76            .map(|p| p.as_ref().to_ascii_lowercase())
77            .collect();
78        Self {
79            patterns: patterns.into(),
80        }
81    }
82
83    fn allows(&self, host: &str) -> bool {
84        self.patterns.iter().any(|p| match p.strip_prefix("*.") {
85            Some(suffix) => host
86                .strip_suffix(suffix)
87                .is_some_and(|head| head.ends_with('.') && head.len() > 1),
88            None => p == "*" || p == host,
89        })
90    }
91
92    fn wrap(&self, inner: BoxService) -> BoxService {
93        let this = self.clone();
94        from_fn(move |req: Request, next: Next| {
95            let allowed = request_host(&req).is_some_and(|h| this.allows(&h));
96            async move {
97                if allowed {
98                    next.run(req).await
99                } else {
100                    bad_request("Invalid or missing Host header.")
101                }
102            }
103        })
104        .layer(inner)
105    }
106}
107
108/// Redirect plain HTTP to HTTPS with an explicit proxy and authority policy.
109///
110/// Forwarded scheme headers are ignored by default. Configure exact trusted
111/// socket peers with [`Self::trusted_proxies`]; those proxies must replace
112/// client-supplied forwarded headers. Incoming HTTP ports are removed by
113/// default, preserving IPv6 brackets. Set a canonical authority for a custom
114/// HTTPS port or deployment host, and an allow-list for incoming hosts.
115#[derive(Debug, Clone, Default)]
116pub struct HttpsRedirect {
117    proxies: TrustedProxies,
118    canonical: Option<Authority>,
119    allowed: Option<TrustedHosts>,
120}
121
122impl HttpsRedirect {
123    /// Redirect while ignoring untrusted forwarded headers.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Configure exact trusted proxy peers using the shared policy.
129    ///
130    /// Args:
131    ///     proxies: Immediate peers required to replace forwarded headers.
132    ///
133    /// Returns:
134    ///     Redirect middleware with explicit forwarded-scheme trust.
135    #[must_use]
136    pub fn trusted_proxies(mut self, proxies: TrustedProxies) -> Self {
137        self.proxies = proxies;
138        self
139    }
140
141    /// Explicit compatibility trust for forwarded scheme from any source.
142    ///
143    /// Args:
144    ///     trust: Blanket ingress trust; false restores the safe default.
145    ///
146    /// Returns:
147    ///     Middleware using the selected forwarded-scheme policy.
148    #[must_use]
149    pub fn trust_forwarded_proto(self, trust: bool) -> Self {
150        self.trusted_proxies(if trust {
151            TrustedProxies::dangerously_trust_all()
152        } else {
153            TrustedProxies::default()
154        })
155    }
156
157    /// Set an explicit HTTPS destination, including an optional port.
158    ///
159    /// Args:
160    ///     authority: Parsed destination without user info.
161    ///
162    /// Returns:
163    ///     Middleware with a fixed destination; invalid user info yields 400.
164    #[must_use]
165    pub fn canonical_authority(mut self, authority: Authority) -> Self {
166        self.canonical = Some(authority);
167        self
168    }
169
170    /// Validate incoming hosts before redirecting or invoking the origin.
171    ///
172    /// Args:
173    ///     hosts: Case-insensitive host allow-list with wildcard support.
174    ///
175    /// Returns:
176    ///     Middleware rejecting missing, malformed or disallowed hosts.
177    #[must_use]
178    pub fn allowed_hosts(mut self, hosts: TrustedHosts) -> Self {
179        self.allowed = Some(hosts);
180        self
181    }
182
183    fn wrap(&self, inner: BoxService) -> BoxService {
184        let this = self.clone();
185        from_fn(move |req: Request, next: Next| {
186            let secure = this
187                .proxies
188                .resolve(&req)
189                .map(|context| context.scheme == "https");
190            let incoming = request_authority(&req);
191            let host = request_host(&req);
192            let allowed = match this.allowed.as_ref() {
193                Some(p) => host.as_ref().is_some_and(|h| p.allows(h)),
194                None => true,
195            };
196            let canonical = this.canonical.clone();
197            async move {
198                if !allowed {
199                    return bad_request("Invalid or disallowed Host header.");
200                }
201                let secure = match secure {
202                    Ok(secure) => secure,
203                    Err(_) => {
204                        return bad_request("Invalid forwarded scheme.");
205                    }
206                };
207                if secure {
208                    return next.run(req).await;
209                }
210                let Some(incoming) = incoming else {
211                    return bad_request("Invalid or missing Host header.");
212                };
213                let host = match canonical.as_ref() {
214                    Some(value) if valid_authority(value) => value.as_str(),
215                    Some(_) => {
216                        return bad_request("Invalid canonical authority.");
217                    }
218                    None => incoming.host(),
219                };
220                let path = req.uri().path_and_query();
221                let target = path.map_or("/", |pq| pq.as_str());
222                let uri = format!("https://{host}{target}");
223                match HeaderValue::from_str(&uri) {
224                    Ok(location) => {
225                        let mut response = Response::new(Body::empty());
226                        let status = StatusCode::PERMANENT_REDIRECT;
227                        *response.status_mut() = status;
228                        let headers = response.headers_mut();
229                        headers.insert(header::LOCATION, location);
230                        response
231                    }
232                    Err(_) => bad_request("Invalid redirect target."),
233                }
234            }
235        })
236        .layer(inner)
237    }
238}
239
240impl_layer!(TrustedHosts, HttpsRedirect);