1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-12-18 06:06:36 +00:00
actix-web/src/middleware/normalize.rs

371 lines
13 KiB
Rust

//! For middleware documentation, see [`NormalizePath`].
use actix_http::http::{PathAndQuery, Uri};
use actix_service::{Service, Transform};
use bytes::Bytes;
use futures_util::future::{ready, Ready};
use regex::Regex;
use crate::{
service::{ServiceRequest, ServiceResponse},
Error,
};
/// Determines the behavior of the [`NormalizePath`] middleware.
///
/// The default is `TrailingSlash::Trim`.
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum TrailingSlash {
/// Trim trailing slashes from the end of the path.
///
/// Using this will require all routes to omit trailing slashes for them to be accessible.
Trim,
/// Only merge any present multiple trailing slashes.
///
/// This option provides the best compatibility with behavior in actix-web v2.0.
MergeOnly,
/// Always add a trailing slash to the end of the path.
///
/// Using this will require all routes have a trailing slash for them to be accessible.
Always,
}
impl Default for TrailingSlash {
fn default() -> Self {
TrailingSlash::Trim
}
}
/// Middleware for normalizing a request's path so that routes can be matched more flexibly.
///
/// # Normalization Steps
/// - Merges consecutive slashes into one. (For example, `/path//one` always becomes `/path/one`.)
/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing
/// slashes as-is, depending on which [`TrailingSlash`] variant is supplied
/// to [`new`](NormalizePath::new()).
///
/// # Default Behavior
/// The default constructor chooses to strip trailing slashes from the end of paths with them
/// ([`TrailingSlash::Trim`]). The implication is that route definitions should be defined without
/// trailing slashes or else they will be inaccessible (or vice versa when using the
/// `TrailingSlash::Always` behavior), as shown in the example tests below.
///
/// # Usage
/// ```rust
/// use actix_web::{web, middleware, App};
///
/// # actix_web::rt::System::new().block_on(async {
/// let app = App::new()
/// .wrap(middleware::NormalizePath::default())
/// .route("/test", web::get().to(|| async { "test" }))
/// .route("/unmatchable/", web::get().to(|| async { "unmatchable" }));
///
/// use actix_web::http::StatusCode;
/// use actix_web::test::{call_service, init_service, TestRequest};
///
/// let app = init_service(app).await;
///
/// let req = TestRequest::with_uri("/test").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/test/").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/unmatchable").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
///
/// let req = TestRequest::with_uri("/unmatchable/").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// # })
/// ```
#[derive(Debug, Clone, Copy, Default)]
pub struct NormalizePath(TrailingSlash);
impl NormalizePath {
/// Create new `NormalizePath` middleware with the specified trailing slash style.
pub fn new(trailing_slash_style: TrailingSlash) -> Self {
NormalizePath(trailing_slash_style)
}
}
impl<S, B> Transform<S, ServiceRequest> for NormalizePath
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = NormalizePathNormalization<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(NormalizePathNormalization {
service,
merge_slash: Regex::new("//+").unwrap(),
trailing_slash_behavior: self.0,
}))
}
}
pub struct NormalizePathNormalization<S> {
service: S,
merge_slash: Regex,
trailing_slash_behavior: TrailingSlash,
}
impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = S::Future;
actix_service::forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let head = req.head_mut();
let original_path = head.uri.path();
// Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end
let path = match self.trailing_slash_behavior {
TrailingSlash::Always => original_path.to_string() + "/",
TrailingSlash::MergeOnly => original_path.to_string(),
TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
};
// normalize multiple /'s to one /
let path = self.merge_slash.replace_all(&path, "/");
// Ensure root paths are still resolvable. If resulting path is blank after previous step
// it means the path was one or more slashes. Reduce to single slash.
let path = if path.is_empty() { "/" } else { path.as_ref() };
// Check whether the path has been changed
//
// This check was previously implemented as string length comparison
//
// That approach fails when a trailing slash is added,
// and a duplicate slash is removed,
// since the length of the strings remains the same
//
// For example, the path "/v1//s" will be normalized to "/v1/s/"
// Both of the paths have the same length,
// so the change can not be deduced from the length comparison
if path != original_path {
let mut parts = head.uri.clone().into_parts();
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
let path = if let Some(q) = query {
Bytes::from(format!("{}?{}", path, q))
} else {
Bytes::copy_from_slice(path.as_bytes())
};
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
let uri = Uri::from_parts(parts).unwrap();
req.match_info_mut().get_mut().update(&uri);
req.head_mut().uri = uri;
}
self.service.call(req)
}
}
#[cfg(test)]
mod tests {
use actix_service::IntoService;
use super::*;
use crate::{
dev::ServiceRequest,
test::{call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test]
async fn test_wrap() {
let app = init_service(
App::new()
.wrap(NormalizePath::default())
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
)
.await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/?query=test").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("///").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/v1//something////").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req2 = TestRequest::with_uri("//v1/something").to_request();
let res2 = call_service(&app, req2).await;
assert!(res2.status().is_success());
let req3 = TestRequest::with_uri("//v1//////something").to_request();
let res3 = call_service(&app, req3).await;
assert!(res3.status().is_success());
let req4 = TestRequest::with_uri("/v1//something").to_request();
let res4 = call_service(&app, req4).await;
assert!(res4.status().is_success());
}
#[actix_rt::test]
async fn trim_trailing_slashes() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Trim))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok)),
)
.await;
// root paths should still work
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/?query=test").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("///").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req = TestRequest::with_uri("/v1/something////").to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success());
let req2 = TestRequest::with_uri("/v1/something/").to_request();
let res2 = call_service(&app, req2).await;
assert!(res2.status().is_success());
let req3 = TestRequest::with_uri("//v1//something//").to_request();
let res3 = call_service(&app, req3).await;
assert!(res3.status().is_success());
let req4 = TestRequest::with_uri("//v1//something").to_request();
let res4 = call_service(&app, req4).await;
assert!(res4.status().is_success());
}
#[actix_rt::test]
async fn keep_trailing_slash_unchanged() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::MergeOnly))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(web::resource("/v1/").to(HttpResponse::Ok)),
)
.await;
let tests = vec![
("/", true), // root paths should still work
("/?query=test", true),
("///", true),
("/v1/something////", false),
("/v1/something/", false),
("//v1//something", true),
("/v1/", true),
("/v1", false),
("/v1////", true),
("//v1//", true),
("///v1", false),
];
for (path, success) in tests {
let req = TestRequest::with_uri(path).to_request();
let res = call_service(&app, req).await;
assert_eq!(res.status().is_success(), success);
}
}
#[actix_rt::test]
async fn test_in_place_normalization() {
let srv = |req: ServiceRequest| {
assert_eq!("/v1/something", req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let req = TestRequest::with_uri("/v1//something////").to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
let req2 = TestRequest::with_uri("///v1/something").to_srv_request();
let res2 = normalize.call(req2).await.unwrap();
assert!(res2.status().is_success());
let req3 = TestRequest::with_uri("//v1///something").to_srv_request();
let res3 = normalize.call(req3).await.unwrap();
assert!(res3.status().is_success());
let req4 = TestRequest::with_uri("/v1//something").to_srv_request();
let res4 = normalize.call(req4).await.unwrap();
assert!(res4.status().is_success());
}
#[actix_rt::test]
async fn should_normalize_nothing() {
const URI: &str = "/v1/something";
let srv = |req: ServiceRequest| {
assert_eq!(URI, req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let req = TestRequest::with_uri(URI).to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
}
#[actix_rt::test]
async fn should_normalize_no_trail() {
let srv = |req: ServiceRequest| {
assert_eq!("/v1/something", req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let req = TestRequest::with_uri("/v1/something/").to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
}
}