1use crate::error::{Error, Result};
24use crate::middleware::{Middleware, Next};
25use crate::request::Request;
26use crate::response::Response;
27use async_trait::async_trait;
28
29fn forwarded_https(req: &Request) -> bool {
30 req.headers
31 .get("x-forwarded-proto")
32 .and_then(|v| v.to_str().ok())
33 .is_some_and(|v| {
34 v.split(',')
35 .next()
36 .unwrap_or("")
37 .trim()
38 .eq_ignore_ascii_case("https")
39 })
40}
41
42fn parse_host(raw: &str) -> Option<(String, Option<u16>)> {
45 if raw.is_empty() || raw.len() > 255 {
46 return None;
47 }
48 let (host, port) = if let Some(rest) = raw.strip_prefix('[') {
49 let (addr, after) = rest.split_once(']')?;
50 let valid = !addr.is_empty()
51 && addr
52 .bytes()
53 .all(|b| b.is_ascii_hexdigit() || b == b':' || b == b'.');
54 if !valid {
55 return None;
56 }
57 let port = match after {
58 "" => None,
59 p => Some(p.strip_prefix(':')?),
60 };
61 (format!("[{addr}]"), port)
62 } else {
63 let (host, port) = match raw.rsplit_once(':') {
64 Some((h, p)) => (h, Some(p)),
65 None => (raw, None),
66 };
67 let valid = !host.is_empty()
68 && host
69 .bytes()
70 .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-')
71 && !host.starts_with('.')
72 && !host.contains("..");
73 if !valid {
74 return None;
75 }
76 (host.to_ascii_lowercase(), port)
77 };
78 let port = match port {
79 None => None,
80 Some(p) if !p.is_empty() && p.len() <= 5 && p.bytes().all(|b| b.is_ascii_digit()) => {
81 Some(p.parse::<u16>().ok()?)
82 }
83 Some(_) => return None,
84 };
85 Some((host, port))
86}
87
88pub struct RequireHttps {
90 https_port: Option<u16>,
91 trust_forwarded_proto: bool,
92 allowed_hosts: Vec<String>,
93}
94
95impl RequireHttps {
96 pub fn new() -> Self {
97 Self {
98 https_port: None,
99 trust_forwarded_proto: false,
100 allowed_hosts: Vec::new(),
101 }
102 }
103
104 pub fn https_port(mut self, port: u16) -> Self {
106 self.https_port = Some(port).filter(|p| *p != 443);
107 self
108 }
109
110 pub fn trust_forwarded_proto(mut self) -> Self {
112 self.trust_forwarded_proto = true;
113 self
114 }
115
116 pub fn allowed_hosts<I, S>(mut self, hosts: I) -> Self
119 where
120 I: IntoIterator<Item = S>,
121 S: Into<String>,
122 {
123 self.allowed_hosts = hosts
124 .into_iter()
125 .map(|h| h.into().to_ascii_lowercase())
126 .collect();
127 self
128 }
129}
130
131impl Default for RequireHttps {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[async_trait]
138impl Middleware for RequireHttps {
139 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
140 if req.is_secure() || (self.trust_forwarded_proto && forwarded_https(&req)) {
141 return next.run(req).await;
142 }
143 let raw = req
144 .headers
145 .get(http::header::HOST)
146 .and_then(|v| v.to_str().ok())
147 .ok_or_else(|| Error::bad_request("Missing Host header"))?;
148 let (host, _) = parse_host(raw).ok_or_else(|| Error::bad_request("Invalid Host header"))?;
149 if !self.allowed_hosts.is_empty()
150 && !self
151 .allowed_hosts
152 .iter()
153 .any(|h| *h == host.trim_matches(['[', ']']))
154 {
155 return Err(Error::bad_request("Unknown host"));
156 }
157 let port = self.https_port.map(|p| format!(":{p}")).unwrap_or_default();
158 let target = req
159 .uri
160 .path_and_query()
161 .map(|p| p.as_str())
162 .filter(|p| p.starts_with('/'))
163 .unwrap_or("/");
164 let target = target.trim_start_matches('/');
166 Ok(Response::new(http::StatusCode::PERMANENT_REDIRECT)
167 .with_header("location", format!("https://{host}{port}/{target}")))
168 }
169}
170
171pub struct AllowedHosts {
188 exact: Vec<String>,
189 suffixes: Vec<String>,
190 except: Vec<String>,
191}
192
193impl AllowedHosts {
194 pub fn new<I, S>(hosts: I) -> Self
195 where
196 I: IntoIterator<Item = S>,
197 S: Into<String>,
198 {
199 let mut exact = Vec::new();
200 let mut suffixes = Vec::new();
201 for host in hosts {
202 let host = host.into().to_ascii_lowercase();
203 match host.strip_prefix("*.") {
204 Some(rest) => suffixes.push(format!(".{rest}")),
205 None => exact.push(host),
206 }
207 }
208 Self {
209 exact,
210 suffixes,
211 except: Vec::new(),
212 }
213 }
214
215 pub fn except_paths<I, S>(mut self, paths: I) -> Self
217 where
218 I: IntoIterator<Item = S>,
219 S: Into<String>,
220 {
221 self.except = paths.into_iter().map(Into::into).collect();
222 self
223 }
224
225 fn allows(&self, host: &str) -> bool {
226 let host = host.trim_matches(['[', ']']);
227 self.exact.iter().any(|h| h == host)
228 || self
229 .suffixes
230 .iter()
231 .any(|s| host.len() > s.len() && host.ends_with(s.as_str()))
232 }
233}
234
235#[async_trait]
236impl Middleware for AllowedHosts {
237 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
238 if self.except.iter().any(|p| p == req.uri.path()) {
239 return next.run(req).await;
240 }
241 let allowed = req
242 .headers
243 .get(http::header::HOST)
244 .and_then(|v| v.to_str().ok())
245 .and_then(parse_host)
246 .is_some_and(|(host, _)| self.allows(&host));
247 if allowed {
248 next.run(req).await
249 } else {
250 Ok(Response::from_error(Error::custom(421, "Unknown host")))
251 }
252 }
253}
254
255pub struct Hsts {
260 max_age: u64,
261 include_subdomains: bool,
262 preload: bool,
263 trust_forwarded_proto: bool,
264}
265
266impl Hsts {
267 pub fn new() -> Self {
269 Self {
270 max_age: 31_536_000,
271 include_subdomains: false,
272 preload: false,
273 trust_forwarded_proto: false,
274 }
275 }
276
277 pub fn max_age(mut self, secs: u64) -> Self {
278 self.max_age = secs;
279 self
280 }
281
282 pub fn include_subdomains(mut self) -> Self {
283 self.include_subdomains = true;
284 self
285 }
286
287 pub fn preload(mut self) -> Self {
289 self.preload = true;
290 self
291 }
292
293 pub fn trust_forwarded_proto(mut self) -> Self {
294 self.trust_forwarded_proto = true;
295 self
296 }
297
298 fn value(&self) -> String {
299 let mut v = format!("max-age={}", self.max_age);
300 if self.include_subdomains {
301 v.push_str("; includeSubDomains");
302 }
303 if self.preload {
304 v.push_str("; preload");
305 }
306 v
307 }
308}
309
310impl Default for Hsts {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[async_trait]
317impl Middleware for Hsts {
318 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
319 let secure = req.is_secure() || (self.trust_forwarded_proto && forwarded_https(&req));
320 let res = match next.run(req).await {
321 Ok(res) => res,
322 Err(err) => Response::from_error(err),
323 };
324 Ok(if secure {
325 res.with_header("strict-transport-security", self.value())
326 } else {
327 res
328 })
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn host_headers_are_parsed_strictly() {
338 let p = |s: &str| parse_host(s);
339 assert_eq!(p("example.com"), Some(("example.com".into(), None)));
340 assert_eq!(
341 p("Example.COM:8080"),
342 Some(("example.com".into(), Some(8080)))
343 );
344 assert_eq!(p("127.0.0.1:80"), Some(("127.0.0.1".into(), Some(80))));
345 assert_eq!(p("[::1]"), Some(("[::1]".into(), None)));
346 assert_eq!(
347 p("[2001:db8::1]:8443"),
348 Some(("[2001:db8::1]".into(), Some(8443)))
349 );
350 for bad in [
351 "",
352 "evil.com/path",
353 "evil.com\r\nX: y",
354 "user@evil.com",
355 "ev il.com",
356 "evil.com:",
357 "evil.com:99999",
358 "evil.com:80a",
359 ".evil.com",
360 "a..b",
361 "[::1",
362 "[]",
363 "[zz]",
364 "[::1]x",
365 "evil.com:80:80",
366 "http://evil.com",
367 "evil.com\\@good.com",
368 &"a".repeat(256),
369 ] {
370 assert_eq!(p(bad), None, "{bad:?}");
371 }
372 }
373
374 #[test]
375 fn hsts_value() {
376 assert_eq!(Hsts::new().value(), "max-age=31536000");
377 assert_eq!(
378 Hsts::new()
379 .max_age(60)
380 .include_subdomains()
381 .preload()
382 .value(),
383 "max-age=60; includeSubDomains; preload"
384 );
385 }
386}