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 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
74 match HeaderValue::from_str(&dest) {
75 Ok(loc) => {
76 let mut resp = Response::new(Body::empty());
77 resp.headers_mut().insert(hyper::header::LOCATION, loc);
78 *resp.status_mut() = matched.kind;
79 tracing::trace!(
80 "uri matches redirects glob pattern, redirecting with status '{}'",
81 matched.kind
82 );
83 Some(Ok(resp))
84 }
85 Err(err) => handle_error(
86 Error::new(err).context("invalid header value from current uri"),
87 opts,
88 req,
89 ),
90 }
91}
92
93pub(crate) fn replace_placeholders(
95 orig_uri: &str,
96 regex: &Regex,
97 dest_uri: &str,
98 ac: &aho_corasick::AhoCorasick,
99) -> Result<String, Error> {
100 let regex_caps = if let Some(regex_caps) = regex.captures(orig_uri) {
101 regex_caps
102 } else {
103 return Err(Error::msg("regex didn't match, extracting captures failed"));
104 };
105
106 let caps: Vec<&str> = (0..regex_caps.len())
107 .map(|i| regex_caps.get(i).map(|s| s.as_str()).unwrap_or(""))
108 .collect();
109
110 tracing::debug!("url redirects/rewrites regex equivalent: {regex}");
111 tracing::debug!("url redirects/rewrites glob pattern captures: {caps:?}");
112 tracing::debug!("url redirects/rewrites glob pattern destination: {dest_uri:?}");
113
114 match ac.try_replace_all(dest_uri, &caps) {
115 Ok(dest) => {
116 tracing::debug!("url redirects/rewrites glob pattern destination replaced: {dest:?}");
117 Ok(dest)
118 }
119 Err(err) => Err(Error::new(err).context("failed replacing captures")),
120 }
121}
122
123pub(crate) fn handle_error<T>(
125 err: Error,
126 opts: &RequestHandlerOpts,
127 req: &Request<T>,
128) -> Option<Result<Response<Body>, Error>> {
129 tracing::error!("{err:?}");
130 Some(error_page::error_response(
131 req.uri(),
132 req.method(),
133 &StatusCode::INTERNAL_SERVER_ERROR,
134 &opts.page404,
135 &opts.page50x,
136 ))
137}
138
139pub fn get_redirection<'a>(
142 uri_host: &'a str,
143 uri_path: &'a str,
144 redirects_opts: Option<&'a [Redirects]>,
145) -> Option<&'a Redirects> {
146 if let Some(redirects_vec) = redirects_opts {
147 for redirect_entry in redirects_vec {
148 if let Some(host) = &redirect_entry.host {
150 tracing::debug!(
151 "checking host '{host}' redirect entry against uri host '{uri_host}'"
152 );
153 if !host.eq(uri_host) {
154 continue;
155 }
156 }
157
158 if redirect_entry.source.is_match(uri_path) {
160 return Some(redirect_entry);
161 }
162 }
163 }
164
165 None
166}
167
168#[cfg(test)]
169mod tests {
170 use super::pre_process;
171 use crate::{
172 Error,
173 handler::RequestHandlerOpts,
174 settings::{Advanced, Redirects, build_placeholder_replacer},
175 };
176 use hyper::{Body, Request, Response, StatusCode};
177 use regex_lite::Regex;
178
179 fn make_request(host: &str, uri: &str) -> Request<Body> {
180 let mut builder = Request::builder();
181 if !host.is_empty() {
182 builder = builder.header("Host", host);
183 }
184 builder.method("GET").uri(uri).body(Body::empty()).unwrap()
185 }
186
187 fn get_redirects() -> Vec<Redirects> {
188 let s1 = Regex::new(r"/source1$").unwrap();
189 let r1 = build_placeholder_replacer(&s1);
190 let s2 = Regex::new(r"/source2$").unwrap();
191 let r2 = build_placeholder_replacer(&s2);
192 let s3 = Regex::new(r"/(prefix/)?(source3)/(.*)").unwrap();
193 let r3 = build_placeholder_replacer(&s3);
194 vec![
195 Redirects {
196 host: None,
197 source: s1,
198 destination: "/destination1".into(),
199 kind: StatusCode::FOUND,
200 replacer: r1,
201 },
202 Redirects {
203 host: Some("example.com".into()),
204 source: s2,
205 destination: "/destination2".into(),
206 kind: StatusCode::MOVED_PERMANENTLY,
207 replacer: r2,
208 },
209 Redirects {
210 host: Some("example.info".into()),
211 source: s3,
212 destination: "/destination3/$2/$3".into(),
213 kind: StatusCode::MOVED_PERMANENTLY,
214 replacer: r3,
215 },
216 ]
217 }
218
219 fn is_redirect(result: Option<Result<Response<Body>, Error>>) -> Option<(StatusCode, String)> {
220 if let Some(Ok(response)) = result {
221 let location = response.headers().get("Location")?.to_str().unwrap().into();
222 Some((response.status(), location))
223 } else {
224 None
225 }
226 }
227
228 #[test]
229 fn test_no_redirects() {
230 assert!(
231 pre_process(
232 &RequestHandlerOpts {
233 advanced_opts: None,
234 ..Default::default()
235 },
236 &make_request("", "/")
237 )
238 .is_none()
239 );
240
241 assert!(
242 pre_process(
243 &RequestHandlerOpts {
244 advanced_opts: Some(Advanced {
245 redirects: None,
246 ..Default::default()
247 }),
248 ..Default::default()
249 },
250 &make_request("", "/")
251 )
252 .is_none()
253 );
254 }
255
256 #[test]
257 fn test_no_match() {
258 assert!(
259 pre_process(
260 &RequestHandlerOpts {
261 advanced_opts: Some(Advanced {
262 redirects: Some(get_redirects()),
263 ..Default::default()
264 }),
265 ..Default::default()
266 },
267 &make_request("example.com", "/source2/whatever")
268 )
269 .is_none()
270 );
271
272 assert!(
273 pre_process(
274 &RequestHandlerOpts {
275 advanced_opts: Some(Advanced {
276 redirects: Some(get_redirects()),
277 ..Default::default()
278 }),
279 ..Default::default()
280 },
281 &make_request("", "/source2")
282 )
283 .is_none()
284 );
285 }
286
287 #[test]
288 fn test_match() {
289 assert_eq!(
290 is_redirect(pre_process(
291 &RequestHandlerOpts {
292 advanced_opts: Some(Advanced {
293 redirects: Some(get_redirects()),
294 ..Default::default()
295 }),
296 ..Default::default()
297 },
298 &make_request("", "/source1")
299 )),
300 Some((StatusCode::FOUND, "/destination1".into()))
301 );
302
303 assert_eq!(
304 is_redirect(pre_process(
305 &RequestHandlerOpts {
306 advanced_opts: Some(Advanced {
307 redirects: Some(get_redirects()),
308 ..Default::default()
309 }),
310 ..Default::default()
311 },
312 &make_request("example.com", "/source2")
313 )),
314 Some((StatusCode::MOVED_PERMANENTLY, "/destination2".into()))
315 );
316
317 assert_eq!(
318 is_redirect(pre_process(
319 &RequestHandlerOpts {
320 advanced_opts: Some(Advanced {
321 redirects: Some(get_redirects()),
322 ..Default::default()
323 }),
324 ..Default::default()
325 },
326 &make_request("example.info", "/source3/whatever")
327 )),
328 Some((
329 StatusCode::MOVED_PERMANENTLY,
330 "/destination3/source3/whatever".into()
331 ))
332 );
333 }
334}