1use headers::HeaderValue;
10use hyper::{Body, Request, Response, StatusCode};
11use regex::Regex;
12
13use crate::{error_page, handler::RequestHandlerOpts, settings::Redirects, Error};
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 handler::RequestHandlerOpts,
146 settings::{Advanced, Redirects},
147 Error,
148 };
149 use hyper::{Body, Request, Response, StatusCode};
150 use regex::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!(pre_process(
195 &RequestHandlerOpts {
196 advanced_opts: None,
197 ..Default::default()
198 },
199 &make_request("", "/")
200 )
201 .is_none());
202
203 assert!(pre_process(
204 &RequestHandlerOpts {
205 advanced_opts: Some(Advanced {
206 redirects: None,
207 ..Default::default()
208 }),
209 ..Default::default()
210 },
211 &make_request("", "/")
212 )
213 .is_none());
214 }
215
216 #[test]
217 fn test_no_match() {
218 assert!(pre_process(
219 &RequestHandlerOpts {
220 advanced_opts: Some(Advanced {
221 redirects: Some(get_redirects()),
222 ..Default::default()
223 }),
224 ..Default::default()
225 },
226 &make_request("example.com", "/source2/whatever")
227 )
228 .is_none());
229
230 assert!(pre_process(
231 &RequestHandlerOpts {
232 advanced_opts: Some(Advanced {
233 redirects: Some(get_redirects()),
234 ..Default::default()
235 }),
236 ..Default::default()
237 },
238 &make_request("", "/source2")
239 )
240 .is_none());
241 }
242
243 #[test]
244 fn test_match() {
245 assert_eq!(
246 is_redirect(pre_process(
247 &RequestHandlerOpts {
248 advanced_opts: Some(Advanced {
249 redirects: Some(get_redirects()),
250 ..Default::default()
251 }),
252 ..Default::default()
253 },
254 &make_request("", "/source1")
255 )),
256 Some((StatusCode::FOUND, "/destination1".into()))
257 );
258
259 assert_eq!(
260 is_redirect(pre_process(
261 &RequestHandlerOpts {
262 advanced_opts: Some(Advanced {
263 redirects: Some(get_redirects()),
264 ..Default::default()
265 }),
266 ..Default::default()
267 },
268 &make_request("example.com", "/source2")
269 )),
270 Some((StatusCode::MOVED_PERMANENTLY, "/destination2".into()))
271 );
272
273 assert_eq!(
274 is_redirect(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.info", "/source3/whatever")
283 )),
284 Some((
285 StatusCode::MOVED_PERMANENTLY,
286 "/destination3/source3/whatever".into()
287 ))
288 );
289 }
290}