actix_clean_path/
lib.rs

1//! `Middleware` to clean request's URI, and redirect if necessary.
2//!
3//! Performs following:
4//!
5//! - Merges multiple `/` into one.
6//! - Resolves and eliminates `..` and `.` if any.
7//! - Appends a trailing `/` if one is not present, and there is no file extension.
8//!
9//! It will respond with a permanent redirect if the path was cleaned.
10//!
11//! ```rust
12//! use actix_web::{web, App, HttpResponse};
13//!
14//! # fn main() {
15//! let app = App::new()
16//!     .wrap(actix_clean_path::CleanPath)
17//!     .route("/", web::get().to(|| HttpResponse::Ok()));
18//! # }
19//! ```
20
21use 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/// `Middleware` to clean request's URI, and redirect if necessary.
28/// See module documenation for more.
29#[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        // non-allocating fast path
76        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}