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 Hsts {
176 max_age: u64,
177 include_subdomains: bool,
178 preload: bool,
179 trust_forwarded_proto: bool,
180}
181
182impl Hsts {
183 pub fn new() -> Self {
185 Self {
186 max_age: 31_536_000,
187 include_subdomains: false,
188 preload: false,
189 trust_forwarded_proto: false,
190 }
191 }
192
193 pub fn max_age(mut self, secs: u64) -> Self {
194 self.max_age = secs;
195 self
196 }
197
198 pub fn include_subdomains(mut self) -> Self {
199 self.include_subdomains = true;
200 self
201 }
202
203 pub fn preload(mut self) -> Self {
205 self.preload = true;
206 self
207 }
208
209 pub fn trust_forwarded_proto(mut self) -> Self {
210 self.trust_forwarded_proto = true;
211 self
212 }
213
214 fn value(&self) -> String {
215 let mut v = format!("max-age={}", self.max_age);
216 if self.include_subdomains {
217 v.push_str("; includeSubDomains");
218 }
219 if self.preload {
220 v.push_str("; preload");
221 }
222 v
223 }
224}
225
226impl Default for Hsts {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232#[async_trait]
233impl Middleware for Hsts {
234 async fn handle(&self, req: Request, next: Next) -> Result<Response> {
235 let secure = req.is_secure() || (self.trust_forwarded_proto && forwarded_https(&req));
236 let res = match next.run(req).await {
237 Ok(res) => res,
238 Err(err) => Response::from_error(err),
239 };
240 Ok(if secure {
241 res.with_header("strict-transport-security", self.value())
242 } else {
243 res
244 })
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn host_headers_are_parsed_strictly() {
254 let p = |s: &str| parse_host(s);
255 assert_eq!(p("example.com"), Some(("example.com".into(), None)));
256 assert_eq!(
257 p("Example.COM:8080"),
258 Some(("example.com".into(), Some(8080)))
259 );
260 assert_eq!(p("127.0.0.1:80"), Some(("127.0.0.1".into(), Some(80))));
261 assert_eq!(p("[::1]"), Some(("[::1]".into(), None)));
262 assert_eq!(
263 p("[2001:db8::1]:8443"),
264 Some(("[2001:db8::1]".into(), Some(8443)))
265 );
266 for bad in [
267 "",
268 "evil.com/path",
269 "evil.com\r\nX: y",
270 "user@evil.com",
271 "ev il.com",
272 "evil.com:",
273 "evil.com:99999",
274 "evil.com:80a",
275 ".evil.com",
276 "a..b",
277 "[::1",
278 "[]",
279 "[zz]",
280 "[::1]x",
281 "evil.com:80:80",
282 "http://evil.com",
283 "evil.com\\@good.com",
284 &"a".repeat(256),
285 ] {
286 assert_eq!(p(bad), None, "{bad:?}");
287 }
288 }
289
290 #[test]
291 fn hsts_value() {
292 assert_eq!(Hsts::new().value(), "max-age=31536000");
293 assert_eq!(
294 Hsts::new()
295 .max_age(60)
296 .include_subdomains()
297 .preload()
298 .value(),
299 "max-age=60; includeSubDomains; preload"
300 );
301 }
302}