1use actix_http::uri::{PathAndQuery, Uri};
4use actix_router::Url;
5use actix_service::{Service, Transform};
6use actix_utils::future::{ready, Ready};
7use bytes::Bytes;
8#[cfg(feature = "unicode")]
9use regex::Regex;
10#[cfg(not(feature = "unicode"))]
11use regex_lite::Regex;
12
13use crate::{
14 service::{ServiceRequest, ServiceResponse},
15 Error,
16};
17
18fn build_byte_index_map(old_path: &str, new_path: &str) -> Vec<u16> {
19 let old_path = old_path.as_bytes();
20 let new_path = new_path.as_bytes();
21
22 let mut map = Vec::with_capacity(old_path.len() + 1);
23 map.push(0);
24
25 let mut old_idx = 0usize;
26 let mut new_idx = 0usize;
27
28 while old_idx < old_path.len() {
29 if new_idx < new_path.len() && old_path[old_idx] == new_path[new_idx] {
30 new_idx += 1;
31 }
32
33 old_idx += 1;
34 map.push(new_idx.min(u16::MAX as usize) as u16);
35 }
36
37 map
38}
39
40#[non_exhaustive]
44#[derive(Debug, Clone, Copy, Default)]
45pub enum TrailingSlash {
46 #[default]
50 Trim,
51
52 MergeOnly,
56
57 Always,
61}
62
63#[derive(Debug, Clone, Copy)]
110pub struct NormalizePath(TrailingSlash);
111
112impl Default for NormalizePath {
113 fn default() -> Self {
114 log::warn!(
115 "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \
116 in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
117 );
118
119 Self(TrailingSlash::Trim)
120 }
121}
122
123impl NormalizePath {
124 pub fn new(trailing_slash_style: TrailingSlash) -> Self {
126 Self(trailing_slash_style)
127 }
128
129 pub fn trim() -> Self {
133 Self::new(TrailingSlash::Trim)
134 }
135}
136
137impl<S, B> Transform<S, ServiceRequest> for NormalizePath
138where
139 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
140 S::Future: 'static,
141{
142 type Response = ServiceResponse<B>;
143 type Error = Error;
144 type Transform = NormalizePathNormalization<S>;
145 type InitError = ();
146 type Future = Ready<Result<Self::Transform, Self::InitError>>;
147
148 fn new_transform(&self, service: S) -> Self::Future {
149 ready(Ok(NormalizePathNormalization {
150 service,
151 merge_slash: Regex::new("//+").unwrap(),
152 trailing_slash_behavior: self.0,
153 }))
154 }
155}
156
157pub struct NormalizePathNormalization<S> {
158 service: S,
159 merge_slash: Regex,
160 trailing_slash_behavior: TrailingSlash,
161}
162
163impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
164where
165 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
166 S::Future: 'static,
167{
168 type Response = ServiceResponse<B>;
169 type Error = Error;
170 type Future = S::Future;
171
172 actix_service::forward_ready!(service);
173
174 fn call(&self, mut req: ServiceRequest) -> Self::Future {
175 let head = req.head_mut();
176
177 let original_path = head.uri.path();
178
179 if !original_path.is_empty() {
182 let path = match self.trailing_slash_behavior {
185 TrailingSlash::Always => format!("{}/", original_path),
186 TrailingSlash::MergeOnly => original_path.to_string(),
187 TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
188 };
189
190 let path = self.merge_slash.replace_all(&path, "/");
192
193 let path = if path.is_empty() { "/" } else { path.as_ref() };
196
197 if path != original_path {
209 let reindex = build_byte_index_map(original_path, path);
210 let mut parts = head.uri.clone().into_parts();
211 let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
212
213 let path = match query {
214 Some(q) => Bytes::from(format!("{}?{}", path, q)),
215 None => Bytes::copy_from_slice(path.as_bytes()),
216 };
217 parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
218
219 let uri = Uri::from_parts(parts).unwrap();
220 req.match_info_mut()
221 .update_with_reindex(Url::new(uri.clone()), |idx| {
222 let idx = usize::from(idx).min(reindex.len() - 1);
223 reindex[idx]
224 });
225 req.head_mut().uri = uri;
226 }
227 }
228 self.service.call(req)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use actix_http::StatusCode;
235 use actix_service::IntoService;
236
237 use super::*;
238 use crate::{
239 guard::fn_guard,
240 test::{call_service, init_service, read_body, TestRequest},
241 web, App, HttpResponse,
242 };
243
244 #[actix_rt::test]
245 async fn test_wrap() {
246 let app = init_service(
247 App::new()
248 .wrap(NormalizePath::default())
249 .service(web::resource("/").to(HttpResponse::Ok))
250 .service(web::resource("/v1/something").to(HttpResponse::Ok))
251 .service(
252 web::resource("/v2/something")
253 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
254 .to(HttpResponse::Ok),
255 ),
256 )
257 .await;
258
259 let test_uris = vec![
260 "/",
261 "/?query=test",
262 "///",
263 "/v1//something",
264 "/v1//something////",
265 "//v1/something",
266 "//v1//////something",
267 "/v2//something?query=test",
268 "/v2//something////?query=test",
269 "//v2/something?query=test",
270 "//v2//////something?query=test",
271 ];
272
273 for uri in test_uris {
274 let req = TestRequest::with_uri(uri).to_request();
275 let res = call_service(&app, req).await;
276 assert!(res.status().is_success(), "Failed uri: {}", uri);
277 }
278 }
279
280 #[actix_rt::test]
281 async fn trim_trailing_slashes() {
282 let app = init_service(
283 App::new()
284 .wrap(NormalizePath(TrailingSlash::Trim))
285 .service(web::resource("/").to(HttpResponse::Ok))
286 .service(web::resource("/v1/something").to(HttpResponse::Ok))
287 .service(
288 web::resource("/v2/something")
289 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
290 .to(HttpResponse::Ok),
291 ),
292 )
293 .await;
294
295 let test_uris = vec![
296 "/",
297 "///",
298 "/v1/something",
299 "/v1/something/",
300 "/v1/something////",
301 "//v1//something",
302 "//v1//something//",
303 "/v2/something?query=test",
304 "/v2/something/?query=test",
305 "/v2/something////?query=test",
306 "//v2//something?query=test",
307 "//v2//something//?query=test",
308 ];
309
310 for uri in test_uris {
311 let req = TestRequest::with_uri(uri).to_request();
312 let res = call_service(&app, req).await;
313 assert!(res.status().is_success(), "Failed uri: {}", uri);
314 }
315 }
316
317 #[actix_rt::test]
318 async fn trim_root_trailing_slashes_with_query() {
319 let app = init_service(
320 App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
321 web::resource("/")
322 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
323 .to(HttpResponse::Ok),
324 ),
325 )
326 .await;
327
328 let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
329
330 for uri in test_uris {
331 let req = TestRequest::with_uri(uri).to_request();
332 let res = call_service(&app, req).await;
333 assert!(res.status().is_success(), "Failed uri: {}", uri);
334 }
335 }
336
337 #[actix_rt::test]
338 async fn ensure_trailing_slash() {
339 let app = init_service(
340 App::new()
341 .wrap(NormalizePath(TrailingSlash::Always))
342 .service(web::resource("/").to(HttpResponse::Ok))
343 .service(web::resource("/v1/something/").to(HttpResponse::Ok))
344 .service(
345 web::resource("/v2/something/")
346 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
347 .to(HttpResponse::Ok),
348 ),
349 )
350 .await;
351
352 let test_uris = vec![
353 "/",
354 "///",
355 "/v1/something",
356 "/v1/something/",
357 "/v1/something////",
358 "//v1//something",
359 "//v1//something//",
360 "/v2/something?query=test",
361 "/v2/something/?query=test",
362 "/v2/something////?query=test",
363 "//v2//something?query=test",
364 "//v2//something//?query=test",
365 ];
366
367 for uri in test_uris {
368 let req = TestRequest::with_uri(uri).to_request();
369 let res = call_service(&app, req).await;
370 assert!(res.status().is_success(), "Failed uri: {}", uri);
371 }
372 }
373
374 #[actix_rt::test]
375 async fn ensure_root_trailing_slash_with_query() {
376 let app = init_service(
377 App::new()
378 .wrap(NormalizePath(TrailingSlash::Always))
379 .service(
380 web::resource("/")
381 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
382 .to(HttpResponse::Ok),
383 ),
384 )
385 .await;
386
387 let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
388
389 for uri in test_uris {
390 let req = TestRequest::with_uri(uri).to_request();
391 let res = call_service(&app, req).await;
392 assert!(res.status().is_success(), "Failed uri: {}", uri);
393 }
394 }
395
396 #[actix_rt::test]
397 async fn keep_trailing_slash_unchanged() {
398 let app = init_service(
399 App::new()
400 .wrap(NormalizePath(TrailingSlash::MergeOnly))
401 .service(web::resource("/").to(HttpResponse::Ok))
402 .service(web::resource("/v1/something").to(HttpResponse::Ok))
403 .service(web::resource("/v1/").to(HttpResponse::Ok))
404 .service(
405 web::resource("/v2/something")
406 .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
407 .to(HttpResponse::Ok),
408 ),
409 )
410 .await;
411
412 let tests = vec![
413 ("/", true), ("/?query=test", true),
415 ("///", true),
416 ("/v1/something////", false),
417 ("/v1/something/", false),
418 ("//v1//something", true),
419 ("/v1/", true),
420 ("/v1", false),
421 ("/v1////", true),
422 ("//v1//", true),
423 ("///v1", false),
424 ("/v2/something?query=test", true),
425 ("/v2/something/?query=test", false),
426 ("/v2/something//?query=test", false),
427 ("//v2//something?query=test", true),
428 ];
429
430 for (uri, success) in tests {
431 let req = TestRequest::with_uri(uri).to_request();
432 let res = call_service(&app, req).await;
433 assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri);
434 }
435 }
436
437 #[actix_rt::test]
438 async fn scope_dynamic_tail_path_is_reindexed() {
439 async fn handler(path: web::Path<String>) -> HttpResponse {
440 HttpResponse::Ok().body(path.into_inner())
441 }
442
443 let app = init_service(
444 App::new().service(
445 web::scope("{tail:.*}")
446 .wrap(NormalizePath::trim())
447 .default_service(web::to(handler)),
448 ),
449 )
450 .await;
451
452 let req = TestRequest::with_uri("/uaie//iuaei").to_request();
453 let res = call_service(&app, req).await;
454
455 assert_eq!(res.status(), StatusCode::OK);
456 assert_eq!(read_body(res).await, Bytes::from_static(b"uaie/iuaei"));
457 }
458
459 #[actix_rt::test]
460 async fn scope_static_prefix_skip_is_reindexed() {
461 let app = init_service(
462 App::new().service(
463 web::scope("/api")
464 .wrap(NormalizePath::trim())
465 .service(web::resource("/v1").to(HttpResponse::Ok)),
466 ),
467 )
468 .await;
469
470 let req = TestRequest::with_uri("/api//v1").to_request();
471 let res = call_service(&app, req).await;
472
473 assert_eq!(res.status(), StatusCode::OK);
474 }
475
476 #[actix_rt::test]
477 async fn no_path() {
478 let app = init_service(
479 App::new()
480 .wrap(NormalizePath::default())
481 .service(web::resource("/").to(HttpResponse::Ok)),
482 )
483 .await;
484
485 let req = TestRequest::with_uri("eh").to_request();
488 let res = call_service(&app, req).await;
489 assert_eq!(res.status(), StatusCode::NOT_FOUND);
490 }
491
492 #[actix_rt::test]
493 async fn test_in_place_normalization() {
494 let srv = |req: ServiceRequest| {
495 assert_eq!("/v1/something", req.path());
496 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
497 };
498
499 let normalize = NormalizePath::default()
500 .new_transform(srv.into_service())
501 .await
502 .unwrap();
503
504 let test_uris = vec![
505 "/v1//something////",
506 "///v1/something",
507 "//v1///something",
508 "/v1//something",
509 ];
510
511 for uri in test_uris {
512 let req = TestRequest::with_uri(uri).to_srv_request();
513 let res = normalize.call(req).await.unwrap();
514 assert!(res.status().is_success(), "Failed uri: {}", uri);
515 }
516 }
517
518 #[actix_rt::test]
519 async fn should_normalize_nothing() {
520 const URI: &str = "/v1/something";
521
522 let srv = |req: ServiceRequest| {
523 assert_eq!(URI, req.path());
524 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
525 };
526
527 let normalize = NormalizePath::default()
528 .new_transform(srv.into_service())
529 .await
530 .unwrap();
531
532 let req = TestRequest::with_uri(URI).to_srv_request();
533 let res = normalize.call(req).await.unwrap();
534 assert!(res.status().is_success());
535 }
536
537 #[actix_rt::test]
538 async fn should_normalize_no_trail() {
539 let srv = |req: ServiceRequest| {
540 assert_eq!("/v1/something", req.path());
541 ready(Ok(req.into_response(HttpResponse::Ok().finish())))
542 };
543
544 let normalize = NormalizePath::default()
545 .new_transform(srv.into_service())
546 .await
547 .unwrap();
548
549 let req = TestRequest::with_uri("/v1/something/").to_srv_request();
550 let res = normalize.call(req).await.unwrap();
551 assert!(res.status().is_success());
552 }
553}