1use headers::HeaderValue;
10use hyper::{Body, Request, Response, StatusCode};
11use regex_lite::Regex;
12
13use crate::{Error, error_page, handler::RequestHandlerOpts, settings::Redirects};
14
15pub(crate) fn pre_process<T>(
17 opts: &RequestHandlerOpts,
18 req: &Request<T>,
19) -> Option<Result<Response<Body>, Error>> {
20 let redirects = opts.advanced_opts.as_ref()?.redirects.as_deref()?;
21
22 let uri = req.uri();
23 let uri_path = uri.path();
24 let host = req
25 .headers()
26 .get(http::header::HOST)
27 .and_then(|v| v.to_str().ok())
28 .unwrap_or("");
29 let mut uri_host = uri.host().unwrap_or(host).to_owned();
30 if let Some(uri_port) = uri.port_u16() {
31 uri_host.push_str(&format!(":{uri_port}"));
32 }
33 let matched = get_redirection(&uri_host, uri_path, Some(redirects))?;
34 let dest = match replace_placeholders(uri_path, &matched.source, &matched.destination) {
35 Ok(dest) => dest,
36 Err(err) => return handle_error(err, opts, req),
37 };
38
39 match HeaderValue::from_str(&dest) {
40 Ok(loc) => {
41 let mut resp = Response::new(Body::empty());
42 resp.headers_mut().insert(hyper::header::LOCATION, loc);
43 *resp.status_mut() = matched.kind;
44 tracing::trace!(
45 "uri matches redirects glob pattern, redirecting with status '{}'",
46 matched.kind
47 );
48 Some(Ok(resp))
49 }
50 Err(err) => handle_error(
51 Error::new(err).context("invalid header value from current uri"),
52 opts,
53 req,
54 ),
55 }
56}
57
58pub(crate) fn replace_placeholders(
60 orig_uri: &str,
61 regex: &Regex,
62 dest_uri: &str,
63) -> Result<String, Error> {
64 let regex_caps = if let Some(regex_caps) = regex.captures(orig_uri) {
65 regex_caps
66 } else {
67 return Err(Error::msg("regex didn't match, extracting captures failed"));
68 };
69
70 let caps_range = 0..regex_caps.len();
71 let caps = caps_range
72 .clone()
73 .map(|i| regex_caps.get(i).map(|s| s.as_str()).unwrap_or(""))
74 .collect::<Vec<&str>>();
75
76 let patterns = caps_range.map(|i| format!("${i}")).collect::<Vec<String>>();
77
78 tracing::debug!("url redirects/rewrites glob pattern: {patterns:?}");
79 tracing::debug!("url redirects/rewrites regex equivalent: {regex}");
80 tracing::debug!("url redirects/rewrites glob pattern captures: {caps:?}");
81 tracing::debug!("url redirects/rewrites glob pattern destination: {dest_uri:?}");
82
83 let ac = match aho_corasick::AhoCorasick::new(patterns) {
84 Ok(ac) => ac,
85 Err(err) => return Err(Error::new(err).context("failed creating Aho-Corasick matcher")),
86 };
87 match ac.try_replace_all(dest_uri, &caps) {
88 Ok(dest) => {
89 tracing::debug!("url redirects/rewrites glob pattern destination replaced: {dest:?}");
90 Ok(dest.to_string())
91 }
92 Err(err) => Err(Error::new(err).context("failed replacing captures")),
93 }
94}
95
96pub(crate) fn handle_error<T>(
98 err: Error,
99 opts: &RequestHandlerOpts,
100 req: &Request<T>,
101) -> Option<Result<Response<Body>, Error>> {
102 tracing::error!("{err:?}");
103 Some(error_page::error_response(
104 req.uri(),
105 req.method(),
106 &StatusCode::INTERNAL_SERVER_ERROR,
107 &opts.page404,
108 &opts.page50x,
109 ))
110}
111
112pub fn get_redirection<'a>(
115 uri_host: &'a str,
116 uri_path: &'a str,
117 redirects_opts: Option<&'a [Redirects]>,
118) -> Option<&'a Redirects> {
119 if let Some(redirects_vec) = redirects_opts {
120 for redirect_entry in redirects_vec {
121 if let Some(host) = &redirect_entry.host {
123 tracing::debug!(
124 "checking host '{host}' redirect entry against uri host '{uri_host}'"
125 );
126 if !host.eq(uri_host) {
127 continue;
128 }
129 }
130
131 if redirect_entry.source.is_match(uri_path) {
133 return Some(redirect_entry);
134 }
135 }
136 }
137
138 None
139}
140
141#[cfg(test)]
142mod tests {
143 use super::pre_process;
144 use crate::{
145 Error,
146 handler::RequestHandlerOpts,
147 settings::{Advanced, Redirects},
148 };
149 use hyper::{Body, Request, Response, StatusCode};
150 use regex_lite::Regex;
151
152 fn make_request(host: &str, uri: &str) -> Request<Body> {
153 let mut builder = Request::builder();
154 if !host.is_empty() {
155 builder = builder.header("Host", host);
156 }
157 builder.method("GET").uri(uri).body(Body::empty()).unwrap()
158 }
159
160 fn get_redirects() -> Vec<Redirects> {
161 vec![
162 Redirects {
163 host: None,
164 source: Regex::new(r"/source1$").unwrap(),
165 destination: "/destination1".into(),
166 kind: StatusCode::FOUND,
167 },
168 Redirects {
169 host: Some("example.com".into()),
170 source: Regex::new(r"/source2$").unwrap(),
171 destination: "/destination2".into(),
172 kind: StatusCode::MOVED_PERMANENTLY,
173 },
174 Redirects {
175 host: Some("example.info".into()),
176 source: Regex::new(r"/(prefix/)?(source3)/(.*)").unwrap(),
177 destination: "/destination3/$2/$3".into(),
178 kind: StatusCode::MOVED_PERMANENTLY,
179 },
180 ]
181 }
182
183 fn is_redirect(result: Option<Result<Response<Body>, Error>>) -> Option<(StatusCode, String)> {
184 if let Some(Ok(response)) = result {
185 let location = response.headers().get("Location")?.to_str().unwrap().into();
186 Some((response.status(), location))
187 } else {
188 None
189 }
190 }
191
192 #[test]
193 fn test_no_redirects() {
194 assert!(
195 pre_process(
196 &RequestHandlerOpts {
197 advanced_opts: None,
198 ..Default::default()
199 },
200 &make_request("", "/")
201 )
202 .is_none()
203 );
204
205 assert!(
206 pre_process(
207 &RequestHandlerOpts {
208 advanced_opts: Some(Advanced {
209 redirects: None,
210 ..Default::default()
211 }),
212 ..Default::default()
213 },
214 &make_request("", "/")
215 )
216 .is_none()
217 );
218 }
219
220 #[test]
221 fn test_no_match() {
222 assert!(
223 pre_process(
224 &RequestHandlerOpts {
225 advanced_opts: Some(Advanced {
226 redirects: Some(get_redirects()),
227 ..Default::default()
228 }),
229 ..Default::default()
230 },
231 &make_request("example.com", "/source2/whatever")
232 )
233 .is_none()
234 );
235
236 assert!(
237 pre_process(
238 &RequestHandlerOpts {
239 advanced_opts: Some(Advanced {
240 redirects: Some(get_redirects()),
241 ..Default::default()
242 }),
243 ..Default::default()
244 },
245 &make_request("", "/source2")
246 )
247 .is_none()
248 );
249 }
250
251 #[test]
252 fn test_match() {
253 assert_eq!(
254 is_redirect(pre_process(
255 &RequestHandlerOpts {
256 advanced_opts: Some(Advanced {
257 redirects: Some(get_redirects()),
258 ..Default::default()
259 }),
260 ..Default::default()
261 },
262 &make_request("", "/source1")
263 )),
264 Some((StatusCode::FOUND, "/destination1".into()))
265 );
266
267 assert_eq!(
268 is_redirect(pre_process(
269 &RequestHandlerOpts {
270 advanced_opts: Some(Advanced {
271 redirects: Some(get_redirects()),
272 ..Default::default()
273 }),
274 ..Default::default()
275 },
276 &make_request("example.com", "/source2")
277 )),
278 Some((StatusCode::MOVED_PERMANENTLY, "/destination2".into()))
279 );
280
281 assert_eq!(
282 is_redirect(pre_process(
283 &RequestHandlerOpts {
284 advanced_opts: Some(Advanced {
285 redirects: Some(get_redirects()),
286 ..Default::default()
287 }),
288 ..Default::default()
289 },
290 &make_request("example.info", "/source3/whatever")
291 )),
292 Some((
293 StatusCode::MOVED_PERMANENTLY,
294 "/destination3/source3/whatever".into()
295 ))
296 );
297 }
298}