Skip to content
Snippets Groups Projects
Verified Commit 80e94329 authored by Eduardo Trujillo's avatar Eduardo Trujillo
Browse files

feat(files): Add support for percent-encoding in paths

parent 88509d4f
No related branches found
No related tags found
1 merge request!3v2.0.0
......@@ -4,11 +4,13 @@ use actix_web::{
FromRequest, HttpRequest, ResponseError,
};
use futures_util::future::{ready, Ready};
use percent_encoding::percent_decode_str;
use snafu::Snafu;
use std::{
convert::{TryFrom, TryInto},
ops::Deref,
path::{Path, PathBuf},
path::{Component, Path, PathBuf},
str::FromStr,
};
#[derive(Snafu, Debug, PartialEq)]
......@@ -22,6 +24,12 @@ pub enum Error {
/// The segment ended with the wrapped invalid character.
#[snafu(display("The segment ended with the wrapped invalid character."))]
BadEnd { char: char },
/// The path is not a valid UTF-8 string after percent-decoding
#[snafu(display("The path is not a valid UTF-8 string after percent-decoding."))]
NotValidUtf8,
/// The path has invalid or unexpected components.
#[snafu(display("The path has invalid or unexpected components."))]
InvalidComponents,
}
/// Return `BadRequest` for `Error`.
......@@ -37,12 +45,23 @@ type Result<T, E = Error> = std::result::Result<T, E>;
pub struct UriPathBuf(PathBuf);
impl UriPathBuf {
pub fn new(path: &str) -> Result<Self> {
let mut buf = PathBuf::new();
pub fn new(raw_path: &str) -> Result<Self> {
let mut path = PathBuf::new();
for segment in path.split('/') {
let mut segment_count = raw_path.matches('/').count() + 1;
let decoded_path = percent_decode_str(raw_path)
.decode_utf8()
.map_err(|_| Error::NotValidUtf8)?;
if segment_count != decoded_path.matches('/').count() + 1 {
return Err(Error::BadChar { char: '/' });
}
for segment in decoded_path.split('/') {
if segment == ".." {
buf.pop();
segment_count -= 1;
path.pop();
} else if segment.starts_with('.') {
return Err(Error::BadStart { char: '.' });
} else if segment.starts_with('*') {
......@@ -54,15 +73,32 @@ impl UriPathBuf {
} else if segment.ends_with('<') {
return Err(Error::BadEnd { char: '<' });
} else if segment.is_empty() {
segment_count -= 1;
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(Error::BadChar { char: '\\' });
} else if cfg!(windows) && segment.contains(':') {
return Err(Error::BadChar { char: ':' });
} else {
buf.push(segment)
path.push(segment)
}
}
for (i, component) in path.components().enumerate() {
if !matches!(component, Component::Normal(_)) || i >= segment_count {
return Err(Error::InvalidComponents);
}
}
Ok(UriPathBuf(buf))
Ok(UriPathBuf(path))
}
}
impl FromStr for UriPathBuf {
type Err = Error;
fn from_str(raw_path: &str) -> Result<Self, Self::Err> {
Self::new(raw_path)
}
}
......@@ -70,8 +106,8 @@ impl FromRequest for UriPathBuf {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.try_into())
fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(request.try_into())
}
}
......@@ -79,7 +115,7 @@ impl TryFrom<&HttpRequest> for UriPathBuf {
type Error = Error;
fn try_from(request: &HttpRequest) -> Result<Self, Error> {
UriPathBuf::new(request.match_info().path())
request.match_info().unprocessed().parse()
}
}
......@@ -87,13 +123,13 @@ impl TryFrom<&ServiceRequest> for UriPathBuf {
type Error = Error;
fn try_from(request: &ServiceRequest) -> Result<Self, Error> {
UriPathBuf::new(request.match_info().path())
request.match_info().unprocessed().parse()
}
}
impl From<UriPathBuf> for PathBuf {
fn from(buf: UriPathBuf) -> PathBuf {
buf.0
fn from(path: UriPathBuf) -> PathBuf {
path.0
}
}
......@@ -110,33 +146,31 @@ mod tests {
#[actix_rt::test]
async fn test_path_buf() {
assert_eq!(
UriPathBuf::new("/test/.tt").map(|t| t.0),
Err(Error::BadStart { char: '.' })
);
assert_eq!(
UriPathBuf::new("/test/*tt").map(|t| t.0),
Err(Error::BadStart { char: '*' })
);
assert_eq!(
UriPathBuf::new("/test/tt:").map(|t| t.0),
Err(Error::BadEnd { char: ':' })
);
assert_eq!(
UriPathBuf::new("/test/tt<").map(|t| t.0),
Err(Error::BadEnd { char: '<' })
);
assert_eq!(
UriPathBuf::new("/test/tt>").map(|t| t.0),
Err(Error::BadEnd { char: '>' })
);
assert_eq!(
UriPathBuf::new("/seg1/seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg1", "seg2"])
);
assert_eq!(
UriPathBuf::new("/seg1/../seg2/").unwrap().0,
PathBuf::from_iter(vec!["seg2"])
);
let cases: &[(&str, Result<PathBuf, Error>)] = &[
("/test/.tt", Err(Error::BadStart { char: '.' })),
("/test/*tt", Err(Error::BadStart { char: '*' })),
("/test/tt:", Err(Error::BadEnd { char: ':' })),
("/test/tt<", Err(Error::BadEnd { char: '<' })),
("/test/tt>", Err(Error::BadEnd { char: '>' })),
("hello%20world", Ok(PathBuf::from_iter(vec!["hello world"]))),
(
"/testing%21/hello%20world",
Ok(PathBuf::from_iter(vec!["testing!", "hello world"])),
),
("/seg1/seg2/", Ok(PathBuf::from_iter(vec!["seg1", "seg2"]))),
("/seg1/../seg2/", Ok(PathBuf::from_iter(vec!["seg2"]))),
(
"/../../../../../dev/null",
Ok(PathBuf::from_iter(vec!["dev/null"])),
),
(
"/../../../..%2F../dev/null",
Err(Error::BadChar { char: '/' }),
),
];
for (input, expected) in cases {
assert_eq!(&UriPathBuf::new(input).map(|t| t.0), expected)
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment