Make trailing slashes significant during routing.

This commit modifies request routing in a backwards incompatible manner.
The change is summarized as: trailing slashes are now significant and
never transparently disregarded. This has the following implications,
all representing behavior that differs from that before this change:

  * Route URIs with trailing slashes (`/foo/`, `/<a>/`) are legal.
  * A request `/foo/` is routed to route `/foo/` but not `/foo`.
  * Similarly, a request `/bar/` is routed to `/<a>/` but not `/<a>`.
  * A request `/bar/foo` is not routed to `/<a>/<b>/<c..>`.

A new `AdHoc::uri_normalizer()` fairing was added that recovers the
previous behavior.

In addition to the above, the `Options::NormalizeDirs` `FileServer`
option is now enabled by default to remain consistent with the above
changes and reduce breaking changes at the `FileServer` level.
This commit is contained in:
Sergio Benitez 2023-04-10 10:48:30 -07:00
parent 908a918e8b
commit 51ed332127
13 changed files with 179 additions and 113 deletions

View File

@ -77,11 +77,9 @@ impl FromMeta for RouteUri {
.help("expected URI in origin form: \"/path/<param>\"") .help("expected URI in origin form: \"/path/<param>\"")
})?; })?;
if !origin.is_normalized_nontrailing() { if !origin.is_normalized() {
let normalized = origin.clone().into_normalized_nontrailing(); let normalized = origin.clone().into_normalized();
let span = origin.path().find("//") let span = origin.path().find("//")
.or_else(|| origin.has_trailing_slash()
.then_some(origin.path().len() - 1))
.or_else(|| origin.query() .or_else(|| origin.query()
.and_then(|q| q.find("&&")) .and_then(|q| q.find("&&"))
.map(|i| origin.path().len() + 1 + i)) .map(|i| origin.path().len() + 1 + i))

View File

@ -309,7 +309,7 @@ impl Parse for InternalUriParams {
// Validation should always succeed since this macro can only be called // Validation should always succeed since this macro can only be called
// if the route attribute succeeded, implying a valid route URI. // if the route attribute succeeded, implying a valid route URI.
let route_uri = Origin::parse_route(&route_uri_str) let route_uri = Origin::parse_route(&route_uri_str)
.map(|o| o.into_normalized_nontrailing().into_owned()) .map(|o| o.into_normalized().into_owned())
.map_err(|_| input.error("internal error: invalid route URI"))?; .map_err(|_| input.error("internal error: invalid route URI"))?;
let content; let content;

View File

@ -338,8 +338,9 @@ fn test_inclusive_segments() {
assert_eq!(get("//a/"), "empty+a/"); assert_eq!(get("//a/"), "empty+a/");
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//c/d"), "empty+a/c/d");
assert_eq!(get("//a/b"), "empty+a/b");
assert_eq!(get("//a/b"), "nonempty+"); 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"), "nonempty+c");
assert_eq!(get("//a//b////c"), "nonempty+c"); assert_eq!(get("//a//b////c"), "nonempty+c");

View File

@ -52,7 +52,7 @@ error: route URIs cannot contain empty segments
23 | #[get("/a/b//")] 23 | #[get("/a/b//")]
| ^^ | ^^
| |
= note: expected "/a/b", found "/a/b//" = note: expected "/a/b/", found "/a/b//"
error: unused parameter error: unused parameter
--> tests/ui-fail-nightly/route-path-bad-syntax.rs:42:10 --> tests/ui-fail-nightly/route-path-bad-syntax.rs:42:10
@ -240,27 +240,3 @@ warning: `segment` starts with `<` but does not end with `>`
| ^^^^^^^^ | ^^^^^^^^
| |
= help: perhaps you meant the dynamic parameter `<name>`? = help: perhaps you meant the dynamic parameter `<name>`?
error: route URIs cannot contain empty segments
--> tests/ui-fail-nightly/route-path-bad-syntax.rs:107:10
|
107 | #[get("/a/")]
| ^^
|
= note: expected "/a", found "/a/"
error: route URIs cannot contain empty segments
--> tests/ui-fail-nightly/route-path-bad-syntax.rs:110:12
|
110 | #[get("/a/b/")]
| ^^
|
= note: expected "/a/b", found "/a/b/"
error: route URIs cannot contain empty segments
--> tests/ui-fail-nightly/route-path-bad-syntax.rs:113:14
|
113 | #[get("/a/b/c/")]
| ^^
|
= note: expected "/a/b/c", found "/a/b/c/"

View File

@ -41,7 +41,7 @@ error: route URIs cannot contain empty segments
| ^^^^^^^^^ | ^^^^^^^^^
error: route URIs cannot contain empty segments error: route URIs cannot contain empty segments
--- note: expected "/a/b", found "/a/b//" --- note: expected "/a/b/", found "/a/b//"
--> tests/ui-fail-stable/route-path-bad-syntax.rs:23:7 --> tests/ui-fail-stable/route-path-bad-syntax.rs:23:7
| |
23 | #[get("/a/b//")] 23 | #[get("/a/b//")]
@ -180,24 +180,3 @@ error: parameters cannot be empty
| |
93 | #[get("/<>")] 93 | #[get("/<>")]
| ^^^^^ | ^^^^^
error: route URIs cannot contain empty segments
--- note: expected "/a", found "/a/"
--> tests/ui-fail-stable/route-path-bad-syntax.rs:107:7
|
107 | #[get("/a/")]
| ^^^^^
error: route URIs cannot contain empty segments
--- note: expected "/a/b", found "/a/b/"
--> tests/ui-fail-stable/route-path-bad-syntax.rs:110:7
|
110 | #[get("/a/b/")]
| ^^^^^^^
error: route URIs cannot contain empty segments
--- note: expected "/a/b/c", found "/a/b/c/"
--> tests/ui-fail-stable/route-path-bad-syntax.rs:113:7
|
113 | #[get("/a/b/c/")]
| ^^^^^^^^^

View File

@ -242,6 +242,84 @@ impl AdHoc {
Ok(rocket.manage(app_config)) Ok(rocket.manage(app_config))
}) })
} }
/// Constructs an `AdHoc` request fairing that strips trailing slashes from
/// all URIs in all incoming requests.
///
/// The fairing returned by this method is intended largely for applications
/// that migrated from Rocket v0.4 to Rocket v0.5. In Rocket v0.4, requests
/// with a trailing slash in the URI were treated as if the trailing slash
/// were not present. For example, the request URI `/foo/` would match the
/// route `/<a>` with `a = foo`. If the application depended on this
/// behavior, say by using URIs with previously innocuous trailing slashes
/// in an external application, requests will not be routed as expected.
///
/// This fairing resolves this issue by stripping a trailing slash, if any,
/// in all incoming URIs. When it does so, it logs a warning. It is
/// recommended to use this fairing as a stop-gap measure instead of a
/// permanent resolution, if possible.
//
/// # Example
///
/// With the fairing attached, request URIs have a trailing slash stripped:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::local::blocking::Client;
/// use rocket::fairing::AdHoc;
///
/// #[get("/<param>")]
/// fn foo(param: &str) -> &str {
/// param
/// }
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build()
/// .mount("/", routes![foo])
/// .attach(AdHoc::uri_normalizer())
/// }
///
/// # let client = Client::debug(rocket()).unwrap();
/// let response = client.get("/bar/").dispatch();
/// assert_eq!(response.into_string().unwrap(), "bar");
/// ```
///
/// Without it, request URIs are unchanged and routed normally:
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::local::blocking::Client;
/// use rocket::fairing::AdHoc;
///
/// #[get("/<param>")]
/// fn foo(param: &str) -> &str {
/// param
/// }
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/", routes![foo])
/// }
///
/// # let client = Client::debug(rocket()).unwrap();
/// let response = client.get("/bar/").dispatch();
/// assert!(response.status().class().is_client_error());
///
/// let response = client.get("/bar").dispatch();
/// assert_eq!(response.into_string().unwrap(), "bar");
/// ```
#[deprecated(since = "0.6", note = "routing from Rocket v0.5 is now standard")]
pub fn uri_normalizer() -> AdHoc {
AdHoc::on_request("URI Normalizer", |req, _| Box::pin(async move {
if !req.uri().is_normalized_nontrailing() {
let normal = req.uri().clone().into_normalized_nontrailing();
warn!("Incoming request URI was normalized for compatibility.");
info_!("{} -> {}", req.uri(), normal);
req.set_uri(normal);
}
}))
}
} }
#[crate::async_trait] #[crate::async_trait]

