mirror of https://github.com/rwf2/Rocket.git
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:
parent
08ae0d0b8c
commit
4d0042c395
|
@ -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) => {
|
||||
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),
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue