diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 950880dc8..4c852ba75 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -4,8 +4,10 @@ - Minimum supported Rust version (MSRV) is now 1.88. - Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881] +- Fix stale path segment indices after path rewrites, preventing out-of-bounds access during extraction. [#3562] [#2881]: https://github.com/actix/actix-web/pull/2881 +[#3562]: https://github.com/actix/actix-web/issues/3562 ## 0.5.3 diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index ab4a943fe..3d1bb27bc 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -93,6 +93,45 @@ impl Path { self.segments.clear(); } + /// Set new path while preserving and remapping existing captured segment indices. + /// + /// The `reindex` closure maps byte indices from the previous path to byte indices in the new + /// path. + #[doc(hidden)] + pub fn update_with_reindex(&mut self, path: T, mut reindex: F) + where + F: FnMut(u16) -> u16, + { + self.skip = reindex(self.skip); + + for (_, item) in &mut self.segments { + if let PathItem::Segment(start, end) = item { + *start = reindex(*start); + *end = reindex(*end); + + if *start > *end { + *start = *end; + } + } + } + + self.path = path; + let path = self.path.path(); + + self.skip = clamp_to_char_boundary(path, self.skip); + + for (_, item) in &mut self.segments { + if let PathItem::Segment(start, end) = item { + *start = clamp_to_char_boundary(path, *start); + *end = clamp_to_char_boundary(path, *end); + + if *start > *end { + *start = *end; + } + } + } + } + /// Reset state. #[inline] pub fn reset(&mut self) { @@ -179,6 +218,16 @@ impl Path { } } +fn clamp_to_char_boundary(path: &str, idx: u16) -> u16 { + let mut idx = usize::from(idx).min(path.len()); + + while idx > 0 && !path.is_char_boundary(idx) { + idx -= 1; + } + + idx as u16 +} + #[derive(Debug)] pub struct PathIter<'a, T> { idx: usize, diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index 96f0126d9..bebc1d3eb 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -7,10 +7,12 @@ - Ignore unparsable cookies in `Cookie` request header. - Add `experimental-introspection` feature to report configured routes [#3594] - Add config/method for `TCP_NODELAY`. [#3918] +- Fix panic when `NormalizePath` rewrites a scoped dynamic path before extraction (e.g., `scope("{tail:.*}")` + `Path`). [#3562] [#3895]: https://github.com/actix/actix-web/pull/3895 [#3594]: https://github.com/actix/actix-web/pull/3594 [#3918]: https://github.com/actix/actix-web/pull/3918 +[#3562]: https://github.com/actix/actix-web/issues/3562 ## 4.12.1 diff --git a/actix-web/src/http/header/accept.rs b/actix-web/src/http/header/accept.rs index 99c95175f..db2938437 100644 --- a/actix-web/src/http/header/accept.rs +++ b/actix-web/src/http/header/accept.rs @@ -26,49 +26,49 @@ common_header! { /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] /// ``` /// + /// # Note + /// This is a request header. Servers should not send `Accept` in responses; to describe the + /// response body media type, use [`ContentType`](super::ContentType) / the `Content-Type` + /// header instead. + /// /// # Example Values /// * `audio/*; q=0.2, audio/basic` /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem}; + /// use actix_web::{http::header::{Accept, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// Accept(vec![ - /// QualityItem::max(mime::TEXT_HTML), - /// ]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(Accept(vec![QualityItem::max(mime::TEXT_HTML)])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem}; + /// use actix_web::{http::header::{Accept, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// Accept(vec![ - /// QualityItem::max(mime::APPLICATION_JSON), - /// ]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(Accept(vec![QualityItem::max(mime::APPLICATION_JSON)])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem, q}; + /// use actix_web::{http::header::{Accept, Header as _, QualityItem, q}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// Accept(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(Accept(vec![ /// QualityItem::max(mime::TEXT_HTML), /// QualityItem::max("application/xhtml+xml".parse().unwrap()), /// QualityItem::new(mime::TEXT_XML, q(0.9)), /// QualityItem::max("image/webp".parse().unwrap()), /// QualityItem::new(mime::STAR_STAR, q(0.8)), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// + /// let accept = Accept::parse(&req).unwrap(); + /// assert_eq!(accept.preference(), mime::TEXT_HTML); /// ``` /// /// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 diff --git a/actix-web/src/http/header/accept_charset.rs b/actix-web/src/http/header/accept_charset.rs index 43a7861fe..04216f69a 100644 --- a/actix-web/src/http/header/accept_charset.rs +++ b/actix-web/src/http/header/accept_charset.rs @@ -10,6 +10,10 @@ common_header! { /// to an origin server that is capable of representing information in /// those charsets. /// + /// # Note + /// This is a request header. Servers should not send `Accept-Charset` in responses; to + /// describe the response body's charset, set an appropriate `Content-Type` header instead. + /// /// # ABNF /// ```plain /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) @@ -20,36 +24,33 @@ common_header! { /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; + /// use actix_web::{http::header::{AcceptCharset, Charset, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; + /// use actix_web::{http::header::{AcceptCharset, Charset, q, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptCharset(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(AcceptCharset(vec![ /// QualityItem::new(Charset::Us_Ascii, q(0.9)), /// QualityItem::new(Charset::Iso_8859_10, q(0.2)), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; + /// use actix_web::{http::header::{AcceptCharset, Charset, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3 diff --git a/actix-web/src/http/header/accept_encoding.rs b/actix-web/src/http/header/accept_encoding.rs index 19d649926..ac85ec9e1 100644 --- a/actix-web/src/http/header/accept_encoding.rs +++ b/actix-web/src/http/header/accept_encoding.rs @@ -11,6 +11,11 @@ common_header! { /// content-codings are acceptable in the response. An `identity` token is used as a synonym /// for "no encoding" in order to communicate when no encoding is preferred. /// + /// # Note + /// This is a request header. Servers should not send `Accept-Encoding` in responses; use the + /// `Content-Encoding` header (or middleware like compression) to describe any content-coding + /// applied to the response body. + /// /// # ABNF /// ```plain /// Accept-Encoding = #( codings [ weight ] ) @@ -26,26 +31,26 @@ common_header! { /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem}; + /// use actix_web::{http::header::{AcceptEncoding, Encoding, Preference, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(AcceptEncoding(vec![ + /// QualityItem::max(Preference::Specific(Encoding::gzip())), + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; + /// use actix_web::{http::header::{AcceptEncoding, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptEncoding(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(AcceptEncoding(vec![ /// "gzip".parse().unwrap(), /// "br".parse().unwrap(), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem>)* diff --git a/actix-web/src/http/header/accept_language.rs b/actix-web/src/http/header/accept_language.rs index b1d588f8d..0f7800d16 100644 --- a/actix-web/src/http/header/accept_language.rs +++ b/actix-web/src/http/header/accept_language.rs @@ -14,6 +14,10 @@ common_header! { /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). /// + /// # Note + /// This is a request header. Servers should not send `Accept-Language` in responses; use + /// `Content-Language` to describe the language of the response body. + /// /// # ABNF /// ```plain /// Accept-Language = 1#( language-range [ weight ] ) @@ -31,29 +35,25 @@ common_header! { /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem}; + /// use actix_web::{http::header::{AcceptLanguage, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptLanguage(vec![ - /// "en-US".parse().unwrap(), - /// ]) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(AcceptLanguage(vec!["en-US".parse().unwrap()])) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem, q}; + /// use actix_web::{http::header::{AcceptLanguage, q, QualityItem}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptLanguage(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(AcceptLanguage(vec![ /// "da".parse().unwrap(), /// "en-GB;q=0.8".parse().unwrap(), /// "en;q=0.7".parse().unwrap(), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* diff --git a/actix-web/src/http/header/if_match.rs b/actix-web/src/http/header/if_match.rs index e0b46a6c3..d6e1e2c4b 100644 --- a/actix-web/src/http/header/if_match.rs +++ b/actix-web/src/http/header/if_match.rs @@ -16,6 +16,11 @@ common_header! { /// intends this precondition to prevent the method from being applied if /// there have been any changes to the representation data. /// + /// # Note + /// This is a request header used for conditional requests (typically to avoid lost updates). + /// Servers should not send `If-Match` in responses; use [`ETag`](super::ETag) to describe the + /// current representation instead. + /// /// # ABNF /// ```plain /// If-Match = "*" / 1#entity-tag @@ -27,25 +32,25 @@ common_header! { /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfMatch; + /// use actix_web::{http::header::IfMatch, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header(IfMatch::Any); + /// let req = test::TestRequest::default() + /// .insert_header(IfMatch::Any) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfMatch, EntityTag}; + /// use actix_web::{http::header::{EntityTag, IfMatch}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// IfMatch::Items(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(IfMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` (IfMatch, IF_MATCH) => {Any / (EntityTag)+} diff --git a/actix-web/src/http/header/if_modified_since.rs b/actix-web/src/http/header/if_modified_since.rs index 8547ff490..9367db62d 100644 --- a/actix-web/src/http/header/if_modified_since.rs +++ b/actix-web/src/http/header/if_modified_since.rs @@ -10,9 +10,14 @@ crate::http::header::common_header! { /// Transfer of the selected representation's data is avoided if that /// data has not changed. /// + /// # Note + /// This is a request header used for cache validation. Servers should not send + /// `If-Modified-Since` in responses; use [`LastModified`](super::LastModified) / the + /// `Last-Modified` header instead. + /// /// # ABNF /// ```plain - /// If-Unmodified-Since = HTTP-date + /// If-Modified-Since = HTTP-date /// ``` /// /// # Example Values @@ -22,14 +27,13 @@ crate::http::header::common_header! { /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfModifiedSince; + /// use actix_web::{http::header::IfModifiedSince, test}; /// - /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.insert_header( - /// IfModifiedSince(modified.into()) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(IfModifiedSince(modified.into())) + /// .to_http_request(); + /// # let _ = req; /// ``` (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] diff --git a/actix-web/src/http/header/if_none_match.rs b/actix-web/src/http/header/if_none_match.rs index 1a424df96..ed8bbde81 100644 --- a/actix-web/src/http/header/if_none_match.rs +++ b/actix-web/src/http/header/if_none_match.rs @@ -15,6 +15,11 @@ crate::http::header::common_header! { /// can be used for cache validation even if there have been changes to /// the representation data. /// + /// # Note + /// This is a request header used for cache validation (and conditional requests). Servers + /// should not send `If-None-Match` in responses; use [`ETag`](super::ETag) to describe the + /// current representation instead. + /// /// # ABNF /// ```plain /// If-None-Match = "*" / 1#entity-tag @@ -29,25 +34,25 @@ crate::http::header::common_header! { /// /// # Examples /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfNoneMatch; + /// use actix_web::{http::header::IfNoneMatch, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header(IfNoneMatch::Any); + /// let req = test::TestRequest::default() + /// .insert_header(IfNoneMatch::Any) + /// .to_http_request(); + /// # let _ = req; /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfNoneMatch, EntityTag}; + /// use actix_web::{http::header::{EntityTag, IfNoneMatch}, test}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// IfNoneMatch::Items(vec![ + /// let req = test::TestRequest::default() + /// .insert_header(IfNoneMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); + /// ])) + /// .to_http_request(); + /// # let _ = req; /// ``` (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} diff --git a/actix-web/src/http/header/if_range.rs b/actix-web/src/http/header/if_range.rs index 3e8727ab0..d611a5897 100644 --- a/actix-web/src/http/header/if_range.rs +++ b/actix-web/src/http/header/if_range.rs @@ -22,6 +22,9 @@ use crate::{error::ParseError, http::header, HttpMessage}; /// representation is unchanged, send me the part(s) that I am requesting /// in Range; otherwise, send me the entire representation. /// +/// # Note +/// This is a request header. Servers should not send `If-Range` in responses. +/// /// # ABNF /// ```plain /// If-Range = entity-tag / HTTP-date @@ -34,26 +37,23 @@ use crate::{error::ParseError, http::header, HttpMessage}; /// /// # Examples /// ``` -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{EntityTag, IfRange}; +/// use actix_web::{http::header::{EntityTag, IfRange}, test}; /// -/// let mut builder = HttpResponse::Ok(); -/// builder.insert_header( -/// IfRange::EntityTag( -/// EntityTag::new(false, "abc".to_owned()) -/// ) -/// ); +/// let req = test::TestRequest::default() +/// .insert_header(IfRange::EntityTag(EntityTag::new(false, "abc".to_owned()))) +/// .to_http_request(); +/// # let _ = req; /// ``` /// /// ``` /// use std::time::{Duration, SystemTime}; -/// use actix_web::{http::header::IfRange, HttpResponse}; +/// use actix_web::{http::header::IfRange, test}; /// -/// let mut builder = HttpResponse::Ok(); /// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); -/// builder.insert_header( -/// IfRange::Date(fetched.into()) -/// ); +/// let req = test::TestRequest::default() +/// .insert_header(IfRange::Date(fetched.into())) +/// .to_http_request(); +/// # let _ = req; /// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub enum IfRange { diff --git a/actix-web/src/http/header/if_unmodified_since.rs b/actix-web/src/http/header/if_unmodified_since.rs index afa4eb8e5..af32d7e46 100644 --- a/actix-web/src/http/header/if_unmodified_since.rs +++ b/actix-web/src/http/header/if_unmodified_since.rs @@ -10,6 +10,11 @@ crate::http::header::common_header! { /// This field accomplishes the same purpose as If-Match for cases where /// the user agent does not have an entity-tag for the representation. /// + /// # Note + /// This is a request header used for conditional requests. Servers should not send + /// `If-Unmodified-Since` in responses; use [`LastModified`](super::LastModified) / the + /// `Last-Modified` header instead. + /// /// # ABNF /// ```plain /// If-Unmodified-Since = HTTP-date @@ -22,14 +27,13 @@ crate::http::header::common_header! { /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfUnmodifiedSince; + /// use actix_web::{http::header::IfUnmodifiedSince, test}; /// - /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.insert_header( - /// IfUnmodifiedSince(modified.into()) - /// ); + /// let req = test::TestRequest::default() + /// .insert_header(IfUnmodifiedSince(modified.into())) + /// .to_http_request(); + /// # let _ = req; /// ``` (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] diff --git a/actix-web/src/http/header/range.rs b/actix-web/src/http/header/range.rs index 4a5d95d93..0f0958e5c 100644 --- a/actix-web/src/http/header/range.rs +++ b/actix-web/src/http/header/range.rs @@ -15,6 +15,10 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa /// only one or more sub-ranges of the selected representation data, rather than the entire selected /// representation data. /// +/// # Note +/// This is a request header. Servers should not send `Range` in responses; use +/// [`ContentRange`](super::ContentRange) / the `Content-Range` header for partial responses. +/// /// # ABNF /// ```plain /// Range = byte-ranges-specifier / other-ranges-specifier @@ -42,16 +46,18 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa /// /// # Examples /// ``` -/// use actix_web::http::header::{Range, ByteRangeSpec}; -/// use actix_web::HttpResponse; +/// use actix_web::{http::header::{ByteRangeSpec, Range}, test}; /// -/// let mut builder = HttpResponse::Ok(); -/// builder.insert_header(Range::Bytes( -/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::From(200)] -/// )); -/// builder.insert_header(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); -/// builder.insert_header(Range::bytes(1, 100)); -/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); +/// let req = test::TestRequest::default() +/// .insert_header(Range::Bytes(vec![ +/// ByteRangeSpec::FromTo(1, 100), +/// ByteRangeSpec::From(200), +/// ])) +/// .insert_header(Range::Unregistered("letters".to_owned(), "a-f".to_owned())) +/// .insert_header(Range::bytes(1, 100)) +/// .insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])) +/// .to_http_request(); +/// # let _ = req; /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Range { diff --git a/actix-web/src/middleware/normalize.rs b/actix-web/src/middleware/normalize.rs index 482107ecb..e98fc4e7a 100644 --- a/actix-web/src/middleware/normalize.rs +++ b/actix-web/src/middleware/normalize.rs @@ -1,6 +1,7 @@ //! For middleware documentation, see [`NormalizePath`]. use actix_http::uri::{PathAndQuery, Uri}; +use actix_router::Url; use actix_service::{Service, Transform}; use actix_utils::future::{ready, Ready}; use bytes::Bytes; @@ -14,6 +15,28 @@ use crate::{ Error, }; +fn build_byte_index_map(old_path: &str, new_path: &str) -> Vec { + let old_path = old_path.as_bytes(); + let new_path = new_path.as_bytes(); + + let mut map = Vec::with_capacity(old_path.len() + 1); + map.push(0); + + let mut old_idx = 0usize; + let mut new_idx = 0usize; + + while old_idx < old_path.len() { + if new_idx < new_path.len() && old_path[old_idx] == new_path[new_idx] { + new_idx += 1; + } + + old_idx += 1; + map.push(new_idx.min(u16::MAX as usize) as u16); + } + + map +} + /// Determines the behavior of the [`NormalizePath`] middleware. /// /// The default is `TrailingSlash::Trim`. @@ -183,6 +206,7 @@ where // Both of the paths have the same length, // so the change can not be deduced from the length comparison if path != original_path { + let reindex = build_byte_index_map(original_path, path); let mut parts = head.uri.clone().into_parts(); let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); @@ -193,7 +217,11 @@ where 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.match_info_mut() + .update_with_reindex(Url::new(uri.clone()), |idx| { + let idx = usize::from(idx).min(reindex.len() - 1); + reindex[idx] + }); req.head_mut().uri = uri; } } @@ -209,7 +237,7 @@ mod tests { use super::*; use crate::{ guard::fn_guard, - test::{call_service, init_service, TestRequest}, + test::{call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, }; @@ -406,6 +434,45 @@ mod tests { } } + #[actix_rt::test] + async fn scope_dynamic_tail_path_is_reindexed() { + async fn handler(path: web::Path) -> HttpResponse { + HttpResponse::Ok().body(path.into_inner()) + } + + let app = init_service( + App::new().service( + web::scope("{tail:.*}") + .wrap(NormalizePath::trim()) + .default_service(web::to(handler)), + ), + ) + .await; + + let req = TestRequest::with_uri("/uaie//iuaei").to_request(); + let res = call_service(&app, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(read_body(res).await, Bytes::from_static(b"uaie/iuaei")); + } + + #[actix_rt::test] + async fn scope_static_prefix_skip_is_reindexed() { + let app = init_service( + App::new().service( + web::scope("/api") + .wrap(NormalizePath::trim()) + .service(web::resource("/v1").to(HttpResponse::Ok)), + ), + ) + .await; + + let req = TestRequest::with_uri("/api//v1").to_request(); + let res = call_service(&app, req).await; + + assert_eq!(res.status(), StatusCode::OK); + } + #[actix_rt::test] async fn no_path() { let app = init_service(