View File

@ -24,9 +24,8 @@ use crate::fs::NamedFile;
/// ///
/// # Example /// # Example
/// ///
/// To serve files from the `/static` directory on the local file system at the /// Serve files from the `/static` directory on the local file system at the
/// `/public` path, allowing `index.html` files to be used to respond to /// `/public` path with the [default options](#impl-Default):
/// requests for a directory (the default), you might write the following:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
@ -38,18 +37,18 @@ use crate::fs::NamedFile;
/// } /// }
/// ``` /// ```
/// ///
/// With this, requests for files at `/public/<path..>` will be handled by /// Requests for files at `/public/<path..>` will be handled by returning the
/// returning the contents of `/static/<path..>`. Requests for _directories_ at /// contents of `/static/<path..>`. Requests for _directories_ at
/// `/public/<directory>` will be handled by returning the contents of /// `/public/<directory>` will be handled by returning the contents of
/// `/static/<directory>/index.html`. /// `/static/<directory>/index.html`.
/// ///
/// ## Relative Paths /// ## Relative Paths
/// ///
/// In the example above, `/static` is an absolute path. If your static files /// In the example above, `/static` is an absolute path. If your static files
/// are stored relative to your crate and your project is managed by Rocket, use /// are stored relative to your crate and your project is managed by Cargo, use
/// the [`relative!`] macro to obtain a path that is relative to your /// the [`relative!`] macro to obtain a path that is relative to your crate's
/// crate's root. For example, to serve files in the `static` subdirectory of /// root. For example, to serve files in the `static` subdirectory of your crate
/// your crate at `/`, you might write: /// at `/`, you might write:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
@ -263,8 +262,8 @@ pub struct Options(u8);
impl Options { impl Options {
/// All options disabled. /// All options disabled.
/// ///
/// This is different than [`Options::default()`](#impl-Default), which /// Note that this is different than [`Options::default()`](#impl-Default),
/// enables `Options::Index`. /// which enables options.
pub const None: Options = Options(0); pub const None: Options = Options(0);
/// Respond to requests for a directory with the `index.html` file in that /// Respond to requests for a directory with the `index.html` file in that
@ -289,14 +288,14 @@ impl Options {
/// Normalizes directory requests by redirecting requests to directory paths /// Normalizes directory requests by redirecting requests to directory paths
/// without a trailing slash to ones with a trailing slash. /// without a trailing slash to ones with a trailing slash.
/// ///
/// **Enabled by default.**
///
/// When enabled, the [`FileServer`] handler will respond to requests for a /// When enabled, the [`FileServer`] handler will respond to requests for a
/// directory without a trailing `/` with a permanent redirect (308) to the /// directory without a trailing `/` with a permanent redirect (308) to the
/// same path with a trailing `/`. This ensures relative URLs within any /// same path with a trailing `/`. This ensures relative URLs within any
/// document served from that directory will be interpreted relative to that /// document served from that directory will be interpreted relative to that
/// directory rather than its parent. /// directory rather than its parent.
/// ///
/// **Disabled by default.**
///
/// # Example /// # Example
/// ///
/// Given the following directory structure... /// Given the following directory structure...
@ -308,14 +307,21 @@ impl Options {
/// └── index.html /// └── index.html
/// ``` /// ```
/// ///
/// ...with `FileServer::from("static")`, both requests to `/foo` and /// And the following server:
/// `/foo/` will serve `static/foo/index.html`. If `index.html` references ///
/// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg` /// ```text
/// (`static/cat.jpeg`) when the request for `/foo` was handled and /// rocket.mount("/", FileServer::from("static"))
/// `/foo/cat.jpeg` (`static/foo/cat.jpeg`) if `/foo/` was handled. As a /// ```
/// result, the request in the former case will fail. To avoid this, ///
/// `NormalizeDirs` will redirect requests to `/foo` to `/foo/` if the file /// ...requests to `example.com/foo` will be redirected to
/// that would be served is a directory. /// `example.com/foo/`. If `index.html` references `cat.jpeg` as a relative
/// URL, the browser will resolve the URL to `example.com/foo/cat.jpeg`,
/// which in-turn Rocket will match to `/static/foo/cat.jpg`.
///
/// Without this option, requests to `example.com/foo` would not be
/// redirected. `index.html` would be rendered, and the relative link to
/// `cat.jpeg` would be resolved by the browser as `example.com/cat.jpeg`.
/// Rocket would thus try to find `/static/cat.jpeg`, which does not exist.
pub const NormalizeDirs: Options = Options(1 << 2); pub const NormalizeDirs: Options = Options(1 << 2);
/// Allow serving a file instead of a directory. /// Allow serving a file instead of a directory.
@ -380,10 +386,10 @@ impl Options {
} }
} }
/// The default set of options: `Options::Index`. /// The default set of options: `Options::Index | Options:NormalizeDirs`.
impl Default for Options { impl Default for Options {
fn default() -> Self { fn default() -> Self {
Options::Index Options::Index | Options::NormalizeDirs
} }
} }

View File

@ -57,10 +57,7 @@ impl Collide for RouteUri<'_> {
} }
} }
// Check for `/a/<a..>` vs. `/a`, which should collide. a_segments.len() == b_segments.len()
a_segments.get(b_segments.len()).map_or(false, |s| s.dynamic_trail)
|| b_segments.get(a_segments.len()).map_or(false, |s| s.dynamic_trail)
|| a_segments.len() == b_segments.len()
} }
} }
@ -192,7 +189,23 @@ mod tests {
assert_no_collision!("/a", "/aaa"); assert_no_collision!("/a", "/aaa");
assert_no_collision!("/", "/a"); assert_no_collision!("/", "/a");
assert_no_collision!("/foo", "/foo/");
assert_no_collision!("/foo/bar", "/foo/");
assert_no_collision!("/foo/bar", "/foo/bar/");
assert_no_collision!("/foo/<a>", "/foo/<a>/");
assert_no_collision!("/foo/<a>", "/<b>/<a>/");
assert_no_collision!("/<b>/<a>", "/<b>/<a>/");
assert_no_collision!("/a/", "/<a>/<b>/<c..>");
assert_no_collision!("/a", "/a/<a..>");
assert_no_collision!("/<a>", "/a/<a..>");
assert_no_collision!("/a/b", "/<a>/<b>/<c..>");
assert_no_collision!("/a/<b>", "/<a>/<b>/<c..>");
assert_no_collision!("/<a>/b", "/<a>/<b>/<c..>");
assert_no_collision!("/hi/<a..>", "/hi");
assert_no_collision!(ranked "/<a>", "/"); assert_no_collision!(ranked "/<a>", "/");
assert_no_collision!(ranked "/a/", "/<a>/");
assert_no_collision!(ranked "/hello/<a>", "/hello/"); assert_no_collision!(ranked "/hello/<a>", "/hello/");
assert_no_collision!(ranked "/", "/?a"); assert_no_collision!(ranked "/", "/?a");
assert_no_collision!(ranked "/", "/?<a>"); assert_no_collision!(ranked "/", "/?<a>");
@ -227,10 +240,9 @@ mod tests {
assert_collision!("/<a..>", "/foo"); assert_collision!("/<a..>", "/foo");
assert_collision!("/", "/<a..>"); assert_collision!("/", "/<a..>");
assert_collision!("/a", "/a/<a..>");
assert_collision!("/a/", "/a/<a..>"); assert_collision!("/a/", "/a/<a..>");
assert_collision!("/<a>/", "/a/<a..>"); assert_collision!("/<a>/", "/a/<a..>");
assert_collision!("/<a>", "/a/<a..>"); assert_collision!("/<a>/bar/", "/a/<a..>");
assert_collision!("/<a>", "/b"); assert_collision!("/<a>", "/b");
assert_collision!("/hello/<name>", "/hello/bob"); assert_collision!("/hello/<name>", "/hello/bob");
@ -244,7 +256,6 @@ mod tests {
assert_collision!("/", "/<_..>"); assert_collision!("/", "/<_..>");
assert_collision!("/a/b/<a..>", "/a/<b..>"); assert_collision!("/a/b/<a..>", "/a/<b..>");
assert_collision!("/a/b/<a..>", "/a/<b>/<b..>"); assert_collision!("/a/b/<a..>", "/a/<b>/<b..>");
assert_collision!("/hi/<a..>", "/hi");
assert_collision!("/hi/<a..>", "/hi/"); assert_collision!("/hi/<a..>", "/hi/");
assert_collision!("/<a..>", "//////"); assert_collision!("/<a..>", "//////");

View File

@ -43,20 +43,14 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool {
let route_segments = &route.uri.metadata.uri_segments; let route_segments = &route.uri.metadata.uri_segments;
let req_segments = req.uri().path().segments(); let req_segments = req.uri().path().segments();
// requests with longer paths only match if we have dynamic trail (<a..>). // A route can never have more segments than a request. Recall that a
if req_segments.num() > route_segments.len() { // trailing slash is considering a segment, albeit empty.
if !route.uri.metadata.dynamic_trail { if route_segments.len() > req_segments.num() {
return false; return false;
}
} }
// The last route segment can be trailing (`/<..>`), which is allowed to be // requests with longer paths only match if we have dynamic trail (<a..>).
// empty in the request. That is, we want to match `GET /a` to `/a/<b..>`. if req_segments.num() > route_segments.len() {
if route_segments.len() > req_segments.num() {
if route_segments.len() != req_segments.num() + 1 {
return false;
}
if !route.uri.metadata.dynamic_trail { if !route.uri.metadata.dynamic_trail {
return false; return false;
} }
@ -151,7 +145,6 @@ mod tests {
assert!(req_matches_route("/a/b?c", "/a/b?<c>")); assert!(req_matches_route("/a/b?c", "/a/b?<c>"));
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c>")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c>"));
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c..>")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c..>"));
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>"));
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&<c..>")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&<c..>"));
@ -169,11 +162,19 @@ mod tests {
assert!(!req_matches_route("/a/", "/a")); assert!(!req_matches_route("/a/", "/a"));
assert!(!req_matches_route("/a/b", "/a/b/")); assert!(!req_matches_route("/a/b", "/a/b/"));
assert!(!req_matches_route("/a", "/<a>/"));
assert!(!req_matches_route("/a/", "/<a>"));
assert!(!req_matches_route("/a/b", "/<a>/b/"));
assert!(!req_matches_route("/a/b", "/<a>/<b>/"));
assert!(!req_matches_route("/a/b/c", "/a/b?<c>")); assert!(!req_matches_route("/a/b/c", "/a/b?<c>"));
assert!(!req_matches_route("/a?b=c", "/a/b?<c>")); assert!(!req_matches_route("/a?b=c", "/a/b?<c>"));
assert!(!req_matches_route("/?b=c", "/a/b?<c>")); assert!(!req_matches_route("/?b=c", "/a/b?<c>"));
assert!(!req_matches_route("/?b=c", "/a?<c>")); assert!(!req_matches_route("/?b=c", "/a?<c>"));
assert!(!req_matches_route("/a/", "/<a>/<b>/<c..>"));
assert!(!req_matches_route("/a/b", "/<a>/<b>/<c..>"));
assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&<c..>")); assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&<c..>"));
assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&<c..>")); assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&<c..>"));
assert!(!req_matches_route("/a/b", "/a/b?c")); assert!(!req_matches_route("/a/b", "/a/b?c"));

