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
/// 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`)
/// prefixed with the path to the crate root. Use `Path::new()` to retrieve an
/// `&'static Path`.
/// `$path`, an absolute or, preferably, a relative path. It returns a path (an
/// `&'static str`) prefixed with the path to the crate root. Use `Path::new()`
/// to retrieve an `&'static Path`.
///
/// See the [relative paths `StaticFiles`
/// documentation](`StaticFiles`#relative-paths) for an example.
@ -52,7 +52,11 @@ use rocket::response::{NamedFile, Redirect};
#[macro_export]
macro_rules! crate_relative {
($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)]
impl Options {
/// `Options` representing the empty set: no options are enabled. This is
/// different than [`Options::default()`](#impl-Default), which enables
/// `Index`.
/// All options disabled.
///
/// This is different than [`Options::default()`](#impl-Default), which
/// enables `Options::Index`.
pub const None: Options = Options(0b0000);
/// `Options` enabling responding to requests for a directory with the
/// `index.html` file in that directory, if it exists. When this is enabled,
/// the [`StaticFiles`] handler will respond to requests for a directory
/// `/foo` of `/foo/` with the file `${root}/foo/index.html` if it exists.
/// This is enabled by default.
/// Respond to requests for a directory with the `index.html` file in that
/// directory, if it exists.
///
/// When enabled, [`StaticFiles`] will respond to requests for a directory
/// `/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);
/// `Options` enabling returning dot files. When this is enabled, the
/// [`StaticFiles`] handler will respond to requests for files or
/// Allow requests to dotfiles.
///
/// When enabled, [`StaticFiles`] will respond to requests for files or
/// 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);
/// `Options` that normalizes directory requests by redirecting requests to
/// directory paths without a trailing slash to ones with a trailing slash.
/// Normalizes directory requests by redirecting requests to directory paths
/// without a trailing slash to ones with a trailing slash.
///
/// When enabled, the [`StaticFiles`] handler will respond to requests for a
/// directory without a trailing `/` with a permanent redirect (308) to the
/// same path with a trailing `/`. This ensures relative URLs within any
/// 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
///
@ -339,58 +352,41 @@ impl StaticFiles {
impl Into<Vec<Route>> for StaticFiles {
fn into(self) -> Vec<Route> {
let non_index = Route::ranked(self.rank, Method::Get, "/<path..>", self.clone());
// `Index` requires routing the index for obvious reasons.
// `NormalizeDirs` requires routing the index so a `.mount("/foo")` with
// a request `/foo`, can be redirected to `/foo/`.
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]
}
let mut route = Route::ranked(self.rank, Method::Get, "/<path..>", self);
route.name = Some("StaticFiles");
// route.name = format!("StaticFiles({})", self.root.fancy_display());
vec![route]
}
}
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]
impl Handler for StaticFiles {
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
// requested a handling of index files.
let current_route = req.route().expect("route while handling");
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);
// Get the segments as a `PathBuf`, allowing dotfiles requested.
let options = self.options;
let allow_dotfiles = options.contains(Options::DotFiles);
let path = req.segments::<Segments<'_>>(0..).ok()
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
.map(|path| self.root.join(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()),
None => Outcome::forward(data),
}

View File

@ -296,3 +296,49 @@ fn test_query_collection() {
let rocket = rocket::ignite().mount("/", routes![query_collection_2]);
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 {
if route.metadata.wild_path || other.metadata.wild_path {
return true;
}
let a_segments = &route.metadata.path_segs;
let b_segments = &other.metadata.path_segs;
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 {
let route_segments = &route.metadata.path_segs;
let req_segments = req.routed_segments(0..);
if route_segments.len() > req_segments.len() {
if route_segments.len() > req_segments.len() + 1 {
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 {
@ -188,6 +195,8 @@ mod tests {
#[test]
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>/hi", "/hello/<person>/hi"));
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("/<b>/<c>/<a..>", "/a/hi/hey/hayo"));
assert!(unranked_collide("/<b>/<c>/hey/hayo", "/a/hi/hey/hayo"));
assert!(unranked_collide("/<a..>", "/foo"));
}
#[test]
fn medium_param_collisions() {
assert!(unranked_collide("/<a>", "/b"));
assert!(unranked_collide("/hello/<name>", "/hello/bob"));
assert!(unranked_collide("/<name>", "//bob"));
}
@ -217,6 +228,13 @@ mod tests {
assert!(unranked_collide("/<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..>", "/"));
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]
@ -244,10 +262,6 @@ mod tests {
assert!(!unranked_collide("/hello", "/a/c"));
assert!(!unranked_collide("/hello/there", "/hello/there/guy"));
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("/a", "/aa"));
assert!(!unranked_collide("/a", "/aaa"));
@ -271,6 +285,7 @@ mod tests {
assert!(!m_collide((Post, "/a"), (Put, "/")));
assert!(!m_collide((Get, "/a"), (Put, "/")));
assert!(!m_collide((Get, "/hello"), (Put, "/hello")));
assert!(!m_collide((Get, "/<foo..>"), (Post, "/")));
}
#[test]
@ -303,13 +318,14 @@ mod tests {
assert!(!s_s_collide("/hello", "/a/c"));
assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
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("/a", "/aa"));
assert!(!s_s_collide("/a", "/aaa"));
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 {

View File

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

View File

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

View File

@ -1,5 +1,3 @@
#[macro_use] extern crate rocket;
#[cfg(test)] mod tests;
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
// prefer to use `StaticFiles`!
mod manual {
use std::path::{PathBuf, Path};
use rocket::response::NamedFile;
#[rocket::get("/rocket-icon.jpg")]
pub async fn icon() -> Option<NamedFile> {
NamedFile::open("static/rocket-icon.jpg").await.ok()
#[rocket::get("/second/<path..>")]
pub async fn second(path: PathBuf) -> Option<NamedFile> {
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]
fn rocket() -> rocket::Rocket {
#[rocket::launch]
fn rocket() -> _ {
rocket::ignite()
.mount("/", routes![manual::icon])
.mount("/", StaticFiles::from(crate_relative!("/static")))
.mount("/", rocket::routes![manual::second])
.mount("/", StaticFiles::from(crate_relative!("static")))
}

View File

@ -6,6 +6,7 @@ use rocket::http::Status;
use super::rocket;
#[track_caller]
fn test_query_file<T> (path: &str, file: T, status: Status)
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("/?v=1", "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]
fn test_hidden_file() {
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&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]
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("/second/rocket-icon.jpg", "static/rocket-icon.jpg", Status::Ok);
}
#[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.
As an example, the following route matches against all paths that begin with
`/page/`:
`/page`:
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use rocket::get;
use std::path::PathBuf;
#[get("/page/<path..>")]
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
[path traversal attacks](https://www.owasp.org/index.php/Path_Traversal). With
this, a safe and secure static file server can be implemented in 4 lines:
[path traversal attacks]. With this, a safe and secure static file server can be
implemented in just 4 lines:
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use std::path::{Path, PathBuf};
use std::path::{Path, PathBuf};
use rocket::response::NamedFile;
#[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!
If you need to serve static files from your Rocket application, consider using
the [`StaticFiles`] custom handler from [`rocket_contrib`], which makes it as
simple as:
`rocket.mount("/public", StaticFiles::from("/static"))`
`rocket.mount("/public", StaticFiles::from("static/"))`
[`rocket_contrib`]: @api/rocket_contrib/
[`StaticFiles`]: @api/rocket_contrib/serve/struct.StaticFiles.html