From 6d54ce9cbbe69e0c153b2fe40856ec801c643c6a Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Wed, 15 Mar 2023 15:03:04 -0400 Subject: [PATCH] url decode path segments with warp::path::param() --- src/filters/path.rs | 8 +++- src/reject.rs | 13 +++++++ tests/path.rs | 12 ++++++ tests/query.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/filters/path.rs b/src/filters/path.rs index 29676a729..e5ab19c65 100644 --- a/src/filters/path.rs +++ b/src/filters/path.rs @@ -131,6 +131,7 @@ use std::str::FromStr; use futures_util::future; use http::uri::PathAndQuery; +use percent_encoding::percent_decode_str; use self::internal::Opaque; use crate::filter::{filter_fn, one, Filter, FilterBase, Internal, One, Tuple}; @@ -266,11 +267,16 @@ pub fn end() -> impl Filter + Copy { pub fn param( ) -> impl Filter, Error = Rejection> + Copy { filter_segment(|seg| { + let seg = percent_decode_str(seg) + .decode_utf8() + .map_err(|_| reject::invalid_encoded_url())?; tracing::trace!("param?: {:?}", seg); if seg.is_empty() { return Err(reject::not_found()); } - T::from_str(seg).map(one).map_err(|_| reject::not_found()) + T::from_str(seg.as_ref()) + .map(one) + .map_err(|_| reject::not_found()) }) } diff --git a/src/reject.rs b/src/reject.rs index 6b19b4e6e..2779148a5 100644 --- a/src/reject.rs +++ b/src/reject.rs @@ -102,6 +102,12 @@ pub(crate) fn invalid_header(name: &'static str) -> Rejection { known(InvalidHeader { name }) } +// 400 Bad Request +#[inline] +pub(crate) fn invalid_encoded_url() -> Rejection { + known(InvalidEncodedUrl { _p: () }) +} + // 400 Bad Request #[inline] pub(crate) fn missing_cookie(name: &'static str) -> Rejection { @@ -289,6 +295,7 @@ enum_known! { MissingConnectionUpgrade(crate::ws::MissingConnectionUpgrade), MissingExtension(crate::ext::MissingExtension), BodyConsumedMultipleTimes(crate::body::BodyConsumedMultipleTimes), + InvalidEncodedUrl(InvalidEncodedUrl), } impl Rejection { @@ -423,6 +430,7 @@ impl Rejections { | Known::MissingHeader(_) | Known::MissingCookie(_) | Known::InvalidQuery(_) + | Known::InvalidEncodedUrl(_) | Known::BodyReadError(_) | Known::BodyDeserializeError(_) => StatusCode::BAD_REQUEST, #[cfg(feature = "websocket")] @@ -523,6 +531,11 @@ unit_error! { pub InvalidQuery: "Invalid query string" } +unit_error! { + /// Invalid encoded url + pub InvalidEncodedUrl: "Invalid encoded url string" +} + unit_error! { /// HTTP method not allowed pub MethodNotAllowed: "HTTP method not allowed" diff --git a/tests/path.rs b/tests/path.rs index 4f9302036..5595c6c58 100644 --- a/tests/path.rs +++ b/tests/path.rs @@ -42,6 +42,18 @@ async fn param() { let req = warp::test::request().path("/warp"); assert_eq!(req.filter(&s).await.unwrap(), "warp"); + // % encoding + let req = warp::test::request().path("/warp%20speed"); + assert_eq!(req.filter(&s).await.unwrap(), "warp speed"); + + // % encoding as int + let req = warp::test::request().path("/%35%30"); + assert_eq!(req.filter(&num).await.unwrap(), 50); + + // + space encoding, is not decoded + let req = warp::test::request().path("/warp+speed"); + assert_eq!(req.filter(&s).await.unwrap(), "warp+speed"); + // u32 doesn't extract a non-int let req = warp::test::request().path("/warp"); assert!(!req.matches(&num).await); diff --git a/tests/query.rs b/tests/query.rs index a20f104e6..3969bc11f 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -137,3 +137,93 @@ async fn raw_query() { let extracted = req.filter(&as_raw).await.unwrap(); assert_eq!(extracted, "foo=bar&baz=quux".to_owned()); } + +#[tokio::test] +async fn url_encoded_raw_query() { + let as_raw = warp::query::raw(); + + let req = warp::test::request().path("/?foo=bar%20hi&baz=quux"); + + let extracted = req.filter(&as_raw).await.unwrap(); + assert_eq!(extracted, "foo=bar%20hi&baz=quux".to_owned()); +} + +#[tokio::test] +async fn plus_encoded_raw_query() { + let as_raw = warp::query::raw(); + + let req = warp::test::request().path("/?foo=bar+hi&baz=quux"); + + let extracted = req.filter(&as_raw).await.unwrap(); + assert_eq!(extracted, "foo=bar+hi&baz=quux".to_owned()); +} + +#[derive(Deserialize, Debug, Eq, PartialEq)] +struct MyArgsWithInt { + string: Option, + number: Option, +} + +#[tokio::test] +async fn query_struct_with_int() { + let as_struct = warp::query::(); + + let req = warp::test::request().path("/?string=text&number=30"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgsWithInt { + string: Some("text".into()), + number: Some(30) + } + ); +} + +#[tokio::test] +async fn missing_query_struct_with_int() { + let as_struct = warp::query::(); + + let req = warp::test::request().path("/"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgsWithInt { + string: None, + number: None + } + ); +} + +#[tokio::test] +async fn url_encoded_query_struct_with_int() { + let as_struct = warp::query::(); + + let req = warp::test::request().path("/?string=test%20text&number=%33%30"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgsWithInt { + string: Some("test text".into()), + number: Some(30) + } + ); +} + +#[tokio::test] +async fn plus_encoded_query_struct() { + let as_struct = warp::query::(); + + let req = warp::test::request().path("/?string=test+text"); + + let extracted = req.filter(&as_struct).await.unwrap(); + assert_eq!( + extracted, + MyArgsWithInt { + string: Some("test text".into()), + number: None + } + ); +}