static_web_server/
rewrites.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 that allows to rewrite request URLs with pattern matching support.
7//!
8
9use headers::HeaderValue;
10use hyper::{header::HOST, Body, Request, Response, StatusCode, Uri};
11
12use crate::{
13    handler::RequestHandlerOpts,
14    redirects::{handle_error, replace_placeholders},
15    settings::{file::RedirectsKind, Rewrites},
16    Error,
17};
18
19/// Applies rewrite rules to a request if necessary.
20pub(crate) fn pre_process<T>(
21    opts: &RequestHandlerOpts,
22    req: &mut Request<T>,
23) -> Option<Result<Response<Body>, Error>> {
24    let rewrites = opts.advanced_opts.as_ref()?.rewrites.as_deref()?;
25    let uri_path = req.uri().path();
26
27    let matched = rewrite_uri_path(uri_path, Some(rewrites))?;
28    let dest = match replace_placeholders(uri_path, &matched.source, &matched.destination) {
29        Ok(dest) => dest,
30        Err(err) => return handle_error(err, opts, req),
31    };
32
33    if let Some(redirect_type) = &matched.redirect {
34        // Handle redirects
35        let loc = match HeaderValue::from_str(&dest) {
36            Ok(val) => val,
37            Err(err) => {
38                return handle_error(
39                    Error::new(err).context("invalid header value from current uri"),
40                    opts,
41                    req,
42                )
43            }
44        };
45        let mut resp = Response::new(Body::empty());
46        resp.headers_mut().insert(hyper::header::LOCATION, loc);
47        *resp.status_mut() = match redirect_type {
48            RedirectsKind::Permanent => StatusCode::MOVED_PERMANENTLY,
49            RedirectsKind::Temporary => StatusCode::FOUND,
50        };
51        Some(Ok(resp))
52    } else {
53        // Handle internal rewrites
54        *req.uri_mut() = match merge_uris(req.uri(), &dest) {
55            Ok(uri) => uri,
56            Err(err) => {
57                return handle_error(
58                    err.context("invalid rewrite target from current uri"),
59                    opts,
60                    req,
61                )
62            }
63        };
64
65        // Adjust Host header to allow rewriting to a different virtual host
66        if let Some(host) = req.uri().host() {
67            let mut host = host.to_owned();
68            if let Some(port) = req.uri().port_u16() {
69                host.push_str(&format!(":{}", port));
70            }
71            if let Ok(host) = host.parse() {
72                req.headers_mut().insert(HOST, host);
73            }
74        }
75
76        None
77    }
78}
79
80fn merge_uris(orig_uri: &Uri, new_uri: &str) -> Result<Uri, Error> {
81    let mut parts = new_uri.parse::<Uri>()?.into_parts();
82    if parts.scheme.is_none() {
83        parts.scheme = orig_uri.scheme().cloned();
84    }
85    if parts.authority.is_none() {
86        parts.authority = orig_uri.authority().cloned();
87    }
88    if parts.path_and_query.is_none() {
89        parts.path_and_query = orig_uri.path_and_query().cloned();
90    }
91    if let Some(path_and_query) = &mut parts.path_and_query {
92        if let (None, Some(query)) = (path_and_query.query(), orig_uri.query()) {
93            *path_and_query = [path_and_query.as_str(), "?", query]
94                .into_iter()
95                .collect::<String>()
96                .parse()?;
97        }
98    }
99    Ok(Uri::from_parts(parts)?)
100}
101
102/// It returns a rewrite's destination path if the current request uri
103/// matches against the provided rewrites array.
104pub fn rewrite_uri_path<'a>(
105    uri_path: &'a str,
106    rewrites_opts: Option<&'a [Rewrites]>,
107) -> Option<&'a Rewrites> {
108    if let Some(rewrites_vec) = rewrites_opts {
109        for rewrites_entry in rewrites_vec {
110            // Match source glob pattern against request uri path
111            if rewrites_entry.source.is_match(uri_path) {
112                return Some(rewrites_entry);
113            }
114        }
115    }
116
117    None
118}
119
120#[cfg(test)]
121mod tests {
122    use super::pre_process;
123    use crate::{
124        handler::RequestHandlerOpts,
125        settings::{file::RedirectsKind, Advanced, Rewrites},
126        Error,
127    };
128    use hyper::{header::HOST, Body, Request, Response, StatusCode};
129    use regex::Regex;
130
131    fn make_request(host: &str, uri: &str) -> Request<Body> {
132        let mut builder = Request::builder();
133        if !host.is_empty() {
134            builder = builder.header("Host", host);
135        }
136        builder.method("GET").uri(uri).body(Body::empty()).unwrap()
137    }
138
139    fn get_rewrites() -> Vec<Rewrites> {
140        vec![
141            Rewrites {
142                source: Regex::new(r"/source1$").unwrap(),
143                destination: "/destination1".into(),
144                redirect: None,
145            },
146            Rewrites {
147                source: Regex::new(r"/source2$").unwrap(),
148                destination: "/destination2".into(),
149                redirect: Some(RedirectsKind::Temporary),
150            },
151            Rewrites {
152                source: Regex::new(r"/(prefix/)?(source3)/(.*)").unwrap(),
153                destination: "/destination3/$2/$3".into(),
154                redirect: Some(RedirectsKind::Permanent),
155            },
156            Rewrites {
157                source: Regex::new(r"/(source4)/(.*)").unwrap(),
158                destination: "http://example.net:1234/destination4/$1?$2".into(),
159                redirect: None,
160            },
161        ]
162    }
163
164    fn is_redirect(result: Option<Result<Response<Body>, Error>>) -> Option<(StatusCode, String)> {
165        if let Some(Ok(response)) = result {
166            let location = response.headers().get("Location")?.to_str().unwrap().into();
167            Some((response.status(), location))
168        } else {
169            None
170        }
171    }
172
173    #[test]
174    fn test_no_rewrites() {
175        let mut req = make_request("", "/");
176        assert!(pre_process(
177            &RequestHandlerOpts {
178                advanced_opts: None,
179                ..Default::default()
180            },
181            &mut req
182        )
183        .is_none());
184        assert_eq!(req.uri(), "/");
185
186        let mut req = make_request("", "/");
187        assert!(pre_process(
188            &RequestHandlerOpts {
189                advanced_opts: Some(Advanced {
190                    rewrites: None,
191                    ..Default::default()
192                }),
193                ..Default::default()
194            },
195            &mut req
196        )
197        .is_none());
198        assert_eq!(req.uri(), "/");
199    }
200
201    #[test]
202    fn test_no_match() {
203        let mut req = make_request("example.com", "/source2/whatever");
204        assert!(pre_process(
205            &RequestHandlerOpts {
206                advanced_opts: Some(Advanced {
207                    rewrites: Some(get_rewrites()),
208                    ..Default::default()
209                }),
210                ..Default::default()
211            },
212            &mut req
213        )
214        .is_none());
215        assert_eq!(req.uri(), "/source2/whatever");
216    }
217
218    #[test]
219    fn test_match() {
220        let mut req = make_request("", "/source1?query");
221        assert!(pre_process(
222            &RequestHandlerOpts {
223                advanced_opts: Some(Advanced {
224                    rewrites: Some(get_rewrites()),
225                    ..Default::default()
226                }),
227                ..Default::default()
228            },
229            &mut req
230        )
231        .is_none());
232        assert_eq!(req.uri(), "/destination1?query");
233
234        let mut req = make_request("", "/source2");
235        assert_eq!(
236            is_redirect(pre_process(
237                &RequestHandlerOpts {
238                    advanced_opts: Some(Advanced {
239                        rewrites: Some(get_rewrites()),
240                        ..Default::default()
241                    }),
242                    ..Default::default()
243                },
244                &mut req
245            )),
246            Some((StatusCode::FOUND, "/destination2".into()))
247        );
248
249        let mut req = make_request("", "/source3/whatever");
250        assert_eq!(
251            is_redirect(pre_process(
252                &RequestHandlerOpts {
253                    advanced_opts: Some(Advanced {
254                        rewrites: Some(get_rewrites()),
255                        ..Default::default()
256                    }),
257                    ..Default::default()
258                },
259                &mut req
260            )),
261            Some((
262                StatusCode::MOVED_PERMANENTLY,
263                "/destination3/source3/whatever".into()
264            ))
265        );
266
267        let mut req = make_request("example.com", "/source4/whatever?query");
268        assert!(pre_process(
269            &RequestHandlerOpts {
270                advanced_opts: Some(Advanced {
271                    rewrites: Some(get_rewrites()),
272                    ..Default::default()
273                }),
274                ..Default::default()
275            },
276            &mut req
277        )
278        .is_none());
279        assert_eq!(
280            req.uri(),
281            "http://example.net:1234/destination4/source4?whatever"
282        );
283        assert_eq!(
284            req.headers()
285                .get(HOST)
286                .map(|h| h.to_str().unwrap())
287                .unwrap_or(""),
288            "example.net:1234"
289        );
290    }
291}