Skip to main content

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