View File

@ -170,13 +170,7 @@ mod test {
assert!(rankless_route_collisions(&["/", "/<a..>"])); assert!(rankless_route_collisions(&["/", "/<a..>"]));
assert!(rankless_route_collisions(&["/a/<_>", "/a/<a..>"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/<a..>"]));
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(&["/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(&["/<_>", "/<_>"]));
assert!(rankless_route_collisions(&["/a/<_>", "/a/b"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/b"]));
@ -235,6 +229,13 @@ mod test {
assert!(!rankless_route_collisions(&["/a/d/<b..>", "/a/b/c"])); assert!(!rankless_route_collisions(&["/a/d/<b..>", "/a/b/c"]));
assert!(!rankless_route_collisions(&["/a/<_>", "/a"])); assert!(!rankless_route_collisions(&["/a/<_>", "/a"]));
assert!(!rankless_route_collisions(&["/a/<_>", "/<_>"])); assert!(!rankless_route_collisions(&["/a/<_>", "/<_>"]));
assert!(!rankless_route_collisions(&["/a/<b>/<c..>", "/a/<c>"]));
assert!(!rankless_route_collisions(&["/<_>", "/a/<_..>"]));
assert!(!rankless_route_collisions(&["/foo", "/foo/<_..>"]));
assert!(!rankless_route_collisions(&["/a/<_..>", "/<_>"]));
assert!(!rankless_route_collisions(&["/a/<_..>", "/a"]));
assert!(!rankless_route_collisions(&["/<a>", "/a/<a..>"]));
assert!(!rankless_route_collisions(&["/a/d/<b..>", "/a/d"]));
} }
#[test] #[test]
@ -259,11 +260,11 @@ mod test {
assert!(!default_rank_route_collisions(&["/<a..>", "/hello"])); assert!(!default_rank_route_collisions(&["/<a..>", "/hello"]));
assert!(!default_rank_route_collisions(&["/<a>", "/a/<path..>"])); assert!(!default_rank_route_collisions(&["/<a>", "/a/<path..>"]));
assert!(!default_rank_route_collisions(&["/a/<b>/c", "/<d>/<c..>"])); assert!(!default_rank_route_collisions(&["/a/<b>/c", "/<d>/<c..>"]));
assert!(!default_rank_route_collisions(&["/a/<b>/<c..>", "/a/<c>"]));
} }
#[test] #[test]
fn test_collision_when_ranked() { fn test_collision_when_ranked() {
assert!(default_rank_route_collisions(&["/a/<b>/<c..>", "/a/<c>"]));
assert!(default_rank_route_collisions(&["/<a>/b", "/a/<b>"])); assert!(default_rank_route_collisions(&["/<a>/b", "/a/<b>"]));
} }
@ -329,7 +330,7 @@ mod test {
assert!(route(&router, Get, "/a/b/c/d/e/f").is_some()); assert!(route(&router, Get, "/a/b/c/d/e/f").is_some());
let router = router_with_routes(&["/foo/<a..>"]); let router = router_with_routes(&["/foo/<a..>"]);
assert!(route(&router, Get, "/foo").is_some()); assert!(route(&router, Get, "/foo").is_none());
assert!(route(&router, Get, "/foo/").is_some()); assert!(route(&router, Get, "/foo/").is_some());
assert!(route(&router, Get, "/foo///bar").is_some()); assert!(route(&router, Get, "/foo///bar").is_some());
} }
@ -497,9 +498,9 @@ mod test {
); );
assert_ranked_routing!( assert_ranked_routing!(
to: "/hi", to: "/hi/",
with: [(1, "/hi/<foo..>"), (0, "/hi/<foo>")], with: [(1, "/hi/<foo..>"), (0, "/hi/<foo>")],
expect: (1, "/hi/<foo..>") expect: (0, "/hi/<foo>"), (1, "/hi/<foo..>")
); );
} }

View File

@ -172,17 +172,21 @@ fn test_redirection() {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
// Root of route is also redirected. // Root of route is also redirected.
let response = client.get("/no_index").dispatch(); let response = client.get("/no_index/").dispatch();
assert_eq!(response.status(), Status::NotFound); assert_eq!(response.status(), Status::NotFound);
let response = client.get("/index").dispatch(); let response = client.get("/index/").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
let response = client.get("/redir").dispatch(); let response = client.get("/redir/inner").dispatch();
assert_eq!(response.status(), Status::PermanentRedirect); assert_eq!(response.status(), Status::PermanentRedirect);
assert_eq!(response.headers().get("Location").next(), Some("/redir/")); assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/"));
let response = client.get("/redir_index").dispatch(); let response = client.get("/redir/other").dispatch();
assert_eq!(response.status(), Status::PermanentRedirect); assert_eq!(response.status(), Status::PermanentRedirect);
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/")); assert_eq!(response.headers().get("Location").next(), Some("/redir/other/"));
let response = client.get("/redir_index/other").dispatch();
assert_eq!(response.status(), Status::PermanentRedirect);
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
} }

View File

@ -74,8 +74,19 @@ fn hello(lang: Option<Lang>, opt: Options<'_>) -> String {
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
use rocket::fairing::AdHoc;
rocket::build() rocket::build()
.mount("/", routes![hello]) .mount("/", routes![hello])
.mount("/hello", routes![world, mir]) .mount("/hello", routes![world, mir])
.mount("/wave", routes![wave]) .mount("/wave", routes![wave])
.attach(AdHoc::on_request("Compatibility Normalizer", |req, _| Box::pin(async move {
if !req.uri().is_normalized_nontrailing() {
let normal = req.uri().clone().into_normalized_nontrailing();
warn!("Incoming request URI was normalized for compatibility.");
info_!("{} -> {}", req.uri(), normal);
req.set_uri(normal);
}
})))
} }

View File

@ -40,7 +40,6 @@ fn test_index_html() {
#[test] #[test]
fn test_hidden_index_html() { 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("//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);
@ -65,6 +64,7 @@ fn test_icon_file() {
#[test] #[test]
fn test_invalid_path() { fn test_invalid_path() {
test_query_file("/hidden", None, Status::PermanentRedirect);
test_query_file("/thou_shalt_not_exist", None, Status::NotFound); test_query_file("/thou_shalt_not_exist", None, Status::NotFound);
test_query_file("/thou/shalt/not/exist", None, Status::NotFound); test_query_file("/thou/shalt/not/exist", None, Status::NotFound);
test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound); test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound);