Allow '<path..>' to match zero segments.

This changes core routing so that '<path..>' in a route URI matches zero
or more segments. Previously, '<path..>' matched _1_ or more.

  * Routes '$a' and '$b/<p..>' collide if $a and $b previously collided.
  * For example, '/' now collides with '/<p..>'.
  * Request '$a' matches route '$b/<p..>' if $a previously matched $b.
  * For example, request '/' matches route '/<p..>'.

Resolves #985.
This commit is contained in:
Sergio Benitez 2021-03-05 02:01:24 -08:00
parent 08ae0d0b8c
commit 4d0042c395
9 changed files with 303 additions and 169 deletions

View File

@ -25,9 +25,9 @@ use rocket::response::{NamedFile, Redirect};
/// ///
/// This macro is primarily intended for use with [`StaticFiles`] to serve files /// This macro is primarily intended for use with [`StaticFiles`] to serve files
/// from a path relative to the crate root. The macro accepts one parameter, /// from a path relative to the crate root. The macro accepts one parameter,
/// `$path`, an absolute or relative path. It returns a path (an `&'static str`) /// `$path`, an absolute or, preferably, a relative path. It returns a path (an
/// prefixed with the path to the crate root. Use `Path::new()` to retrieve an /// `&'static str`) prefixed with the path to the crate root. Use `Path::new()`
/// `&'static Path`. /// to retrieve an `&'static Path`.
/// ///
/// See the [relative paths `StaticFiles` /// See the [relative paths `StaticFiles`
/// documentation](`StaticFiles`#relative-paths) for an example. /// documentation](`StaticFiles`#relative-paths) for an example.
@ -52,7 +52,11 @@ use rocket::response::{NamedFile, Redirect};
#[macro_export] #[macro_export]
macro_rules! crate_relative { macro_rules! crate_relative {
($path:expr) => { ($path:expr) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path) if cfg!(windows) {
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
} else {
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
}
}; };
} }
@ -77,32 +81,41 @@ pub struct Options(u8);
#[allow(non_upper_case_globals, non_snake_case)] #[allow(non_upper_case_globals, non_snake_case)]
impl Options { impl Options {
/// `Options` representing the empty set: no options are enabled. This is /// All options disabled.
/// different than [`Options::default()`](#impl-Default), which enables ///
/// `Index`. /// This is different than [`Options::default()`](#impl-Default), which
/// enables `Options::Index`.
pub const None: Options = Options(0b0000); pub const None: Options = Options(0b0000);
/// `Options` enabling responding to requests for a directory with the /// Respond to requests for a directory with the `index.html` file in that
/// `index.html` file in that directory, if it exists. When this is enabled, /// directory, if it exists.
/// the [`StaticFiles`] handler will respond to requests for a directory ///
/// `/foo` of `/foo/` with the file `${root}/foo/index.html` if it exists. /// When enabled, [`StaticFiles`] will respond to requests for a directory
/// This is enabled by default. /// `/foo` or `/foo/` with the file at `${root}/foo/index.html` if it
/// exists. When disabled, requests to directories will always forward.
///
/// **Enabled by default.**
pub const Index: Options = Options(0b0001); pub const Index: Options = Options(0b0001);
/// `Options` enabling returning dot files. When this is enabled, the /// Allow requests to dotfiles.
/// [`StaticFiles`] handler will respond to requests for files or ///
/// When enabled, [`StaticFiles`] will respond to requests for files or
/// directories beginning with `.`. When disabled, any dotfiles will be /// directories beginning with `.`. When disabled, any dotfiles will be
/// treated as missing. This is _not_ enabled by default. /// treated as missing.
///
/// **Disabled by default.**
pub const DotFiles: Options = Options(0b0010); pub const DotFiles: Options = Options(0b0010);
/// `Options` that normalizes directory requests by redirecting requests to /// Normalizes directory requests by redirecting requests to directory paths
/// directory paths without a trailing slash to ones with a trailing slash. /// without a trailing slash to ones with a trailing slash.
/// ///
/// When enabled, the [`StaticFiles`] handler will respond to requests for a /// When enabled, the [`StaticFiles`] handler will respond to requests for a
/// directory without a trailing `/` with a permanent redirect (308) to the /// directory without a trailing `/` with a permanent redirect (308) to the
/// same path with a trailing `/`. This ensures relative URLs within any /// same path with a trailing `/`. This ensures relative URLs within any
/// document served from that directory will be interpreted relative to that /// document served from that directory will be interpreted relative to that
/// directory rather than its parent. This is _not_ enabled by default. /// directory rather than its parent.
///
/// **Disabled by default.**
/// ///
/// # Example /// # Example
/// ///
@ -339,58 +352,41 @@ impl StaticFiles {
impl Into<Vec<Route>> for StaticFiles { impl Into<Vec<Route>> for StaticFiles {
fn into(self) -> Vec<Route> { fn into(self) -> Vec<Route> {
let non_index = Route::ranked(self.rank, Method::Get, "/<path..>", self.clone()); let mut route = Route::ranked(self.rank, Method::Get, "/<path..>", self);
// `Index` requires routing the index for obvious reasons. route.name = Some("StaticFiles");
// `NormalizeDirs` requires routing the index so a `.mount("/foo")` with // route.name = format!("StaticFiles({})", self.root.fancy_display());
// a request `/foo`, can be redirected to `/foo/`. vec![route]
if self.options.contains(Options::Index) || self.options.contains(Options::NormalizeDirs) {
let index = Route::ranked(self.rank, Method::Get, "/", self);
vec![index, non_index]
} else {
vec![non_index]
}
} }
} }
async fn handle_dir<'r, P>(opt: Options, r: &'r Request<'_>, d: Data, p: P) -> Outcome<'r>
where P: AsRef<Path>
{
if opt.contains(Options::NormalizeDirs) && !r.uri().path().ends_with('/') {
let new_path = r.uri().map_path(|p| format!("{}/", p))
.expect("adding a trailing slash to a known good path results in a valid path")
.into_owned();
return Outcome::from_or_forward(r, d, Redirect::permanent(new_path));
}
if !opt.contains(Options::Index) {
return Outcome::forward(d);
}
let file = NamedFile::open(p.as_ref().join("index.html")).await.ok();
Outcome::from_or_forward(r, d, file)
}
#[rocket::async_trait] #[rocket::async_trait]
impl Handler for StaticFiles { impl Handler for StaticFiles {
async fn handle<'r, 's: 'r>(&'s self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { async fn handle<'r, 's: 'r>(&'s self, req: &'r Request<'_>, data: Data) -> Outcome<'r> {
// If this is not the route with segments, handle it only if the user // Get the segments as a `PathBuf`, allowing dotfiles requested.
// requested a handling of index files. let options = self.options;
let current_route = req.route().expect("route while handling"); let allow_dotfiles = options.contains(Options::DotFiles);
let is_segments_route = current_route.uri.path().ends_with(">");
if !is_segments_route {
return handle_dir(self.options, req, data, &self.root).await;
}
// Otherwise, we're handling segments. Get the segments as a `PathBuf`,
// only allowing dotfiles if the user allowed it.
let allow_dotfiles = self.options.contains(Options::DotFiles);
let path = req.segments::<Segments<'_>>(0..).ok() let path = req.segments::<Segments<'_>>(0..).ok()
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
.map(|path| self.root.join(path)); .map(|path| self.root.join(path));
match path { match path {
Some(p) if p.is_dir() => handle_dir(self.options, req, data, p).await, Some(p) if p.is_dir() => {
// Normalize '/a/b/foo' to '/a/b/foo/'.
if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') {
let normal = req.uri().map_path(|p| format!("{}/", p))
.expect("adding a trailing slash to a known good path => valid path")
.into_owned();
return Outcome::from_or_forward(req, data, Redirect::permanent(normal));
}
if !options.contains(Options::Index) {
return Outcome::forward(data);
}
let index = NamedFile::open(p.join("index.html")).await.ok();
Outcome::from_or_forward(req, data, index)
},
Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()), Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()),
None => Outcome::forward(data), None => Outcome::forward(data),
} }

View File

@ -296,3 +296,49 @@ fn test_query_collection() {
let rocket = rocket::ignite().mount("/", routes![query_collection_2]); let rocket = rocket::ignite().mount("/", routes![query_collection_2]);
run_tests(rocket); run_tests(rocket);
} }
use rocket::request::FromSegments;
use rocket::http::uri::Segments;
struct PathString(String);
impl FromSegments<'_> for PathString {
type Error = std::convert::Infallible;
fn from_segments(segments: Segments<'_>) -> Result<Self, Self::Error> {
Ok(PathString(segments.collect::<Vec<_>>().join("/")))
}
}
#[get("/<_>/b/<path..>", rank = 1)]
fn segments(path: PathString) -> String {
format!("nonempty+{}", path.0)
}
#[get("/<path..>", rank = 2)]
fn segments_empty(path: PathString) -> String {
format!("empty+{}", path.0)
}
#[test]
fn test_inclusive_segments() {
let rocket = rocket::ignite()
.mount("/", routes![segments])
.mount("/", routes![segments_empty]);
let client = Client::untracked(rocket).unwrap();
let get = |uri| client.get(uri).dispatch().into_string().unwrap();
assert_eq!(get("/"), "empty+");
assert_eq!(get("//"), "empty+");
assert_eq!(get("//a/"), "empty+a");
assert_eq!(get("//a//"), "empty+a");
assert_eq!(get("//a//c/d"), "empty+a/c/d");
assert_eq!(get("//a/b"), "nonempty+");
assert_eq!(get("//a/b/c"), "nonempty+c");
assert_eq!(get("//a/b//c"), "nonempty+c");
assert_eq!(get("//a//b////c"), "nonempty+c");
assert_eq!(get("//a//b////c/d/e"), "nonempty+c/d/e");
}

