1use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
22use actix_web::http::{self, PathAndQuery, Uri};
23use actix_web::{Error, HttpResponse};
24use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
25use std::task::{Context, Poll};
26
27#[derive(Default, Clone, Copy)]
30pub struct CleanPath;
31
32impl<S, B> Transform<S> for CleanPath
33where
34 S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
35 S::Future: 'static,
36{
37 type Request = ServiceRequest;
38 type Response = ServiceResponse<B>;
39 type Error = Error;
40 type InitError = ();
41 type Transform = CleanPathNormalization<S>;
42 type Future = Ready<Result<Self::Transform, Self::InitError>>;
43
44 fn new_transform(&self, service: S) -> Self::Future {
45 ok(CleanPathNormalization { service })
46 }
47}
48
49#[doc(hidden)]
50pub struct CleanPathNormalization<S> {
51 service: S,
52}
53
54impl<S, B> Service for CleanPathNormalization<S>
55where
56 S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
57 S::Future: 'static,
58{
59 type Request = ServiceRequest;
60 type Response = ServiceResponse<B>;
61 type Error = Error;
62 type Future = Either<
63 Ready<Result<Self::Response, Error>>,
64 LocalBoxFuture<'static, Result<Self::Response, Error>>,
65 >;
66
67 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
68 self.service.poll_ready(cx)
69 }
70
71 fn call(&mut self, req: ServiceRequest) -> Self::Future {
72 let original_path = req.uri().path();
73 let trailing_slash = original_path.ends_with('/');
74
75 if !original_path.contains("/.")
77 && !original_path.contains("//")
78 && (has_ext(original_path) ^ trailing_slash)
79 {
80 return Either::Right(Box::pin(self.service.call(req)));
81 }
82
83 let mut path = path_clean::clean(&original_path);
84 if path != "/" {
85 if trailing_slash || !has_ext(&path) {
86 path.push('/');
87 }
88 }
89
90 if path != original_path {
91 let mut parts = req.uri().clone().into_parts();
92 let pq = parts.path_and_query.as_ref().unwrap();
93 let path = if let Some(q) = pq.query() {
94 format!("{}?{}", path, q)
95 } else {
96 path
97 };
98 parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
99 let uri = Uri::from_parts(parts).unwrap();
100
101 Either::Left(ok(req.error_response(actix_web::Error::from(
102 HttpResponse::PermanentRedirect()
103 .header(http::header::LOCATION, uri.to_string())
104 .finish(),
105 ))))
106 } else {
107 Either::Right(Box::pin(self.service.call(req)))
108 }
109 }
110}
111
112fn has_ext(path: &str) -> bool {
113 path.rfind('.')
114 .map(|index| {
115 let sub = &path[index + 1..];
116 !sub.is_empty() && !sub.contains('/')
117 })
118 .unwrap_or(false)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::CleanPath;
124 use actix_web::test::{call_service, init_service, TestRequest};
125 use actix_web::{http, web, App, HttpResponse};
126
127 #[actix_rt::test]
128 async fn test_clean() {
129 let mut app = init_service(
130 App::new()
131 .wrap(CleanPath)
132 .service(web::resource("/*").to(|| HttpResponse::Ok())),
133 )
134 .await;
135
136 let cases = vec![
137 ("/.", "/"),
138 ("/..", "/"),
139 ("/..//..", "/"),
140 ("/./", "/"),
141 ("//", "/"),
142 ("///", "/"),
143 ("///?a=1", "/?a=1"),
144 ("///?a=1&b=2", "/?a=1&b=2"),
145 ("//?a=1", "/?a=1"),
146 ("//a//b//", "/a/b/"),
147 ("//a//b//.", "/a/b/"),
148 ("//a//b//../", "/a/"),
149 ("//a//b//./", "/a/b/"),
150 ("//m.js", "/m.js"),
151 ("/a//b", "/a/b/"),
152 ("/a//b/", "/a/b/"),
153 ("/a//b//", "/a/b/"),
154 ("/a//m.js", "/a/m.js"),
155 ("/m.", "/m./"),
156 ];
157 for (given, clean) in cases.iter() {
158 let req = TestRequest::with_uri(given).to_request();
159 let res = call_service(&mut app, req).await;
160 assert!(res.status().is_redirection(), "for {}", given);
161 assert_eq!(
162 &res.headers()
163 .get(http::header::LOCATION)
164 .unwrap()
165 .to_str()
166 .unwrap(),
167 clean,
168 "for {}",
169 given,
170 );
171 }
172 }
173
174 #[actix_rt::test]
175 async fn test_pristine() {
176 let mut app = init_service(
177 App::new()
178 .wrap(CleanPath)
179 .service(web::resource("/*").to(|| HttpResponse::Ok())),
180 )
181 .await;
182
183 let cases = vec!["/", "/a/", "/a/b/", "/m.js", "/m./"];
184 for given in cases.iter() {
185 let req = TestRequest::with_uri(given).to_request();
186 let res = call_service(&mut app, req).await;
187 assert!(res.status().is_success(), "for {}", given);
188 }
189 }
190}