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