static_web_server/
https_redirect.rs1use headers::{HeaderMapExt, Host};
10use hyper::{
11 Request, Response, StatusCode,
12 header::{HeaderValue, LOCATION},
13};
14use std::sync::Arc;
15
16use crate::Result;
17use crate::body::Body;
18
19pub struct RedirectOpts {
21 pub https_hostname: String,
23 pub https_port: u16,
25 pub allowed_hosts: Vec<String>,
27}
28
29pub fn redirect_to_https<T>(
31 req: &Request<T>,
32 opts: Arc<RedirectOpts>,
33) -> Result<Response<Body>, StatusCode> {
34 if let Some(ref host) = req.headers().typed_get::<Host>() {
35 let from_hostname = host.hostname();
36 if !opts
37 .allowed_hosts
38 .iter()
39 .any(|s| s.as_str() == from_hostname)
40 {
41 tracing::debug!("redirect host is not allowed!");
42 return Err(StatusCode::BAD_REQUEST);
43 }
44
45 let url = format!(
46 "https://{}:{}{}",
47 opts.https_hostname,
48 opts.https_port,
49 req.uri()
50 );
51 tracing::debug!("https redirect to {}", url);
52
53 let location = match HeaderValue::from_str(&url) {
54 Ok(location) => location,
55 Err(err) => {
56 tracing::error!("invalid https redirect location `{url}`: {err:?}");
57 return Err(StatusCode::BAD_REQUEST);
58 }
59 };
60
61 let mut resp = Response::new(crate::body::empty());
62 *resp.status_mut() = StatusCode::MOVED_PERMANENTLY;
63 resp.headers_mut().insert(LOCATION, location);
64 return Ok(resp);
65 }
66
67 tracing::debug!("redirect host was not determined!");
68 Err(StatusCode::BAD_REQUEST)
69}
70
71#[cfg(test)]
72mod tests {
73 use hyper::{Method, Request, StatusCode, header::LOCATION};
74 use std::sync::Arc;
75
76 use super::{RedirectOpts, redirect_to_https};
77
78 fn make_opts(hostname: &str, port: u16, allowed: &[&str]) -> Arc<RedirectOpts> {
79 Arc::new(RedirectOpts {
80 https_hostname: hostname.to_owned(),
81 https_port: port,
82 allowed_hosts: allowed.iter().map(|s| s.to_string()).collect(),
83 })
84 }
85
86 fn request_with_host(host: &str, path: &str) -> Request<()> {
87 Request::builder()
88 .method(Method::GET)
89 .uri(path)
90 .header("host", host)
91 .body(())
92 .unwrap()
93 }
94
95 #[test]
96 fn redirects_allowed_host_to_https() {
97 let req = request_with_host("example.com", "/foo/bar");
98 let opts = make_opts("example.com", 443, &["example.com"]);
99 let resp = redirect_to_https(&req, opts).unwrap();
100 assert_eq!(resp.status(), StatusCode::MOVED_PERMANENTLY);
101 let location = resp.headers().get(LOCATION).unwrap().to_str().unwrap();
102 assert_eq!(location, "https://example.com:443/foo/bar");
103 }
104
105 #[test]
106 fn rejects_disallowed_host() {
107 let req = request_with_host("attacker.com", "/");
108 let opts = make_opts("example.com", 443, &["example.com"]);
109 let err = redirect_to_https(&req, opts).unwrap_err();
110 assert_eq!(err, StatusCode::BAD_REQUEST);
111 }
112
113 #[test]
114 fn rejects_request_without_host_header() {
115 let req = Request::builder()
116 .method(Method::GET)
117 .uri("/foo")
118 .body(())
119 .unwrap();
120 let opts = make_opts("example.com", 443, &["example.com"]);
121 let err = redirect_to_https(&req, opts).unwrap_err();
122 assert_eq!(err, StatusCode::BAD_REQUEST);
123 }
124
125 #[test]
126 fn includes_path_and_query_in_redirect() {
127 let req = request_with_host("example.com", "/page?key=val");
128 let opts = make_opts("example.com", 8443, &["example.com"]);
129 let resp = redirect_to_https(&req, opts).unwrap();
130 let location = resp.headers().get(LOCATION).unwrap().to_str().unwrap();
131 assert_eq!(location, "https://example.com:8443/page?key=val");
132 }
133
134 #[test]
135 fn redirects_to_custom_https_hostname() {
136 let req = request_with_host("www.example.com", "/");
137 let opts = make_opts("secure.example.com", 443, &["www.example.com"]);
138 let resp = redirect_to_https(&req, opts).unwrap();
139 let location = resp.headers().get(LOCATION).unwrap().to_str().unwrap();
140 assert_eq!(location, "https://secure.example.com:443/");
141 }
142}