View File

@ -46,6 +46,10 @@ impl Route {
} }
fn paths_collide(route: &Route, other: &Route) -> bool { fn paths_collide(route: &Route, other: &Route) -> bool {
if route.metadata.wild_path || other.metadata.wild_path {
return true;
}
let a_segments = &route.metadata.path_segs; let a_segments = &route.metadata.path_segs;
let b_segments = &other.metadata.path_segs; let b_segments = &other.metadata.path_segs;
for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) { for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) {
@ -60,13 +64,15 @@ fn paths_collide(route: &Route, other: &Route) -> bool {
} }
} }
a_segments.len() == b_segments.len() a_segments.get(b_segments.len()).map_or(false, |s| s.trailing)
|| b_segments.get(a_segments.len()).map_or(false, |s| s.trailing)
|| a_segments.len() == b_segments.len()
} }
fn paths_match(route: &Route, req: &Request<'_>) -> bool { fn paths_match(route: &Route, req: &Request<'_>) -> bool {
let route_segments = &route.metadata.path_segs; let route_segments = &route.metadata.path_segs;
let req_segments = req.routed_segments(0..); let req_segments = req.routed_segments(0..);
if route_segments.len() > req_segments.len() { if route_segments.len() > req_segments.len() + 1 {
return false; return false;
} }
@ -84,7 +90,8 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool {
} }
} }
route_segments.len() == req_segments.len() route_segments.get(req_segments.len()).map_or(false, |s| s.trailing)
|| route_segments.len() == req_segments.len()
} }
fn queries_match(route: &Route, req: &Request<'_>) -> bool { fn queries_match(route: &Route, req: &Request<'_>) -> bool {
@ -188,6 +195,8 @@ mod tests {
#[test] #[test]
fn simple_param_collisions() { fn simple_param_collisions() {
assert!(unranked_collide("/<a>", "/<b>"));
assert!(unranked_collide("/<a>", "/b"));
assert!(unranked_collide("/hello/<name>", "/hello/<person>")); assert!(unranked_collide("/hello/<name>", "/hello/<person>"));
assert!(unranked_collide("/hello/<name>/hi", "/hello/<person>/hi")); assert!(unranked_collide("/hello/<name>/hi", "/hello/<person>/hi"));
assert!(unranked_collide("/hello/<name>/hi/there", "/hello/<person>/hi/there")); assert!(unranked_collide("/hello/<name>/hi/there", "/hello/<person>/hi/there"));
@ -203,10 +212,12 @@ mod tests {
assert!(unranked_collide("/a/<b>/<c>/<a..>", "/a/hi/hey/hayo")); assert!(unranked_collide("/a/<b>/<c>/<a..>", "/a/hi/hey/hayo"));
assert!(unranked_collide("/<b>/<c>/<a..>", "/a/hi/hey/hayo")); assert!(unranked_collide("/<b>/<c>/<a..>", "/a/hi/hey/hayo"));
assert!(unranked_collide("/<b>/<c>/hey/hayo", "/a/hi/hey/hayo")); assert!(unranked_collide("/<b>/<c>/hey/hayo", "/a/hi/hey/hayo"));
assert!(unranked_collide("/<a..>", "/foo"));
} }
#[test] #[test]
fn medium_param_collisions() { fn medium_param_collisions() {
assert!(unranked_collide("/<a>", "/b"));
assert!(unranked_collide("/hello/<name>", "/hello/bob")); assert!(unranked_collide("/hello/<name>", "/hello/bob"));
assert!(unranked_collide("/<name>", "//bob")); assert!(unranked_collide("/<name>", "//bob"));
} }
@ -217,6 +228,13 @@ mod tests {
assert!(unranked_collide("/<a..>", "//a/bcjdklfj//<c>")); assert!(unranked_collide("/<a..>", "//a/bcjdklfj//<c>"));
assert!(unranked_collide("/a/<a..>", "//a/bcjdklfj//<c>")); assert!(unranked_collide("/a/<a..>", "//a/bcjdklfj//<c>"));
assert!(unranked_collide("/a/<b>/<c..>", "//a/bcjdklfj//<c>")); assert!(unranked_collide("/a/<b>/<c..>", "//a/bcjdklfj//<c>"));
assert!(unranked_collide("/<a..>", "/"));
assert!(unranked_collide("/", "/<_..>"));
assert!(unranked_collide("/a/b/<a..>", "/a/<b..>"));
assert!(unranked_collide("/a/b/<a..>", "/a/<b>/<b..>"));
assert!(unranked_collide("/hi/<a..>", "/hi"));
assert!(unranked_collide("/hi/<a..>", "/hi/"));
assert!(unranked_collide("/<a..>", "//////"));
} }
#[test] #[test]
@ -244,10 +262,6 @@ mod tests {
assert!(!unranked_collide("/hello", "/a/c")); assert!(!unranked_collide("/hello", "/a/c"));
assert!(!unranked_collide("/hello/there", "/hello/there/guy")); assert!(!unranked_collide("/hello/there", "/hello/there/guy"));
assert!(!unranked_collide("/a/<b>", "/b/<b>")); assert!(!unranked_collide("/a/<b>", "/b/<b>"));
assert!(!unranked_collide("/<a..>", "/"));
assert!(!unranked_collide("/hi/<a..>", "/hi"));
assert!(!unranked_collide("/hi/<a..>", "/hi/"));
assert!(!unranked_collide("/<a..>", "//////"));
assert!(!unranked_collide("/t", "/test")); assert!(!unranked_collide("/t", "/test"));
assert!(!unranked_collide("/a", "/aa")); assert!(!unranked_collide("/a", "/aa"));
assert!(!unranked_collide("/a", "/aaa")); assert!(!unranked_collide("/a", "/aaa"));
@ -271,6 +285,7 @@ mod tests {
assert!(!m_collide((Post, "/a"), (Put, "/"))); assert!(!m_collide((Post, "/a"), (Put, "/")));
assert!(!m_collide((Get, "/a"), (Put, "/"))); assert!(!m_collide((Get, "/a"), (Put, "/")));
assert!(!m_collide((Get, "/hello"), (Put, "/hello"))); assert!(!m_collide((Get, "/hello"), (Put, "/hello")));
assert!(!m_collide((Get, "/<foo..>"), (Post, "/")));
} }
#[test] #[test]
@ -303,13 +318,14 @@ mod tests {
assert!(!s_s_collide("/hello", "/a/c")); assert!(!s_s_collide("/hello", "/a/c"));
assert!(!s_s_collide("/hello/there", "/hello/there/guy")); assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
assert!(!s_s_collide("/a/<b>", "/b/<b>")); assert!(!s_s_collide("/a/<b>", "/b/<b>"));
assert!(!s_s_collide("/<a..>", "/"));
assert!(!s_s_collide("/hi/<a..>", "/hi/"));
assert!(!s_s_collide("/a/hi/<a..>", "/a/hi/"));
assert!(!s_s_collide("/t", "/test")); assert!(!s_s_collide("/t", "/test"));
assert!(!s_s_collide("/a", "/aa")); assert!(!s_s_collide("/a", "/aa"));
assert!(!s_s_collide("/a", "/aaa")); assert!(!s_s_collide("/a", "/aaa"));
assert!(!s_s_collide("/", "/a")); assert!(!s_s_collide("/", "/a"));
assert!(s_s_collide("/a/hi/<a..>", "/a/hi/"));
assert!(s_s_collide("/hi/<a..>", "/hi/"));
assert!(s_s_collide("/<a..>", "/"));
} }
fn mt_mt_collide(mt1: &str, mt2: &str) -> bool { fn mt_mt_collide(mt1: &str, mt2: &str) -> bool {

View File

@ -125,7 +125,7 @@ mod test {
router router
} }
fn router_with_unranked_routes(routes: &[&'static str]) -> Router { fn router_with_rankless_routes(routes: &[&'static str]) -> Router {
let mut router = Router::new(); let mut router = Router::new();
for route in routes { for route in routes {
let route = Route::ranked(0, Get, route.to_string(), dummy); let route = Route::ranked(0, Get, route.to_string(), dummy);
@ -135,8 +135,8 @@ mod test {
router router
} }
fn unranked_route_collisions(routes: &[&'static str]) -> bool { fn rankless_route_collisions(routes: &[&'static str]) -> bool {
let router = router_with_unranked_routes(routes); let router = router_with_rankless_routes(routes);
router.has_collisions() router.has_collisions()
} }
@ -146,75 +146,88 @@ mod test {
} }
#[test] #[test]
fn test_collisions() { fn test_rankless_collisions() {
assert!(unranked_route_collisions(&["/hello", "/hello"])); assert!(rankless_route_collisions(&["/hello", "/hello"]));
assert!(unranked_route_collisions(&["/<a>", "/hello"])); assert!(rankless_route_collisions(&["/<a>", "/hello"]));
assert!(unranked_route_collisions(&["/<a>", "/<b>"])); assert!(rankless_route_collisions(&["/<a>", "/<b>"]));
assert!(unranked_route_collisions(&["/hello/bob", "/hello/<b>"])); assert!(rankless_route_collisions(&["/hello/bob", "/hello/<b>"]));
assert!(unranked_route_collisions(&["/a/b/<c>/d", "/<a>/<b>/c/d"])); assert!(rankless_route_collisions(&["/a/b/<c>/d", "/<a>/<b>/c/d"]));
assert!(unranked_route_collisions(&["/a/b", "/<a..>"]));
assert!(unranked_route_collisions(&["/a/b/c", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/b", "/<a..>"]));
assert!(unranked_route_collisions(&["/<a>/b", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/b/c", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/<b>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/<a>/b", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/b/<c>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/<b>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/<a..>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/b/<c>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/<a..>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/<a..>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/b/<a..>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/<a..>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/b/c/d", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/b/<a..>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/<_>", "/<_>"])); assert!(rankless_route_collisions(&["/a/b/c/d", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/<_>", "/a/b"])); assert!(rankless_route_collisions(&["/", "/<a..>"]));
assert!(unranked_route_collisions(&["/a/<_>", "/a/<b>"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/<_..>", "/a/b"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/<_..>"]));
assert!(unranked_route_collisions(&["/<_..>", "/<_>"])); assert!(rankless_route_collisions(&["/<_>", "/a/<_..>"]));
assert!(unranked_route_collisions(&["/<_>/b", "/a/b"])); assert!(rankless_route_collisions(&["/foo", "/foo/<_..>"]));
assert!(rankless_route_collisions(&["/foo/bar/baz", "/foo/<_..>"]));
assert!(rankless_route_collisions(&["/a/d/<b..>", "/a/d"]));
assert!(rankless_route_collisions(&["/a/<_..>", "/<_>"]));
assert!(rankless_route_collisions(&["/a/<_..>", "/a"]));
assert!(rankless_route_collisions(&["/<a>", "/a/<a..>"]));
assert!(rankless_route_collisions(&["/<_>", "/<_>"]));
assert!(rankless_route_collisions(&["/a/<_>", "/a/b"]));
assert!(rankless_route_collisions(&["/a/<_>", "/a/<b>"]));
assert!(rankless_route_collisions(&["/<_..>", "/a/b"]));
assert!(rankless_route_collisions(&["/<_..>", "/<_>"]));
assert!(rankless_route_collisions(&["/<_>/b", "/a/b"]));
assert!(rankless_route_collisions(&["/", "/<foo..>"]));
} }
#[test] #[test]
fn test_collisions_normalize() { fn test_collisions_normalize() {
assert!(unranked_route_collisions(&["/hello/", "/hello"])); assert!(rankless_route_collisions(&["/hello/", "/hello"]));
assert!(unranked_route_collisions(&["//hello/", "/hello"])); assert!(rankless_route_collisions(&["//hello/", "/hello"]));
assert!(unranked_route_collisions(&["//hello/", "/hello//"])); assert!(rankless_route_collisions(&["//hello/", "/hello//"]));
assert!(unranked_route_collisions(&["/<a>", "/hello//"])); assert!(rankless_route_collisions(&["/<a>", "/hello//"]));
assert!(unranked_route_collisions(&["/<a>", "/hello///"])); assert!(rankless_route_collisions(&["/<a>", "/hello///"]));
assert!(unranked_route_collisions(&["/hello///bob", "/hello/<b>"])); assert!(rankless_route_collisions(&["/hello///bob", "/hello/<b>"]));
assert!(unranked_route_collisions(&["/<a..>//", "/a//<a..>"])); assert!(rankless_route_collisions(&["/<a..>//", "/a//<a..>"]));
assert!(unranked_route_collisions(&["/a/<a..>//", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/<a..>//", "/a/<a..>"]));
assert!(unranked_route_collisions(&["/a/<a..>//", "/a/b//c//d/"])); assert!(rankless_route_collisions(&["/a/<a..>//", "/a/b//c//d/"]));
assert!(unranked_route_collisions(&["/a/<a..>/", "/a/bd/e/"])); assert!(rankless_route_collisions(&["/a/<a..>/", "/a/bd/e/"]));
assert!(unranked_route_collisions(&["/a/<a..>//", "/a/b//c//d/e/"])); assert!(rankless_route_collisions(&["/<a..>/", "/a/bd/e/"]));
assert!(unranked_route_collisions(&["/a//<a..>//", "/a/b//c//d/e/"])); assert!(rankless_route_collisions(&["//", "/<foo..>"]));
assert!(unranked_route_collisions(&["///<_>", "/<_>"])); assert!(rankless_route_collisions(&["/a/<a..>//", "/a/b//c//d/e/"]));
assert!(unranked_route_collisions(&["/a/<_>", "///a//b"])); assert!(rankless_route_collisions(&["/a//<a..>//", "/a/b//c//d/e/"]));
assert!(unranked_route_collisions(&["//a///<_>", "/a//<b>"])); assert!(rankless_route_collisions(&["///<_>", "/<_>"]));
assert!(unranked_route_collisions(&["//<_..>", "/a/b"])); assert!(rankless_route_collisions(&["/a/<_>", "///a//b"]));
assert!(unranked_route_collisions(&["//<_..>", "/<_>"])); assert!(rankless_route_collisions(&["//a///<_>", "/a//<b>"]));
assert!(rankless_route_collisions(&["//<_..>", "/a/b"]));
assert!(rankless_route_collisions(&["//<_..>", "/<_>"]));
assert!(rankless_route_collisions(&["///<a>/", "/a/<a..>"]));
assert!(rankless_route_collisions(&["///<a..>/", "/a/<a..>"]));
} }
#[test] #[test]
fn test_collisions_query() { fn test_collisions_query() {
// Query shouldn't affect things when unranked. // Query shouldn't affect things when rankless.
assert!(unranked_route_collisions(&["/hello?<foo>", "/hello"])); assert!(rankless_route_collisions(&["/hello?<foo>", "/hello"]));
assert!(unranked_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"])); assert!(rankless_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"]));
assert!(unranked_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"])); assert!(rankless_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"]));
assert!(unranked_route_collisions(&["/<a>", "/<b>?<foo>"])); assert!(rankless_route_collisions(&["/<a>", "/<b>?<foo>"]));
assert!(unranked_route_collisions(&["/hello/bob?a=b", "/hello/<b>?d=e"])); assert!(rankless_route_collisions(&["/hello/bob?a=b", "/hello/<b>?d=e"]));
assert!(unranked_route_collisions(&["/<foo>?a=b", "/foo?d=e"])); assert!(rankless_route_collisions(&["/<foo>?a=b", "/foo?d=e"]));
assert!(unranked_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e&<c>"])); assert!(rankless_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e&<c>"]));
assert!(unranked_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e"])); assert!(rankless_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e"]));
} }
#[test] #[test]
fn test_no_collisions() { fn test_no_collisions() {
assert!(!unranked_route_collisions(&["/<a>", "/a/<a..>"])); assert!(!rankless_route_collisions(&["/a/b", "/a/b/c"]));
assert!(!unranked_route_collisions(&["/a/b", "/a/b/c"])); assert!(!rankless_route_collisions(&["/a/b/c/d", "/a/b/c/<d>/e"]));
assert!(!unranked_route_collisions(&["/a/b/c/d", "/a/b/c/<d>/e"])); assert!(!rankless_route_collisions(&["/a/d/<b..>", "/a/b/c"]));
assert!(!unranked_route_collisions(&["/a/d/<b..>", "/a/b/c"])); assert!(!rankless_route_collisions(&["/<_>", "/"]));
assert!(!unranked_route_collisions(&["/a/d/<b..>", "/a/d"])); assert!(!rankless_route_collisions(&["/a/<_>", "/a"]));
assert!(!unranked_route_collisions(&["/<_>", "/"])); assert!(!rankless_route_collisions(&["/a/<_>", "/<_>"]));
assert!(!unranked_route_collisions(&["/a/<_>", "/a"]));
assert!(!unranked_route_collisions(&["/a/<_..>", "/a"]));
assert!(!unranked_route_collisions(&["/a/<_..>", "/<_>"]));
assert!(!unranked_route_collisions(&["/a/<_>", "/<_>"]));
} }
#[test] #[test]
@ -223,13 +236,21 @@ mod test {
assert!(!default_rank_route_collisions(&["/hello/bob", "/hello/<b>"])); assert!(!default_rank_route_collisions(&["/hello/bob", "/hello/<b>"]));
assert!(!default_rank_route_collisions(&["/a/b/c/d", "/<a>/<b>/c/d"])); assert!(!default_rank_route_collisions(&["/a/b/c/d", "/<a>/<b>/c/d"]));
assert!(!default_rank_route_collisions(&["/hi", "/<hi>"])); assert!(!default_rank_route_collisions(&["/hi", "/<hi>"]));
assert!(!default_rank_route_collisions(&["/hi", "/<hi>"])); assert!(!default_rank_route_collisions(&["/a", "/a/<path..>"]));
assert!(!default_rank_route_collisions(&["/", "/<path..>"]));
assert!(!default_rank_route_collisions(&["/a/b", "/a/b/<c..>"])); assert!(!default_rank_route_collisions(&["/a/b", "/a/b/<c..>"]));
assert!(!default_rank_route_collisions(&["/<_>", "/static"])); assert!(!default_rank_route_collisions(&["/<_>", "/static"]));
assert!(!default_rank_route_collisions(&["/<_..>", "/static"]));
assert!(!default_rank_route_collisions(&["/<path..>", "/"]));
assert!(!default_rank_route_collisions(&["/<_>/<_>", "/foo/bar"])); assert!(!default_rank_route_collisions(&["/<_>/<_>", "/foo/bar"]));
assert!(!default_rank_route_collisions(&["/foo/<_>", "/foo/bar"])); assert!(!default_rank_route_collisions(&["/foo/<_>", "/foo/bar"]));
} }
#[test]
fn test_collision_when_ranked() {
assert!(default_rank_route_collisions(&["/<a>", "/a/<path..>"]));
}
#[test] #[test]
fn test_collision_when_ranked_query() { fn test_collision_when_ranked_query() {
assert!(default_rank_route_collisions(&["/a?a=b", "/a?c=d"])); assert!(default_rank_route_collisions(&["/a?a=b", "/a?c=d"]));
@ -287,10 +308,18 @@ mod test {
assert!(route(&router, Delete, "/hello").is_some()); assert!(route(&router, Delete, "/hello").is_some());
let router = router_with_routes(&["/<a..>"]); let router = router_with_routes(&["/<a..>"]);
assert!(route(&router, Get, "/").is_some());
assert!(route(&router, Get, "//").is_some());
assert!(route(&router, Get, "/hi").is_some());
assert!(route(&router, Get, "/hello/hi").is_some()); assert!(route(&router, Get, "/hello/hi").is_some());
assert!(route(&router, Get, "/a/b/").is_some()); assert!(route(&router, Get, "/a/b/").is_some());
assert!(route(&router, Get, "/i/a").is_some()); assert!(route(&router, Get, "/i/a").is_some());
assert!(route(&router, Get, "/a/b/c/d/e/f").is_some()); assert!(route(&router, Get, "/a/b/c/d/e/f").is_some());
let router = router_with_routes(&["/foo/<a..>"]);
assert!(route(&router, Get, "/foo").is_some());
assert!(route(&router, Get, "/foo/").is_some());
assert!(route(&router, Get, "/foo///bar").is_some());
} }
#[test] #[test]
@ -320,11 +349,16 @@ mod test {
assert!(route(&router, Put, "/hello/hi").is_none()); assert!(route(&router, Put, "/hello/hi").is_none());
assert!(route(&router, Put, "/a/b").is_none()); assert!(route(&router, Put, "/a/b").is_none());
assert!(route(&router, Put, "/a/b").is_none()); assert!(route(&router, Put, "/a/b").is_none());
let router = router_with_routes(&["/prefix/<a..>"]);
assert!(route(&router, Get, "/").is_none());
assert!(route(&router, Get, "/prefi/").is_none());
} }
macro_rules! assert_ranked_routes { macro_rules! assert_ranked_match {
($routes:expr, $to:expr, $want:expr) => ({ ($routes:expr, $to:expr => $want:expr) => ({
let router = router_with_routes($routes); let router = router_with_routes($routes);
assert!(!router.has_collisions());
let route_path = route(&router, Get, $to).unwrap().uri.to_string(); let route_path = route(&router, Get, $to).unwrap().uri.to_string();
assert_eq!(route_path, $want.to_string()); assert_eq!(route_path, $want.to_string());
}) })
@ -332,24 +366,27 @@ mod test {
#[test] #[test]
fn test_default_ranking() { fn test_default_ranking() {
assert_ranked_routes!(&["/hello", "/<name>"], "/hello", "/hello"); assert_ranked_match!(&["/hello", "/<name>"], "/hello" => "/hello");
assert_ranked_routes!(&["/<name>", "/hello"], "/hello", "/hello"); assert_ranked_match!(&["/<name>", "/hello"], "/hello" => "/hello");
assert_ranked_routes!(&["/<a>", "/hi", "/<b>"], "/hi", "/hi"); assert_ranked_match!(&["/<a>", "/hi", "/hi/<b>"], "/hi" => "/hi");
assert_ranked_routes!(&["/<a>/b", "/hi/c"], "/hi/c", "/hi/c"); assert_ranked_match!(&["/<a>/b", "/hi/c"], "/hi/c" => "/hi/c");
assert_ranked_routes!(&["/<a>/<b>", "/hi/a"], "/hi/c", "/<a>/<b>"); assert_ranked_match!(&["/<a>/<b>", "/hi/a"], "/hi/c" => "/<a>/<b>");
assert_ranked_routes!(&["/hi/a", "/hi/<c>"], "/hi/c", "/hi/<c>"); assert_ranked_match!(&["/hi/a", "/hi/<c>"], "/hi/c" => "/hi/<c>");
assert_ranked_routes!(&["/a", "/a?<b>"], "/a?b=c", "/a?<b>"); assert_ranked_match!(&["/a", "/a?<b>"], "/a?b=c" => "/a?<b>");
assert_ranked_routes!(&["/a", "/a?<b>"], "/a", "/a?<b>"); assert_ranked_match!(&["/a", "/a?<b>"], "/a" => "/a?<b>");
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a", "/a?<b>"); assert_ranked_match!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a" => "/a?<b>");
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b", "/<a>?<b>"); assert_ranked_match!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b" => "/<a>?<b>");
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b?v=1", "/<a>?<b>"); assert_ranked_match!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b?v=1" => "/<a>?<b>");
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a?b=c", "/a?<b>"); assert_ranked_match!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a?b=c" => "/a?<b>");
assert_ranked_routes!(&["/a", "/a?b"], "/a?b", "/a?b"); assert_ranked_match!(&["/a", "/a?b"], "/a?b" => "/a?b");
assert_ranked_routes!(&["/<a>", "/a?b"], "/a?b", "/a?b"); assert_ranked_match!(&["/<a>", "/a?b"], "/a?b" => "/a?b");
assert_ranked_routes!(&["/a", "/<a>?b"], "/a?b", "/a"); assert_ranked_match!(&["/a", "/<a>?b"], "/a?b" => "/a");
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a", "/a?<b>"); assert_ranked_match!(&["/a?<c>&b", "/a?<b>"], "/a" => "/a?<b>");
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a?b", "/a?<c>&b"); assert_ranked_match!(&["/a?<c>&b", "/a?<b>"], "/a?b" => "/a?<c>&b");
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a?c", "/a?<b>"); assert_ranked_match!(&["/a?<c>&b", "/a?<b>"], "/a?c" => "/a?<b>");
assert_ranked_match!(&["/", "/<foo..>"], "/" => "/");
assert_ranked_match!(&["/", "/<foo..>"], "/hi" => "/<foo..>");
assert_ranked_match!(&["/hi", "/<foo..>"], "/hi" => "/hi");
} }
fn ranked_collisions(routes: &[(isize, &'static str)]) -> bool { fn ranked_collisions(routes: &[(isize, &'static str)]) -> bool {
@ -444,6 +481,12 @@ mod test {
with: [(1, "/a/<b..>"), (2, "/a/b/<c..>")], with: [(1, "/a/<b..>"), (2, "/a/b/<c..>")],
expect: (1, "/a/<b..>"), (2, "/a/b/<c..>") expect: (1, "/a/<b..>"), (2, "/a/b/<c..>")
); );
assert_ranked_routing!(
to: "/hi",
with: [(1, "/hi/<foo..>"), (0, "/hi/<foo>")],
expect: (1, "/hi/<foo..>")
);
} }
macro_rules! assert_default_ranked_routing { macro_rules! assert_default_ranked_routing {

View File

@ -43,6 +43,7 @@ pub(crate) struct Metadata {
pub static_query_fields: Vec<(String, String)>, pub static_query_fields: Vec<(String, String)>,
pub static_path: bool, pub static_path: bool,
pub wild_path: bool, pub wild_path: bool,
pub trailing_path: bool,
pub wild_query: bool, pub wild_query: bool,
} }
@ -185,6 +186,7 @@ impl Route {
static_path: path_segs.iter().all(|s| !s.dynamic), static_path: path_segs.iter().all(|s| !s.dynamic),
wild_path: path_segs.iter().all(|s| s.dynamic) wild_path: path_segs.iter().all(|s| s.dynamic)
&& path_segs.last().map_or(false, |p| p.trailing), && path_segs.last().map_or(false, |p| p.trailing),
trailing_path: path_segs.last().map_or(false, |p| p.trailing),
wild_query: query_segs.iter().all(|s| s.dynamic), wild_query: query_segs.iter().all(|s| s.dynamic),
static_query_fields: query_segs.iter().filter(|s| !s.dynamic) static_query_fields: query_segs.iter().filter(|s| !s.dynamic)
.map(|s| ValueField::parse(&s.value)) .map(|s| ValueField::parse(&s.value))

View File

@ -1,5 +1,3 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests; #[cfg(test)] mod tests;
use rocket_contrib::serve::{StaticFiles, crate_relative}; use rocket_contrib::serve::{StaticFiles, crate_relative};
@ -7,17 +5,23 @@ use rocket_contrib::serve::{StaticFiles, crate_relative};
// If we wanted or needed to serve files manually, we'd use `NamedFile`. Always // If we wanted or needed to serve files manually, we'd use `NamedFile`. Always
// prefer to use `StaticFiles`! // prefer to use `StaticFiles`!
mod manual { mod manual {
use std::path::{PathBuf, Path};
use rocket::response::NamedFile; use rocket::response::NamedFile;
#[rocket::get("/rocket-icon.jpg")] #[rocket::get("/second/<path..>")]
pub async fn icon() -> Option<NamedFile> { pub async fn second(path: PathBuf) -> Option<NamedFile> {
NamedFile::open("static/rocket-icon.jpg").await.ok() let mut path = Path::new(super::crate_relative!("static")).join(path);
if path.is_dir() {
path.push("index.html");
}
NamedFile::open(path).await.ok()
} }
} }
#[launch] #[rocket::launch]
fn rocket() -> rocket::Rocket { fn rocket() -> _ {
rocket::ignite() rocket::ignite()
.mount("/", routes![manual::icon]) .mount("/", rocket::routes![manual::second])
.mount("/", StaticFiles::from(crate_relative!("/static"))) .mount("/", StaticFiles::from(crate_relative!("static")))
} }

View File

@ -6,6 +6,7 @@ use rocket::http::Status;
use super::rocket; use super::rocket;
#[track_caller]
fn test_query_file<T> (path: &str, file: T, status: Status) fn test_query_file<T> (path: &str, file: T, status: Status)
where T: Into<Option<&'static str>> where T: Into<Option<&'static str>>
{ {
@ -33,19 +34,33 @@ fn test_index_html() {
test_query_file("/", "static/index.html", Status::Ok); test_query_file("/", "static/index.html", Status::Ok);
test_query_file("/?v=1", "static/index.html", Status::Ok); test_query_file("/?v=1", "static/index.html", Status::Ok);
test_query_file("/?this=should&be=ignored", "static/index.html", Status::Ok); test_query_file("/?this=should&be=ignored", "static/index.html", Status::Ok);
test_query_file("/second/", "static/index.html", Status::Ok);
test_query_file("/second/?v=1", "static/index.html", Status::Ok);
}
#[test]
fn test_hidden_index_html() {
test_query_file("/hidden", "static/hidden/index.html", Status::Ok);
test_query_file("/hidden/", "static/hidden/index.html", Status::Ok);
test_query_file("//hidden//", "static/hidden/index.html", Status::Ok);
test_query_file("/second/hidden", "static/hidden/index.html", Status::Ok);
test_query_file("/second/hidden/", "static/hidden/index.html", Status::Ok);
test_query_file("/second/hidden///", "static/hidden/index.html", Status::Ok);
} }
#[test] #[test]
fn test_hidden_file() { fn test_hidden_file() {
test_query_file("/hidden/hi.txt", "static/hidden/hi.txt", Status::Ok); test_query_file("/hidden/hi.txt", "static/hidden/hi.txt", Status::Ok);
test_query_file("/second/hidden/hi.txt", "static/hidden/hi.txt", Status::Ok);
test_query_file("/hidden/hi.txt?v=1", "static/hidden/hi.txt", Status::Ok); test_query_file("/hidden/hi.txt?v=1", "static/hidden/hi.txt", Status::Ok);
test_query_file("/hidden/hi.txt?v=1&a=b", "static/hidden/hi.txt", Status::Ok); test_query_file("/hidden/hi.txt?v=1&a=b", "static/hidden/hi.txt", Status::Ok);
test_query_file("/second/hidden/hi.txt?v=1&a=b", "static/hidden/hi.txt", Status::Ok);
} }
#[test] #[test]
fn test_icon_file() { fn test_icon_file() {
test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok); test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok);
test_query_file("/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok); test_query_file("/second/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok);
} }
#[test] #[test]

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Hmm...</title>
</head>
<body>
👀
</body>
</html>

View File

@ -120,28 +120,27 @@ path. The type of such parameters, known as _segments guards_, must implement
text after a segments guard will result in a compile-time error. text after a segments guard will result in a compile-time error.
As an example, the following route matches against all paths that begin with As an example, the following route matches against all paths that begin with
`/page/`: `/page`:
```rust ```rust
# #[macro_use] extern crate rocket; # use rocket::get;
# fn main() {}
use std::path::PathBuf; use std::path::PathBuf;
#[get("/page/<path..>")] #[get("/page/<path..>")]
fn get_page(path: PathBuf) { /* ... */ } fn get_page(path: PathBuf) { /* ... */ }
``` ```
The path after `/page/` will be available in the `path` parameter. The The path after `/page/` will be available in the `path` parameter, which may be
empty for paths that are simply `/page`, `/page/`, `/page//`, and so on. The
`FromSegments` implementation for `PathBuf` ensures that `path` cannot lead to `FromSegments` implementation for `PathBuf` ensures that `path` cannot lead to
[path traversal attacks](https://www.owasp.org/index.php/Path_Traversal). With [path traversal attacks]. With this, a safe and secure static file server can be
this, a safe and secure static file server can be implemented in 4 lines: implemented in just 4 lines:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# fn main() {} # fn main() {}
# use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rocket::response::NamedFile; use rocket::response::NamedFile;
#[get("/<file..>")] #[get("/<file..>")]
@ -150,13 +149,15 @@ async fn files(file: PathBuf) -> Option<NamedFile> {
} }
``` ```
[path traversal attacks]: https://www.owasp.org/index.php/Path_Traversal
! tip: Rocket makes it even _easier_ to serve static files! ! tip: Rocket makes it even _easier_ to serve static files!
If you need to serve static files from your Rocket application, consider using If you need to serve static files from your Rocket application, consider using
the [`StaticFiles`] custom handler from [`rocket_contrib`], which makes it as the [`StaticFiles`] custom handler from [`rocket_contrib`], which makes it as
simple as: simple as:
`rocket.mount("/public", StaticFiles::from("/static"))` `rocket.mount("/public", StaticFiles::from("static/"))`
[`rocket_contrib`]: @api/rocket_contrib/ [`rocket_contrib`]: @api/rocket_contrib/
[`StaticFiles`]: @api/rocket_contrib/serve/struct.StaticFiles.html [`StaticFiles`]: @api/rocket_contrib/serve/struct.StaticFiles.html