diff --git a/contrib/lib/src/serve.rs b/contrib/lib/src/serve.rs index 6d295fdb..58e6312a 100644 --- a/contrib/lib/src/serve.rs +++ b/contrib/lib/src/serve.rs @@ -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> for StaticFiles { fn into(self) -> Vec { - let non_index = Route::ranked(self.rank, Method::Get, "/", 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, "/", 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 -{ - 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::>(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), } diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index 9b8fb945..995a1e71 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -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 { + Ok(PathString(segments.collect::>().join("/"))) + } + +} + +#[get("/<_>/b/", rank = 1)] +fn segments(path: PathString) -> String { + format!("nonempty+{}", path.0) +} + +#[get("/", 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"); +} diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 0dce75c9..8a34a255 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -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("/", "/")); + assert!(unranked_collide("/", "/b")); assert!(unranked_collide("/hello/", "/hello/")); assert!(unranked_collide("/hello//hi", "/hello//hi")); assert!(unranked_collide("/hello//hi/there", "/hello//hi/there")); @@ -203,10 +212,12 @@ mod tests { assert!(unranked_collide("/a///", "/a/hi/hey/hayo")); assert!(unranked_collide("///", "/a/hi/hey/hayo")); assert!(unranked_collide("///hey/hayo", "/a/hi/hey/hayo")); + assert!(unranked_collide("/", "/foo")); } #[test] fn medium_param_collisions() { + assert!(unranked_collide("/", "/b")); assert!(unranked_collide("/hello/", "/hello/bob")); assert!(unranked_collide("/", "//bob")); } @@ -217,6 +228,13 @@ mod tests { assert!(unranked_collide("/", "//a/bcjdklfj//")); assert!(unranked_collide("/a/", "//a/bcjdklfj//")); assert!(unranked_collide("/a//", "//a/bcjdklfj//")); + assert!(unranked_collide("/", "/")); + assert!(unranked_collide("/", "/<_..>")); + assert!(unranked_collide("/a/b/", "/a/")); + assert!(unranked_collide("/a/b/", "/a//")); + assert!(unranked_collide("/hi/", "/hi")); + assert!(unranked_collide("/hi/", "/hi/")); + assert!(unranked_collide("/", "//////")); } #[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/")); - assert!(!unranked_collide("/", "/")); - assert!(!unranked_collide("/hi/", "/hi")); - assert!(!unranked_collide("/hi/", "/hi/")); - assert!(!unranked_collide("/", "//////")); 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, "/"), (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/")); - assert!(!s_s_collide("/", "/")); - assert!(!s_s_collide("/hi/", "/hi/")); - assert!(!s_s_collide("/a/hi/", "/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/hi/")); + assert!(s_s_collide("/hi/", "/hi/")); + assert!(s_s_collide("/", "/")); } fn mt_mt_collide(mt1: &str, mt2: &str) -> bool { diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs index 12fe220a..b71e9eed 100644 --- a/core/lib/src/router/mod.rs +++ b/core/lib/src/router/mod.rs @@ -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(&["/", "/hello"])); - assert!(unranked_route_collisions(&["/", "/"])); - assert!(unranked_route_collisions(&["/hello/bob", "/hello/"])); - assert!(unranked_route_collisions(&["/a/b//d", "///c/d"])); - assert!(unranked_route_collisions(&["/a/b", "/"])); - assert!(unranked_route_collisions(&["/a/b/c", "/a/"])); - assert!(unranked_route_collisions(&["//b", "/a/"])); - assert!(unranked_route_collisions(&["/a/", "/a/"])); - assert!(unranked_route_collisions(&["/a/b/", "/a/"])); - assert!(unranked_route_collisions(&["/", "/a/"])); - assert!(unranked_route_collisions(&["/a/", "/a/"])); - assert!(unranked_route_collisions(&["/a/b/", "/a/"])); - assert!(unranked_route_collisions(&["/a/b/c/d", "/a/"])); - assert!(unranked_route_collisions(&["/<_>", "/<_>"])); - assert!(unranked_route_collisions(&["/a/<_>", "/a/b"])); - assert!(unranked_route_collisions(&["/a/<_>", "/a/"])); - 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(&["/", "/hello"])); + assert!(rankless_route_collisions(&["/", "/"])); + assert!(rankless_route_collisions(&["/hello/bob", "/hello/"])); + assert!(rankless_route_collisions(&["/a/b//d", "///c/d"])); + + assert!(rankless_route_collisions(&["/a/b", "/"])); + assert!(rankless_route_collisions(&["/a/b/c", "/a/"])); + assert!(rankless_route_collisions(&["//b", "/a/"])); + assert!(rankless_route_collisions(&["/a/", "/a/"])); + assert!(rankless_route_collisions(&["/a/b/", "/a/"])); + assert!(rankless_route_collisions(&["/", "/a/"])); + assert!(rankless_route_collisions(&["/a/", "/a/"])); + assert!(rankless_route_collisions(&["/a/b/", "/a/"])); + assert!(rankless_route_collisions(&["/a/b/c/d", "/a/"])); + assert!(rankless_route_collisions(&["/", "/"])); + assert!(rankless_route_collisions(&["/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/", "/a/d"])); + assert!(rankless_route_collisions(&["/a/<_..>", "/<_>"])); + assert!(rankless_route_collisions(&["/a/<_..>", "/a"])); + assert!(rankless_route_collisions(&["/", "/a/"])); + + assert!(rankless_route_collisions(&["/<_>", "/<_>"])); + assert!(rankless_route_collisions(&["/a/<_>", "/a/b"])); + assert!(rankless_route_collisions(&["/a/<_>", "/a/"])); + assert!(rankless_route_collisions(&["/<_..>", "/a/b"])); + assert!(rankless_route_collisions(&["/<_..>", "/<_>"])); + assert!(rankless_route_collisions(&["/<_>/b", "/a/b"])); + assert!(rankless_route_collisions(&["/", "/"])); } #[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(&["/", "/hello//"])); - assert!(unranked_route_collisions(&["/", "/hello///"])); - assert!(unranked_route_collisions(&["/hello///bob", "/hello/"])); - assert!(unranked_route_collisions(&["///", "/a//"])); - assert!(unranked_route_collisions(&["/a///", "/a/"])); - assert!(unranked_route_collisions(&["/a///", "/a/b//c//d/"])); - assert!(unranked_route_collisions(&["/a//", "/a/bd/e/"])); - assert!(unranked_route_collisions(&["/a///", "/a/b//c//d/e/"])); - assert!(unranked_route_collisions(&["/a////", "/a/b//c//d/e/"])); - assert!(unranked_route_collisions(&["///<_>", "/<_>"])); - assert!(unranked_route_collisions(&["/a/<_>", "///a//b"])); - assert!(unranked_route_collisions(&["//a///<_>", "/a//"])); - 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(&["/", "/hello//"])); + assert!(rankless_route_collisions(&["/", "/hello///"])); + assert!(rankless_route_collisions(&["/hello///bob", "/hello/"])); + assert!(rankless_route_collisions(&["///", "/a//"])); + assert!(rankless_route_collisions(&["/a///", "/a/"])); + assert!(rankless_route_collisions(&["/a///", "/a/b//c//d/"])); + assert!(rankless_route_collisions(&["/a//", "/a/bd/e/"])); + assert!(rankless_route_collisions(&["//", "/a/bd/e/"])); + assert!(rankless_route_collisions(&["//", "/"])); + assert!(rankless_route_collisions(&["/a///", "/a/b//c//d/e/"])); + assert!(rankless_route_collisions(&["/a////", "/a/b//c//d/e/"])); + assert!(rankless_route_collisions(&["///<_>", "/<_>"])); + assert!(rankless_route_collisions(&["/a/<_>", "///a//b"])); + assert!(rankless_route_collisions(&["//a///<_>", "/a//"])); + assert!(rankless_route_collisions(&["//<_..>", "/a/b"])); + assert!(rankless_route_collisions(&["//<_..>", "/<_>"])); + assert!(rankless_route_collisions(&["////", "/a/"])); + assert!(rankless_route_collisions(&["////", "/a/"])); } #[test] fn test_collisions_query() { - // Query shouldn't affect things when unranked. - assert!(unranked_route_collisions(&["/hello?", "/hello"])); - assert!(unranked_route_collisions(&["/?foo=bar", "/hello?foo=bar&cat=fat"])); - assert!(unranked_route_collisions(&["/?foo=bar", "/hello?foo=bar&cat=fat"])); - assert!(unranked_route_collisions(&["/", "/?"])); - assert!(unranked_route_collisions(&["/hello/bob?a=b", "/hello/?d=e"])); - assert!(unranked_route_collisions(&["/?a=b", "/foo?d=e"])); - assert!(unranked_route_collisions(&["/?a=b&", "/?d=e&"])); - assert!(unranked_route_collisions(&["/?a=b&", "/?d=e"])); + // Query shouldn't affect things when rankless. + assert!(rankless_route_collisions(&["/hello?", "/hello"])); + assert!(rankless_route_collisions(&["/?foo=bar", "/hello?foo=bar&cat=fat"])); + assert!(rankless_route_collisions(&["/?foo=bar", "/hello?foo=bar&cat=fat"])); + assert!(rankless_route_collisions(&["/", "/?"])); + assert!(rankless_route_collisions(&["/hello/bob?a=b", "/hello/?d=e"])); + assert!(rankless_route_collisions(&["/?a=b", "/foo?d=e"])); + assert!(rankless_route_collisions(&["/?a=b&", "/?d=e&"])); + assert!(rankless_route_collisions(&["/?a=b&", "/?d=e"])); } #[test] fn test_no_collisions() { - assert!(!unranked_route_collisions(&["/", "/a/"])); - assert!(!unranked_route_collisions(&["/a/b", "/a/b/c"])); - assert!(!unranked_route_collisions(&["/a/b/c/d", "/a/b/c//e"])); - assert!(!unranked_route_collisions(&["/a/d/", "/a/b/c"])); - assert!(!unranked_route_collisions(&["/a/d/", "/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//e"])); + assert!(!rankless_route_collisions(&["/a/d/", "/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/"])); assert!(!default_rank_route_collisions(&["/a/b/c/d", "///c/d"])); assert!(!default_rank_route_collisions(&["/hi", "/"])); - assert!(!default_rank_route_collisions(&["/hi", "/"])); + assert!(!default_rank_route_collisions(&["/a", "/a/"])); + assert!(!default_rank_route_collisions(&["/", "/"])); assert!(!default_rank_route_collisions(&["/a/b", "/a/b/"])); assert!(!default_rank_route_collisions(&["/<_>", "/static"])); + assert!(!default_rank_route_collisions(&["/<_..>", "/static"])); + assert!(!default_rank_route_collisions(&["/", "/"])); 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/"])); + } + #[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(&["/"]); + 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/"]); + 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/"]); + 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", "/"], "/hello", "/hello"); - assert_ranked_routes!(&["/", "/hello"], "/hello", "/hello"); - assert_ranked_routes!(&["/", "/hi", "/"], "/hi", "/hi"); - assert_ranked_routes!(&["//b", "/hi/c"], "/hi/c", "/hi/c"); - assert_ranked_routes!(&["//", "/hi/a"], "/hi/c", "//"); - assert_ranked_routes!(&["/hi/a", "/hi/"], "/hi/c", "/hi/"); - assert_ranked_routes!(&["/a", "/a?"], "/a?b=c", "/a?"); - assert_ranked_routes!(&["/a", "/a?"], "/a", "/a?"); - assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/a", "/a?"); - assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/b", "/?"); - assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/b?v=1", "/?"); - assert_ranked_routes!(&["/a", "/", "/a?", "/?"], "/a?b=c", "/a?"); - assert_ranked_routes!(&["/a", "/a?b"], "/a?b", "/a?b"); - assert_ranked_routes!(&["/", "/a?b"], "/a?b", "/a?b"); - assert_ranked_routes!(&["/a", "/?b"], "/a?b", "/a"); - assert_ranked_routes!(&["/a?&b", "/a?"], "/a", "/a?"); - assert_ranked_routes!(&["/a?&b", "/a?"], "/a?b", "/a?&b"); - assert_ranked_routes!(&["/a?&b", "/a?"], "/a?c", "/a?"); + assert_ranked_match!(&["/hello", "/"], "/hello" => "/hello"); + assert_ranked_match!(&["/", "/hello"], "/hello" => "/hello"); + assert_ranked_match!(&["/", "/hi", "/hi/"], "/hi" => "/hi"); + assert_ranked_match!(&["//b", "/hi/c"], "/hi/c" => "/hi/c"); + assert_ranked_match!(&["//", "/hi/a"], "/hi/c" => "//"); + assert_ranked_match!(&["/hi/a", "/hi/"], "/hi/c" => "/hi/"); + assert_ranked_match!(&["/a", "/a?"], "/a?b=c" => "/a?"); + assert_ranked_match!(&["/a", "/a?"], "/a" => "/a?"); + assert_ranked_match!(&["/a", "/", "/a?", "/?"], "/a" => "/a?"); + assert_ranked_match!(&["/a", "/", "/a?", "/?"], "/b" => "/?"); + assert_ranked_match!(&["/a", "/", "/a?", "/?"], "/b?v=1" => "/?"); + assert_ranked_match!(&["/a", "/", "/a?", "/?"], "/a?b=c" => "/a?"); + assert_ranked_match!(&["/a", "/a?b"], "/a?b" => "/a?b"); + assert_ranked_match!(&["/", "/a?b"], "/a?b" => "/a?b"); + assert_ranked_match!(&["/a", "/?b"], "/a?b" => "/a"); + assert_ranked_match!(&["/a?&b", "/a?"], "/a" => "/a?"); + assert_ranked_match!(&["/a?&b", "/a?"], "/a?b" => "/a?&b"); + assert_ranked_match!(&["/a?&b", "/a?"], "/a?c" => "/a?"); + assert_ranked_match!(&["/", "/"], "/" => "/"); + assert_ranked_match!(&["/", "/"], "/hi" => "/"); + assert_ranked_match!(&["/hi", "/"], "/hi" => "/hi"); } fn ranked_collisions(routes: &[(isize, &'static str)]) -> bool { @@ -444,6 +481,12 @@ mod test { with: [(1, "/a/"), (2, "/a/b/")], expect: (1, "/a/"), (2, "/a/b/") ); + + assert_ranked_routing!( + to: "/hi", + with: [(1, "/hi/"), (0, "/hi/")], + expect: (1, "/hi/") + ); } macro_rules! assert_default_ranked_routing { diff --git a/core/lib/src/router/route.rs b/core/lib/src/router/route.rs index 7cacb858..886c50ce 100644 --- a/core/lib/src/router/route.rs +++ b/core/lib/src/router/route.rs @@ -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)) diff --git a/examples/static_files/src/main.rs b/examples/static_files/src/main.rs index 5bf135fe..75ca2a6d 100644 --- a/examples/static_files/src/main.rs +++ b/examples/static_files/src/main.rs @@ -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::open("static/rocket-icon.jpg").await.ok() + #[rocket::get("/second/")] + pub async fn second(path: PathBuf) -> Option { + 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"))) } diff --git a/examples/static_files/src/tests.rs b/examples/static_files/src/tests.rs index 939f6735..b437f77d 100644 --- a/examples/static_files/src/tests.rs +++ b/examples/static_files/src/tests.rs @@ -6,6 +6,7 @@ use rocket::http::Status; use super::rocket; +#[track_caller] fn test_query_file (path: &str, file: T, status: Status) where T: Into> { @@ -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] diff --git a/examples/static_files/static/hidden/index.html b/examples/static_files/static/hidden/index.html new file mode 100644 index 00000000..67db2721 --- /dev/null +++ b/examples/static_files/static/hidden/index.html @@ -0,0 +1,11 @@ + + + + + + Hmm... + + + 👀 + + diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 26d592b4..88880ee4 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -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/")] 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("/")] @@ -150,13 +149,15 @@ async fn files(file: PathBuf) -> Option { } ``` +[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