1use headers::HeaderValue;
22use hyper::{Body, Request, Response, StatusCode};
23use regex_lite::Regex;
24
25use crate::{Error, error_page, handler::RequestHandlerOpts, settings::Redirects};
26
27pub(crate) const MAX_URI_LEN_FOR_REGEX: usize = 8 * 1024;
34
35pub(crate) fn pre_process<T>(
37 opts: &RequestHandlerOpts,
38 req: &Request<T>,
39) -> Option<Result<Response<Body>, Error>> {
40 let redirects = opts.advanced_opts.as_ref()?.redirects.as_deref()?;
41
42 let uri = req.uri();
43 let uri_path = uri.path();
44 if uri_path.len() > MAX_URI_LEN_FOR_REGEX {
47 tracing::debug!(
48 "redirects: skipping match, uri path length {} exceeds cap {}",
49 uri_path.len(),
50 MAX_URI_LEN_FOR_REGEX
51 );
52 return None;
53 }
54 let host = req
55 .headers()
56 .get(http::header::HOST)
57 .and_then(|v| v.to_str().ok())
58 .unwrap_or("");
59 let mut uri_host = uri.host().unwrap_or(host).to_owned();
60 if let Some(uri_port) = uri.port_u16() {
61 uri_host.push_str(&format!(":{uri_port}"));
62 }
63 let matched = get_redirection(&uri_host, uri_path, Some(redirects))?;
64 let mut dest = match replace_placeholders(
65 uri_path,
66 &matched.source,
67 &matched.destination,
68 &matched.replacer,
69 ) {
70 Ok(dest) => dest,
71 Err(err) => return handle_error(err, opts, req),
72 };
73 if let Some(query) = uri.query() {
74 if !dest.ends_with('?') && !dest.ends_with('&') {
75 dest.push(if dest.contains('?') { '&' } else { '?' });
76 }
77 dest.push_str(query);
78 }
79
80 match HeaderValue::from_str(&dest) {
81 Ok(loc) => {
82 let mut resp = Response::new(Body::empty());
83 resp.headers_mut().insert(hyper::header::LOCATION, loc);
84 *resp.status_mut() = matched.kind;
85 tracing::trace!(
86 "uri matches redirects glob pattern, redirecting with status '{}'",
87 matched.kind
88 );
89 Some(Ok(resp))
90 }
91 Err(err) => handle_error(
92 Error::new(err).context("invalid header value from current uri"),
93 opts,
94 req,
95 ),
96 }
97}
98
99pub(crate) fn replace_placeholders(
101 orig_uri: &str,
102 regex: &Regex,
103 dest_uri: &str,
104 ac: &aho_corasick::AhoCorasick,
105) -> Result<String, Error> {
106 let regex_caps = if let Some(regex_caps) = regex.captures(orig_uri) {
107 regex_caps
108 } else {
109 return Err(Error::msg("regex didn't match, extracting captures failed"));
110 };
111
112 let caps: Vec<&str> = (0..regex_caps.len())
113 .map(|i| regex_caps.get(i).map(|s| s.as_str()).unwrap_or(""))
114 .collect();
115
116 tracing::debug!("url redirects/rewrites regex equivalent: {regex}");
117 tracing::debug!("url redirects/rewrites glob pattern captures: {caps:?}");
118 tracing::debug!("url redirects/rewrites glob pattern destination: {dest_uri:?}");
119
120 match ac.try_replace_all(dest_uri, &caps) {
121 Ok(dest) => {
122 tracing::debug!("url redirects/rewrites glob pattern destination replaced: {dest:?}");
123 Ok(dest)
124 }
125 Err(err) => Err(Error::new(err).context("failed replacing captures")),
126 }
127}
128
129pub(crate) fn handle_error<T>(
131 err: Error,
132 opts: &RequestHandlerOpts,
133 req: &Request<T>,
134) -> Option<Result<Response<Body>, Error>> {
135 tracing::error!("{err:?}");
136 Some(error_page::error_response(
137 req.uri(),
138 req.method(),
139 &StatusCode::INTERNAL_SERVER_ERROR,
140 &opts.page404,
141 &opts.page50x,
142 ))
143}
144
145pub fn get_redirection<'a>(
148 uri_host: &'a str,
149 uri_path: &'a str,
150 redirects_opts: Option<&'a [Redirects]>,
151) -> Option<&'a Redirects> {
152 if let Some(redirects_vec) = redirects_opts {
153 for redirect_entry in redirects_vec {
154 if let Some(host) = &redirect_entry.host {
156 tracing::debug!(
157 "checking host '{host}' redirect entry against uri host '{uri_host}'"
158 );
159 if !host.eq(uri_host) {
160 continue;
161 }
162 }
163
164 if redirect_entry.source.is_match(uri_path) {
166 return Some(redirect_entry);
167 }
168 }
169 }
170
171 None
172}
173
174#[cfg(test)]
175mod tests {
176 use super::pre_process;
177 use crate::{
178 Error,
179 handler::RequestHandlerOpts,
180 settings::{Advanced, Redirects, build_placeholder_replacer},
181 };
182 use hyper::{Body, Request, Response, StatusCode};
183 use regex_lite::Regex;
184
185 fn make_request(host: &str, uri: &str) -> Request<Body> {
186 let mut builder = Request::builder();
187 if !host.is_empty() {
188 builder = builder.header("Host", host);
189 }
190 builder.method("GET").uri(uri).body(Body::empty()).unwrap()
191 }
192
193 fn get_redirects() -> Vec<Redirects> {
194 let s1 = Regex::new(r"/source1$").unwrap();
195 let r1 = build_placeholder_replacer(&s1);
196 let s2 = Regex::new(r"/source2$").unwrap();
197 let r2 = build_placeholder_replacer(&s2);
198 let s3 = Regex::new(r"/(prefix/)?(source3)/(.*)").unwrap();
199 let r3 = build_placeholder_replacer(&s3);
200 let s4 = Regex::new(r"/source4/(.*)").unwrap();
201 let r4 = build_placeholder_replacer(&s4);
202 vec![
203 Redirects {
204 host: None,
205 source: s1,
206 destination: "/destination1".into(),
207 kind: StatusCode::FOUND,
208 replacer: r1,
209 },
210 Redirects {
211 host: Some("example.com".into()),
212 source: s2,
213 destination: "/destination2".into(),
214 kind: StatusCode::MOVED_PERMANENTLY,
215 replacer: r2,
216 },
217 Redirects {
218 host: Some("example.info".into()),
219 source: s3,
220 destination: "/destination3/$2/$3".into(),
221 kind: StatusCode::MOVED_PERMANENTLY,
222 replacer: r3,
223 },
224 Redirects {
225 host: None,
226 source: s4,
227 destination: "/destination4?p=$1".into(),
228 kind: StatusCode::FOUND,
229 replacer: r4,
230 },
231 ]
232 }
233
234 fn is_redirect(result: Option<Result<Response<Body>, Error>>) -> Option<(StatusCode, String)> {
235 if let Some(Ok(response)) = result {
236 let location = response.headers().get("Location")?.to_str().unwrap().into();
237 Some((response.status(), location))
238 } else {
239 None
240 }
241 }
242
243 #[test]
244 fn test_no_redirects() {
245 assert!(
246 pre_process(
247 &RequestHandlerOpts {
248 advanced_opts: None,
249 ..Default::default()
250 },
251 &make_request("", "/")
252 )
253 .is_none()
254 );
255
256 assert!(
257 pre_process(
258 &RequestHandlerOpts {
259 advanced_opts: Some(Advanced {
260 redirects: None,
261 ..Default::default()
262 }),
263 ..Default::default()
264 },
265 &make_request("", "/")
266 )
267 .is_none()
268 );
269 }
270
271 #[test]
272 fn test_no_match() {
273 assert!(
274 pre_process(
275 &RequestHandlerOpts {
276 advanced_opts: Some(Advanced {
277 redirects: Some(get_redirects()),
278 ..Default::default()
279 }),
280 ..Default::default()
281 },
282 &make_request("example.com", "/source2/whatever")
283 )
284 .is_none()
285 );
286
287 assert!(
288 pre_process(
289 &RequestHandlerOpts {
290 advanced_opts: Some(Advanced {
291 redirects: Some(get_redirects()),
292 ..Default::default()
293 }),
294 ..Default::default()
295 },
296 &make_request("", "/source2")
297 )
298 .is_none()
299 );
300 }
301
302 #[test]
303 fn test_match() {
304 assert_eq!(
305 is_redirect(pre_process(
306 &RequestHandlerOpts {
307 advanced_opts: Some(Advanced {
308 redirects: Some(get_redirects()),
309 ..Default::default()
310 }),
311 ..Default::default()
312 },
313 &make_request("", "/source1")
314 )),
315 Some((StatusCode::FOUND, "/destination1".into()))
316 );
317
318 assert_eq!(
319 is_redirect(pre_process(
320 &RequestHandlerOpts {
321 advanced_opts: Some(Advanced {
322 redirects: Some(get_redirects()),
323 ..Default::default()
324 }),
325 ..Default::default()
326 },
327 &make_request("example.com", "/source2")
328 )),
329 Some((StatusCode::MOVED_PERMANENTLY, "/destination2".into()))
330 );
331
332 assert_eq!(
333 is_redirect(pre_process(
334 &RequestHandlerOpts {
335 advanced_opts: Some(Advanced {
336 redirects: Some(get_redirects()),
337 ..Default::default()
338 }),
339 ..Default::default()
340 },
341 &make_request("example.info", "/source3/whatever")
342 )),
343 Some((
344 StatusCode::MOVED_PERMANENTLY,
345 "/destination3/source3/whatever".into()
346 ))
347 );
348
349 assert_eq!(
350 is_redirect(pre_process(
351 &RequestHandlerOpts {
352 advanced_opts: Some(Advanced {
353 redirects: Some(get_redirects()),
354 ..Default::default()
355 }),
356 ..Default::default()
357 },
358 &make_request("", "/source4/whatever")
359 )),
360 Some((StatusCode::FOUND, "/destination4?p=whatever".into()))
361 );
362 }
363
364 #[test]
365 fn test_query() {
366 assert_eq!(
367 is_redirect(pre_process(
368 &RequestHandlerOpts {
369 advanced_opts: Some(Advanced {
370 redirects: Some(get_redirects()),
371 ..Default::default()
372 }),
373 ..Default::default()
374 },
375 &make_request("", "/source1?q=query-string")
376 )),
377 Some((StatusCode::FOUND, "/destination1?q=query-string".into()))
378 );
379
380 assert_eq!(
381 is_redirect(pre_process(
382 &RequestHandlerOpts {
383 advanced_opts: Some(Advanced {
384 redirects: Some(get_redirects()),
385 ..Default::default()
386 }),
387 ..Default::default()
388 },
389 &make_request("example.com", "/source2?q=query-string")
390 )),
391 Some((
392 StatusCode::MOVED_PERMANENTLY,
393 "/destination2?q=query-string".into()
394 ))
395 );
396
397 assert_eq!(
398 is_redirect(pre_process(
399 &RequestHandlerOpts {
400 advanced_opts: Some(Advanced {
401 redirects: Some(get_redirects()),
402 ..Default::default()
403 }),
404 ..Default::default()
405 },
406 &make_request("example.info", "/source3/whatever?q=query-string")
407 )),
408 Some((
409 StatusCode::MOVED_PERMANENTLY,
410 "/destination3/source3/whatever?q=query-string".into()
411 ))
412 );
413
414 assert_eq!(
415 is_redirect(pre_process(
416 &RequestHandlerOpts {
417 advanced_opts: Some(Advanced {
418 redirects: Some(get_redirects()),
419 ..Default::default()
420 }),
421 ..Default::default()
422 },
423 &make_request("", "/source4/whatever?q=query-string")
424 )),
425 Some((
426 StatusCode::FOUND,
427 "/destination4?p=whatever&q=query-string".into()
428 ))
429 );
430 }
431}