Skip to main content

static_web_server/
https_redirect.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! Module to redirect HTTP requests to HTTPS.
7//!
8
9use 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
19/// HTTPS redirect options.
20pub struct RedirectOpts {
21    /// HTTPS hostname to redirect to.
22    pub https_hostname: String,
23    /// HTTPS hostname port to redirect to.
24    pub https_port: u16,
25    /// Hostnames or IPS to redirect from.
26    pub allowed_hosts: Vec<String>,
27}
28
29/// It redirects all requests from HTTP to HTTPS.
30pub 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}