From f6e69919ede4872c1d987b4b932c44580190971c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 6 Aug 2021 22:42:31 +0100 Subject: [PATCH 1/8] update to router 0.5.0 beta (#2339) --- Cargo.toml | 4 +- actix-router/CHANGES.md | 120 ++ actix-router/Cargo.toml | 38 + actix-router/LICENSE-APACHE | 1 + actix-router/LICENSE-MIT | 1 + actix-router/benches/router.rs | 194 +++ actix-router/examples/flamegraph.rs | 169 +++ actix-router/src/de.rs | 723 +++++++++++ actix-router/src/lib.rs | 149 +++ actix-router/src/path.rs | 220 ++++ actix-router/src/resource.rs | 1803 +++++++++++++++++++++++++++ actix-router/src/router.rs | 281 +++++ actix-router/src/url.rs | 288 +++++ src/app.rs | 2 +- src/app_service.rs | 2 +- src/config.rs | 2 +- src/dev.rs | 21 +- src/request.rs | 8 +- src/resource.rs | 12 +- src/rmap.rs | 44 +- src/scope.rs | 2 +- src/service.rs | 20 +- src/types/path.rs | 8 +- src/web.rs | 8 +- 24 files changed, 4063 insertions(+), 57 deletions(-) create mode 100644 actix-router/CHANGES.md create mode 100644 actix-router/Cargo.toml create mode 120000 actix-router/LICENSE-APACHE create mode 120000 actix-router/LICENSE-MIT create mode 100644 actix-router/benches/router.rs create mode 100644 actix-router/examples/flamegraph.rs create mode 100644 actix-router/src/de.rs create mode 100644 actix-router/src/lib.rs create mode 100644 actix-router/src/path.rs create mode 100644 actix-router/src/resource.rs create mode 100644 actix-router/src/router.rs create mode 100644 actix-router/src/url.rs diff --git a/Cargo.toml b/Cargo.toml index 7556bd8d7..ff3321f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "actix-web-codegen", "actix-http-test", "actix-test", + "actix-router", ] # enable when MSRV is 1.51+ # resolver = "2" @@ -67,7 +68,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.1" -actix-router = "0.2.7" +actix-router = "0.5.0-beta.1" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" @@ -126,6 +127,7 @@ actix-files = { path = "actix-files" } actix-http = { path = "actix-http" } actix-http-test = { path = "actix-http-test" } actix-multipart = { path = "actix-multipart" } +actix-router = { path = "actix-router" } actix-test = { path = "actix-test" } actix-web = { path = "." } actix-web-actors = { path = "actix-web-actors" } diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md new file mode 100644 index 000000000..dea7cb76f --- /dev/null +++ b/actix-router/CHANGES.md @@ -0,0 +1,120 @@ +# Changes + +## Unreleased - 2021-xx-xx +* Introduce `ResourceDef::join`. [#380] +* Disallow prefix routes with tail segments. [#379] +* Enforce path separators on dynamic prefixes. [#378] +* Improve malformed path error message. [#384] + +[#378]: https://github.com/actix/actix-net/pull/378 +[#379]: https://github.com/actix/actix-net/pull/379 +[#380]: https://github.com/actix/actix-net/pull/380 +[#384]: https://github.com/actix/actix-net/pull/384 + + +## 0.5.0-beta.1 - 2021-07-20 +* Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366] +* Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373] +* Fix segment interpolation leaving `Path` in unintended state after matching. [#368] +* Fix `ResourceDef` `PartialEq` implementation. [#373] +* Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372] +* Implement `IntoPatterns` for `bytestring::ByteString`. [#372] +* Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370] +* Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371] +* `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373] +* Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371] +* Rename `ResourceDef::{is_prefix_match => find_match}`. [#373] +* Rename `ResourceDef::{match_path => capture_match_info}`. [#373] +* Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373] +* Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373] +* Rename `Router::{*_checked => *_fn}`. [#373] +* Return type of `ResourceDef::name` is now `Option<&str>`. [#373] +* Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373] + +[#368]: https://github.com/actix/actix-net/pull/368 +[#366]: https://github.com/actix/actix-net/pull/366 +[#368]: https://github.com/actix/actix-net/pull/368 +[#370]: https://github.com/actix/actix-net/pull/370 +[#371]: https://github.com/actix/actix-net/pull/371 +[#372]: https://github.com/actix/actix-net/pull/372 +[#373]: https://github.com/actix/actix-net/pull/373 + + +## 0.4.0 - 2021-06-06 +* When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357] +* Path tail patterns now match new lines (`\n`) in request URL. [#360] +* Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359] +* Methods `Path::{add, add_static}` now take `impl Into>`. [#345] + +[#345]: https://github.com/actix/actix-net/pull/345 +[#357]: https://github.com/actix/actix-net/pull/357 +[#359]: https://github.com/actix/actix-net/pull/359 +[#360]: https://github.com/actix/actix-net/pull/360 + + +## 0.3.0 - 2019-12-31 +* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 + + +## 0.2.7 - 2021-02-06 +* Add `Router::recognize_checked` [#247] + +[#247]: https://github.com/actix/actix-net/pull/247 + + +## 0.2.6 - 2021-01-09 +* Use `bytestring` version range compatible with Bytes v1.0. [#246] + +[#246]: https://github.com/actix/actix-net/pull/246 + + +## 0.2.5 - 2020-09-20 +* Fix `from_hex()` method + + +## 0.2.4 - 2019-12-31 +* Add `ResourceDef::resource_path_named()` path generation method + + +## 0.2.3 - 2019-12-25 +* Add impl `IntoPattern` for `&String` + + +## 0.2.2 - 2019-12-25 +* Use `IntoPattern` for `RouterBuilder::path()` + + +## 0.2.1 - 2019-12-25 +* Add `IntoPattern` trait +* Add multi-pattern resources + + +## 0.2.0 - 2019-12-07 +* Update http to 0.2 +* Update regex to 1.3 +* Use bytestring instead of string + + +## 0.1.5 - 2019-05-15 +* Remove debug prints + + +## 0.1.4 - 2019-05-15 +* Fix checked resource match + + +## 0.1.3 - 2019-04-22 +* Added support for `remainder match` (i.e "/path/{tail}*") + + +## 0.1.2 - 2019-04-07 +* Export `Quoter` type +* Allow to reset `Path` instance + + +## 0.1.1 - 2019-04-03 +* Get dynamic segment by name instead of iterator. + + +## 0.1.0 - 2019-03-09 +* Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml new file mode 100644 index 000000000..2a2ce1cc1 --- /dev/null +++ b/actix-router/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "actix-router" +version = "0.5.0-beta.1" +authors = [ + "Nikolay Kim ", + "Ali MJ Al-Nasrawy ", + "Rob Ede ", +] +description = "Resource path matching and router" +keywords = ["actix", "router", "routing"] +repository = "https://github.com/actix/actix-net.git" +license = "MIT OR Apache-2.0" +edition = "2018" + +[lib] +name = "actix_router" +path = "src/lib.rs" + +[features] +default = ["http"] + +[dependencies] +bytestring = ">=0.1.5, <2" +firestorm = "0.4" +http = { version = "0.2.3", optional = true } +log = "0.4" +regex = "1.5" +serde = "1" + +[dev-dependencies] +criterion = { version = "0.3", features = ["html_reports"] } +firestorm = { version = "0.4", features = ["enable_system_time"] } +http = "0.2.3" +serde = { version = "1", features = ["derive"] } + +[[bench]] +name = "router" +harness = false diff --git a/actix-router/LICENSE-APACHE b/actix-router/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-router/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-router/LICENSE-MIT b/actix-router/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-router/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs new file mode 100644 index 000000000..a428b9f13 --- /dev/null +++ b/actix-router/benches/router.rs @@ -0,0 +1,194 @@ +//! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +macro_rules! register { + (colon) => {{ + register!(finish => ":p1", ":p2", ":p3", ":p4") + }}; + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (regex) => {{ + register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + std::array::IntoIter::new(arr) + }}; +} + +fn call() -> impl Iterator { + let arr = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", + ]; + + std::array::IntoIter::new(arr) +} + +fn compare_routers(c: &mut Criterion) { + let mut group = c.benchmark_group("Compare Routers"); + + let mut actix = actix_router::Router::::build(); + for route in register!(brackets) { + actix.path(route, true); + } + let actix = actix.finish(); + group.bench_function("actix", |b| { + b.iter(|| { + for route in call() { + let mut path = actix_router::Path::new(route); + black_box(actix.recognize(&mut path).unwrap()); + } + }); + }); + + let regex_set = regex::RegexSet::new(register!(regex)).unwrap(); + group.bench_function("regex", |b| { + b.iter(|| { + for route in call() { + black_box(regex_set.matches(route)); + } + }); + }); + + group.finish(); +} + +criterion_group!(benches, compare_routers); +criterion_main!(benches); diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs new file mode 100644 index 000000000..798cc22d9 --- /dev/null +++ b/actix-router/examples/flamegraph.rs @@ -0,0 +1,169 @@ +macro_rules! register { + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + + arr.to_vec() + }}; +} + +static PATHS: [&str; 5] = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", +]; + +fn main() { + let mut router = actix_router::Router::::build(); + + for route in register!(brackets) { + router.path(route, true); + } + + let actix = router.finish(); + + if firestorm::enabled() { + firestorm::bench("target", || { + for &route in &PATHS { + let mut path = actix_router::Path::new(route); + actix.recognize(&mut path).unwrap(); + } + }) + .unwrap(); + } +} diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs new file mode 100644 index 000000000..775c48b8a --- /dev/null +++ b/actix-router/src/de.rs @@ -0,0 +1,723 @@ +use serde::de::{self, Deserializer, Error as DeError, Visitor}; +use serde::forward_to_deserialize_any; + +use crate::path::{Path, PathIter}; +use crate::ResourcePath; + +macro_rules! unsupported_type { + ($trait_fn:ident, $name:expr) => { + fn $trait_fn(self, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom(concat!( + "unsupported type: ", + $name + ))) + } + }; +} + +macro_rules! parse_single_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + let v = self.path[0].parse().map_err(|_| { + de::value::Error::custom(format!( + "can not parse {:?} to a {}", + &self.path[0], $tp + )) + })?; + visitor.$visit_fn(v) + } + } + }; +} + +pub struct PathDeserializer<'de, T: ResourcePath> { + path: &'de Path, +} + +impl<'de, T: ResourcePath + 'de> PathDeserializer<'de, T> { + pub fn new(path: &'de Path) -> Self { + PathDeserializer { path } + } +} + +impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> { + type Error = de::value::Error; + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(ParamsDeserializer { + params: self.path.iter(), + current: None, + }) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.is_empty() { + Err(de::value::Error::custom("expected at least one parameters")) + } else { + visitor.visit_enum(ValueEnum { + value: &self.path[0], + }) + } + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + visitor.visit_str(&self.path[0]) + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + + unsupported_type!(deserialize_any, "'any'"); + unsupported_type!(deserialize_bytes, "bytes"); + unsupported_type!(deserialize_option, "Option"); + unsupported_type!(deserialize_identifier, "identifier"); + unsupported_type!(deserialize_ignored_any, "ignored_any"); + + parse_single_value!(deserialize_bool, visit_bool, "bool"); + parse_single_value!(deserialize_i8, visit_i8, "i8"); + parse_single_value!(deserialize_i16, visit_i16, "i16"); + parse_single_value!(deserialize_i32, visit_i32, "i32"); + parse_single_value!(deserialize_i64, visit_i64, "i64"); + parse_single_value!(deserialize_u8, visit_u8, "u8"); + parse_single_value!(deserialize_u16, visit_u16, "u16"); + parse_single_value!(deserialize_u32, visit_u32, "u32"); + parse_single_value!(deserialize_u64, visit_u64, "u64"); + parse_single_value!(deserialize_f32, visit_f32, "f32"); + parse_single_value!(deserialize_f64, visit_f64, "f64"); + parse_single_value!(deserialize_string, visit_string, "String"); + parse_single_value!(deserialize_byte_buf, visit_string, "String"); + parse_single_value!(deserialize_char, visit_char, "char"); +} + +struct ParamsDeserializer<'de, T: ResourcePath> { + params: PathIter<'de, T>, + current: Option<(&'de str, &'de str)>, +} + +impl<'de, T: ResourcePath> de::MapAccess<'de> for ParamsDeserializer<'de, T> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + self.current = self.params.next().map(|ref item| (item.0, item.1)); + match self.current { + Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some((_, value)) = self.current.take() { + seed.deserialize(Value { value }) + } else { + Err(de::value::Error::custom("unexpected item")) + } + } +} + +struct Key<'de> { + key: &'de str, +} + +impl<'de> Deserializer<'de> for Key<'de> { + type Error = de::value::Error; + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_str(self.key) + } + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("Unexpected")) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes + byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum ignored_any + } +} + +macro_rules! parse_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let v = self.value.parse().map_err(|_| { + de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) + })?; + visitor.$visit_fn(v) + } + }; +} + +struct Value<'de> { + value: &'de str, +} + +impl<'de> Deserializer<'de> for Value<'de> { + type Error = de::value::Error; + + parse_value!(deserialize_bool, visit_bool, "bool"); + parse_value!(deserialize_i8, visit_i8, "i8"); + parse_value!(deserialize_i16, visit_i16, "i16"); + parse_value!(deserialize_i32, visit_i32, "i16"); + parse_value!(deserialize_i64, visit_i64, "i64"); + parse_value!(deserialize_u8, visit_u8, "u8"); + parse_value!(deserialize_u16, visit_u16, "u16"); + parse_value!(deserialize_u32, visit_u32, "u32"); + parse_value!(deserialize_u64, visit_u64, "u64"); + parse_value!(deserialize_f32, visit_f32, "f32"); + parse_value!(deserialize_f64, visit_f64, "f64"); + parse_value!(deserialize_string, visit_string, "String"); + parse_value!(deserialize_byte_buf, visit_string, "String"); + parse_value!(deserialize_char, visit_char, "char"); + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_bytes(self.value.as_bytes()) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(self.value) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_enum(ValueEnum { value: self.value }) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, _: usize, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple")) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: struct")) + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + _: usize, + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple struct")) + } + + unsupported_type!(deserialize_any, "any"); + unsupported_type!(deserialize_seq, "seq"); + unsupported_type!(deserialize_map, "map"); + unsupported_type!(deserialize_identifier, "identifier"); +} + +struct ParamsSeq<'de, T: ResourcePath> { + params: PathIter<'de, T>, +} + +impl<'de, T: ResourcePath> de::SeqAccess<'de> for ParamsSeq<'de, T> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: U) -> Result, Self::Error> + where + U: de::DeserializeSeed<'de>, + { + match self.params.next() { + Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), + None => Ok(None), + } + } +} + +struct ValueEnum<'de> { + value: &'de str, +} + +impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { + type Error = de::value::Error; + type Variant = UnitVariant; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: de::DeserializeSeed<'de>, + { + Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) + } +} + +struct UnitVariant; + +impl<'de> de::VariantAccess<'de> for UnitVariant { + type Error = de::value::Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + Ok(()) + } + + fn newtype_variant_seed(self, _seed: T) -> Result + where + T: de::DeserializeSeed<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn struct_variant( + self, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } +} + +#[cfg(test)] +mod tests { + use serde::{de, Deserialize}; + + use super::*; + use crate::path::Path; + use crate::router::Router; + + #[derive(Deserialize)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Id { + _id: String, + } + + #[derive(Debug, Deserialize)] + struct Test1(String, u32); + + #[derive(Debug, Deserialize)] + struct Test2 { + key: String, + value: u32, + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(rename_all = "lowercase")] + enum TestEnum { + Val1, + Val2, + } + + #[derive(Debug, Deserialize)] + struct Test3 { + val: TestEnum, + } + + #[test] + fn test_request_extract() { + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/user1/"); + assert!(router.recognize(&mut path).is_some()); + + let s: MyStruct = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + + let s: (String, String) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/32/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Test1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let s: Test2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, 32); + + let s: (String, u8) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res: Vec = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); + } + + #[test] + fn test_extract_path_single() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/32/"); + assert!(router.recognize(&mut path).is_some()); + let i: i8 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, 32); + } + + #[test] + fn test_extract_enum() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: TestEnum = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, TestEnum::Val1); + + let mut router = Router::<()>::build(); + router.path("/{val1}/{val2}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/val2/"); + assert!(router.recognize(&mut path).is_some()); + let i: (TestEnum, TestEnum) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, (TestEnum::Val1, TestEnum::Val2)); + } + + #[test] + fn test_extract_enum_value() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: Test3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i.val, TestEnum::Val1); + + let mut path = Path::new("/val3/"); + assert!(router.recognize(&mut path).is_some()); + let i: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(i.is_err()); + assert!(format!("{:?}", i).contains("unknown variant")); + } + + #[test] + fn test_extract_errors() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + + let s: Result<(String, String), de::value::Error> = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + } + + // #[test] + // fn test_extract_path_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // macro_rules! test_single_value { + // ($value:expr, $expected:expr) => {{ + // let req = TestRequest::with_uri($value).finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &PathConfig::default()).unwrap(), + // $expected + // ); + // }}; + // } + + // test_single_value!("/%25/", "%"); + // test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); + // test_single_value!("/%2B/", "+"); + // test_single_value!("/%252B/", "%2B"); + // test_single_value!("/%2F/", "/"); + // test_single_value!("/%252F/", "%2F"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffoo/", + // "http://localhost:80/foo" + // ); + // test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", + // "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" + // ); + + // let req = TestRequest::with_uri("/%25/7/?id=test").finish(); + + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + + // let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.key, "%"); + // assert_eq!(s.value, 7); + + // let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.0, "%"); + // assert_eq!(s.1, "7"); + // } + + // #[test] + // fn test_extract_path_no_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // let req = TestRequest::with_uri("/%25/").finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &&PathConfig::default().disable_decoding()) + // .unwrap(), + // "%25" + // ); + // } +} diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs new file mode 100644 index 000000000..463e59e42 --- /dev/null +++ b/actix-router/src/lib.rs @@ -0,0 +1,149 @@ +//! Resource path matching and router. + +#![deny(rust_2018_idioms, nonstandard_style)] +#![doc(html_logo_url = "https://actix.rs/img/logo.png")] +#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] + +mod de; +mod path; +mod resource; +mod router; + +pub use self::de::PathDeserializer; +pub use self::path::Path; +pub use self::resource::ResourceDef; +pub use self::router::{ResourceInfo, Router, RouterBuilder}; + +// TODO: this trait is necessary, document it +// see impl Resource for ServiceRequest +pub trait Resource { + fn resource_path(&mut self) -> &mut Path; +} + +pub trait ResourcePath { + fn path(&self) -> &str; +} + +impl ResourcePath for String { + fn path(&self) -> &str { + self.as_str() + } +} + +impl<'a> ResourcePath for &'a str { + fn path(&self) -> &str { + self + } +} + +impl ResourcePath for bytestring::ByteString { + fn path(&self) -> &str { + &*self + } +} + +/// One or many patterns. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Patterns { + Single(String), + List(Vec), +} + +impl Patterns { + pub fn is_empty(&self) -> bool { + match self { + Patterns::Single(_) => false, + Patterns::List(pats) => pats.is_empty(), + } + } +} + +/// Helper trait for type that could be converted to one or more path pattern. +pub trait IntoPatterns { + fn patterns(&self) -> Patterns; +} + +impl IntoPatterns for String { + fn patterns(&self) -> Patterns { + Patterns::Single(self.clone()) + } +} + +impl<'a> IntoPatterns for &'a String { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).clone()) + } +} + +impl<'a> IntoPatterns for &'a str { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).to_owned()) + } +} + +impl IntoPatterns for bytestring::ByteString { + fn patterns(&self) -> Patterns { + Patterns::Single(self.to_string()) + } +} + +impl IntoPatterns for Patterns { + fn patterns(&self) -> Patterns { + self.clone() + } +} + +impl> IntoPatterns for Vec { + fn patterns(&self) -> Patterns { + let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); + + match patterns.size_hint() { + (1, _) => Patterns::Single(patterns.next().unwrap()), + _ => Patterns::List(patterns.collect()), + } + } +} + +macro_rules! array_patterns_single (($tp:ty) => { + impl IntoPatterns for [$tp; 1] { + fn patterns(&self) -> Patterns { + Patterns::Single(self[0].to_owned()) + } + } +}); + +macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { + // for each array length specified in $num + $( + impl IntoPatterns for [$tp; $num] { + fn patterns(&self) -> Patterns { + Patterns::List(self.iter().map($str_fn).collect()) + } + } + )+ +}); + +array_patterns_single!(&str); +array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +array_patterns_single!(String); +array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +#[cfg(feature = "http")] +mod url; + +#[cfg(feature = "http")] +pub use self::url::{Quoter, Url}; + +#[cfg(feature = "http")] +mod http_impls { + use http::Uri; + + use super::ResourcePath; + + impl ResourcePath for Uri { + fn path(&self) -> &str { + self.path() + } + } +} diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs new file mode 100644 index 000000000..e29591f96 --- /dev/null +++ b/actix-router/src/path.rs @@ -0,0 +1,220 @@ +use std::borrow::Cow; +use std::ops::Index; + +use firestorm::profile_method; +use serde::de; + +use crate::{de::PathDeserializer, Resource, ResourcePath}; + +#[derive(Debug, Clone)] +pub(crate) enum PathItem { + Static(Cow<'static, str>), + Segment(u16, u16), +} + +impl Default for PathItem { + fn default() -> Self { + Self::Static(Cow::Borrowed("")) + } +} + +/// Resource path match information. +/// +/// If resource path contains variable patterns, `Path` stores them. +#[derive(Debug, Clone, Default)] +pub struct Path { + path: T, + pub(crate) skip: u16, + pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, +} + +impl Path { + pub fn new(path: T) -> Path { + Path { + path, + skip: 0, + segments: Vec::new(), + } + } + + /// Get reference to inner path instance. + #[inline] + pub fn get_ref(&self) -> &T { + &self.path + } + + /// Get mutable reference to inner path instance. + #[inline] + pub fn get_mut(&mut self) -> &mut T { + &mut self.path + } + + /// Path. + #[inline] + pub fn path(&self) -> &str { + profile_method!(path); + + let skip = self.skip as usize; + let path = self.path.path(); + if skip <= path.len() { + &path[skip..] + } else { + "" + } + } + + /// Set new path. + #[inline] + pub fn set(&mut self, path: T) { + self.skip = 0; + self.path = path; + self.segments.clear(); + } + + /// Reset state. + #[inline] + pub fn reset(&mut self) { + self.skip = 0; + self.segments.clear(); + } + + /// Skip first `n` chars in path. + #[inline] + pub fn skip(&mut self, n: u16) { + self.skip += n; + } + + pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { + profile_method!(add); + + match value { + PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), + PathItem::Segment(begin, end) => self.segments.push(( + name.into(), + PathItem::Segment(self.skip + begin, self.skip + end), + )), + } + } + + #[doc(hidden)] + pub fn add_static( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.segments + .push((name.into(), PathItem::Static(value.into()))); + } + + /// Check if there are any matched patterns. + #[inline] + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + /// Returns number of interpolated segments. + #[inline] + pub fn segment_count(&self) -> usize { + self.segments.len() + } + + /// Get matched parameter by name without type conversion + pub fn get(&self, name: &str) -> Option<&str> { + profile_method!(get); + + for (seg_name, val) in self.segments.iter() { + if name == seg_name { + return match val { + PathItem::Static(ref s) => Some(&s), + PathItem::Segment(s, e) => { + Some(&self.path.path()[(*s as usize)..(*e as usize)]) + } + }; + } + } + + None + } + + /// Get unprocessed part of the path + pub fn unprocessed(&self) -> &str { + &self.path.path()[(self.skip as usize)..] + } + + /// Get matched parameter by name. + /// + /// If keyed parameter is not available empty string is used as default value. + pub fn query(&self, key: &str) -> &str { + profile_method!(query); + + if let Some(s) = self.get(key) { + s + } else { + "" + } + } + + /// Return iterator to items in parameter container. + pub fn iter(&self) -> PathIter<'_, T> { + PathIter { + idx: 0, + params: self, + } + } + + /// Try to deserialize matching parameters to a specified type `U` + pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { + profile_method!(load); + de::Deserialize::deserialize(PathDeserializer::new(self)) + } +} + +#[derive(Debug)] +pub struct PathIter<'a, T> { + idx: usize, + params: &'a Path, +} + +impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { + type Item = (&'a str, &'a str); + + #[inline] + fn next(&mut self) -> Option<(&'a str, &'a str)> { + if self.idx < self.params.segment_count() { + let idx = self.idx; + let res = match self.params.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], + }; + self.idx += 1; + return Some((&self.params.segments[idx].0, res)); + } + None + } +} + +impl<'a, T: ResourcePath> Index<&'a str> for Path { + type Output = str; + + fn index(&self, name: &'a str) -> &str { + self.get(name) + .expect("Value for parameter is not available") + } +} + +impl Index for Path { + type Output = str; + + fn index(&self, idx: usize) -> &str { + match self.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], + } + } +} + +impl Resource for Path { + fn resource_path(&mut self) -> &mut Self { + self + } +} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs new file mode 100644 index 000000000..61ff587a5 --- /dev/null +++ b/actix-router/src/resource.rs @@ -0,0 +1,1803 @@ +use std::{ + borrow::{Borrow, Cow}, + collections::HashMap, + hash::{BuildHasher, Hash, Hasher}, + mem, +}; + +use firestorm::{profile_fn, profile_method, profile_section}; +use regex::{escape, Regex, RegexSet}; + +use crate::{ + path::{Path, PathItem}, + IntoPatterns, Patterns, Resource, ResourcePath, +}; + +const MAX_DYNAMIC_SEGMENTS: usize = 16; + +/// Regex flags to allow '.' in regex to match '\n' +/// +/// See the docs under: https://docs.rs/regex/1/regex/#grouping-and-flags +const REGEX_FLAGS: &str = "(?s-m)"; + +/// Describes the set of paths that match to a resource. +/// +/// `ResourceDef`s are effectively a way to transform the a custom resource pattern syntax into +/// suitable regular expressions from which to check matches with paths and capture portions of a +/// matched path into variables. Common cases are on a fast path that avoids going through the +/// regex engine. +/// +/// +/// # Static Resources +/// A static resource is the most basic type of definition. Pass a regular string to +/// [new][Self::new]. Conforming paths must match the string exactly. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new("/home"); +/// +/// assert!(resource.is_match("/home")); +/// +/// assert!(!resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// assert!(!resource.is_match("/search")); +/// ``` +/// +/// +/// # Dynamic Segments +/// Also known as "path parameters". Resources can define sections of a pattern that be extracted +/// from a conforming path, if it conforms to (one of) the resource pattern(s). +/// +/// The marker for a dynamic segment is curly braces wrapping an identifier. For example, +/// `/user/{id}` would match paths like `/user/123` or `/user/james` and be able to extract the user +/// IDs "123" and "james", respectively. +/// +/// However, this resource pattern (`/user/{id}`) would, not cover `/user/123/stars` (unless +/// constructed as a prefix; see next section) since the default pattern for segments matches all +/// characters until it finds a `/` character (or the end of the path). Custom segment patterns are +/// covered further down. +/// +/// Dynamic segments do not need to be delimited by `/` characters, they can be defined within a +/// path segment. For example, `/rust-is-{opinion}` can match the paths `/rust-is-cool` and +/// `/rust-is-hard`. +/// +/// For information on capturing segment values from paths or other custom resource types, +/// see [`capture_match_info`][Self::capture_match_info] +/// and [`capture_match_info_fn`][Self::capture_match_info_fn]. +/// +/// A resource definition can contain at most 16 dynamic segments. +/// +/// ## Examples +/// ``` +/// use actix_router::{Path, ResourceDef}; +/// +/// let resource = ResourceDef::prefix("/user/{id}"); +/// +/// assert!(resource.is_match("/user/123")); +/// assert!(!resource.is_match("/user")); +/// assert!(!resource.is_match("/user/")); +/// +/// let mut path = Path::new("/user/123"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("id").unwrap(), "123"); +/// ``` +/// +/// +/// # Prefix Resources +/// A prefix resource is defined as pattern that can match just the start of a path. +/// +/// This library chooses to restrict that definition slightly. In particular, when matching, the +/// prefix must be separated from the remaining part of the path by a `/` character, either at the +/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not +/// much of a limitation. +/// +/// Prefix resources can contain dynamic segments. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::prefix("/home"); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// +/// let resource = ResourceDef::prefix("/user/{id}/"); +/// assert!(resource.is_match("/user/123/")); +/// assert!(resource.is_match("/user/123/stars")); +/// ``` +/// +/// +/// # Custom Regex Segments +/// Dynamic segments can be customised to only match a specific regular expression. It can be +/// helpful to do this if resource definitions would otherwise conflict and cause one to +/// be inaccessible. +/// +/// The regex used when capturing segment values can be specified explicitly using this syntax: +/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID +/// is numeric. +/// +/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in +/// the earlier section, that segments capture a slice of the path up to the next `/` character. +/// +/// Custom regex segments can be used in static and prefix resource definition variants. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(r"/user/{id:\d+}"); +/// assert!(resource.is_match("/user/123")); +/// assert!(resource.is_match("/user/314159")); +/// assert!(!resource.is_match("/user/abc")); +/// ``` +/// +/// +/// # Tail Segments +/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those +/// up until a `/` character), there is a special pattern to match (and capture) the remaining +/// path portion. +/// +/// To do this, use the segment pattern: `{name}*`. Since a tail segment also has a name, values are +/// extracted in the same way as non-tail dynamic segments. +/// +/// ## Examples +/// ```rust +/// # use actix_router::{Path, ResourceDef}; +/// let resource = ResourceDef::new("/blob/{tail}*"); +/// assert!(resource.is_match("/blob/HEAD/Cargo.toml")); +/// assert!(resource.is_match("/blob/HEAD/README.md")); +/// +/// let mut path = Path::new("/blob/main/LICENSE"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); +/// ``` +/// +/// +/// # Multi-Pattern Resources +/// For resources that can map to multiple distinct paths, it may be suitable to use +/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined +/// into a regex set which is usually quicker to check matches on than checking each +/// pattern individually. +/// +/// Multi-pattern resources can contain dynamic segments just like single pattern ones. +/// However, take care to use consistent and semantically-equivalent segment names; it could affect +/// expectations in the router using these definitions and cause runtime panics. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(["/home", "/index"]); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/index")); +/// ``` +/// +/// +/// # Trailing Slashes +/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. +/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if +/// they you wish to accommodate "recoverable" path errors. Below are several examples of +/// resource-path pairs that would not be compatible. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// assert!(!ResourceDef::new("/root").is_match("/root/")); +/// assert!(!ResourceDef::new("/root/").is_match("/root")); +/// assert!(!ResourceDef::prefix("/root/").is_match("/root")); +/// ``` +#[derive(Clone, Debug)] +pub struct ResourceDef { + id: u16, + + /// Optional name of resource. + name: Option, + + /// Pattern that generated the resource definition. + /// + /// `None` when pattern type is `DynamicSet`. + patterns: Patterns, + + /// Pattern type. + pat_type: PatternType, + + /// List of segments that compose the pattern, in order. + /// + /// `None` when pattern type is `DynamicSet`. + segments: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +enum PatternSegment { + /// Literal slice of pattern. + Const(String), + + /// Name of dynamic segment. + Var(String), +} + +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +enum PatternType { + /// Single constant/literal segment. + Static(String), + + /// Single constant/literal prefix segment. + Prefix(String), + + /// Single regular expression and list of dynamic segment names. + Dynamic(Regex, Vec<&'static str>), + + /// Regular expression set and list of component expressions plus dynamic segment names. + DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>), +} + +impl ResourceDef { + /// Constructs a new resource definition from patterns. + /// + /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. + /// + /// # Panics + /// Panics if path pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::new("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/1234")); + /// assert!(!resource.is_match("/foo")); + /// + /// let resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert!(resource.is_match("/profile")); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("/foo")); + /// ``` + pub fn new(paths: T) -> Self { + profile_method!(new); + + match paths.patterns() { + Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false), + + // since zero length pattern sets are possible + // just return a useless `ResourceDef` + Patterns::List(patterns) if patterns.is_empty() => ResourceDef { + id: 0, + name: None, + patterns: Patterns::List(patterns), + pat_type: PatternType::DynamicSet(RegexSet::empty(), Vec::new()), + segments: None, + }, + + Patterns::List(patterns) => { + let mut re_set = Vec::with_capacity(patterns.len()); + let mut pattern_data = Vec::new(); + + for pattern in &patterns { + match ResourceDef::parse(&pattern, false, true) { + (PatternType::Dynamic(re, names), _) => { + re_set.push(re.as_str().to_owned()); + pattern_data.push((re, names)); + } + _ => unreachable!(), + } + } + + let pattern_re_set = RegexSet::new(re_set).unwrap(); + + ResourceDef { + id: 0, + name: None, + patterns: Patterns::List(patterns), + pat_type: PatternType::DynamicSet(pattern_re_set, pattern_data), + segments: None, + } + } + } + } + + /// Constructs a new resource definition using a string pattern that performs prefix matching. + /// + /// More specifically, the regular expressions generated for matching are different when using + /// this method vs using `new`; they will not be appended with the `$` meta-character that + /// matches the end of an input. + /// + /// Although it will compile and run correctly, it is meaningless to construct a prefix + /// resource definition with a tail segment; use [`new`][Self::new] in this case. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("user/123/stars")); + /// assert!(!resource.is_match("/foo")); + /// + /// let resource = ResourceDef::prefix("user/{id}"); + /// assert!(resource.is_match("user/123")); + /// assert!(resource.is_match("user/123/stars")); + /// assert!(!resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("foo")); + /// ``` + pub fn prefix(path: &str) -> Self { + profile_method!(prefix); + ResourceDef::from_single_pattern(path, true) + } + + /// Constructs a new resource definition using a string pattern that performs prefix matching, + /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::root_prefix("user/{id}"); + /// + /// assert_eq!(&resource, &ResourceDef::prefix("/user/{id}")); + /// assert_eq!(&resource, &ResourceDef::root_prefix("/user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("/user/{id}")); + /// + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// ``` + pub fn root_prefix(path: &str) -> Self { + profile_method!(root_prefix); + ResourceDef::prefix(&insert_slash(path)) + } + + /// Returns a numeric resource ID. + /// + /// If not explicitly set using [`set_id`][Self::set_id], this will return `0`. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert_eq!(resource.id(), 0); + /// + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn id(&self) -> u16 { + self.id + } + + /// Set numeric resource ID. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn set_id(&mut self, id: u16) { + self.id = id; + } + + /// Returns resource definition name, if set. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert!(resource.name().is_none()); + /// + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Assigns a new name to the resource. + /// + /// # Panics + /// Panics if `name` is an empty string. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + /// ``` + pub fn set_name(&mut self, name: impl Into) { + let name = name.into(); + + if name.is_empty() { + panic!("resource name should not be empty"); + } + + self.name = Some(name) + } + + /// Returns `true` if pattern type is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// assert!(ResourceDef::prefix("/user").is_prefix()); + /// assert!(!ResourceDef::new("/user").is_prefix()); + /// ``` + pub fn is_prefix(&self) -> bool { + match &self.pat_type { + PatternType::Prefix(_) => true, + PatternType::Dynamic(re, _) if !re.as_str().ends_with('$') => true, + _ => false, + } + } + + /// Returns the pattern string that generated the resource definition. + /// + /// Returns `None` if definition was constructed with multiple patterns. + /// See [`patterns_iter`][Self::pattern_iter]. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/user/{id}"); + /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); + /// + /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert!(resource.pattern().is_none()); + pub fn pattern(&self) -> Option<&str> { + match &self.patterns { + Patterns::Single(pattern) => Some(pattern.as_str()), + Patterns::List(_) => None, + } + } + + /// Returns iterator of pattern strings that generated the resource definition. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert!(iter.next().is_none()); + /// + /// let mut resource = ResourceDef::new(["/root", "/backup"]); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert_eq!(iter.next().unwrap(), "/backup"); + /// assert!(iter.next().is_none()); + pub fn pattern_iter(&self) -> impl Iterator { + struct PatternIter<'a> { + patterns: &'a Patterns, + list_idx: usize, + done: bool, + } + + impl<'a> Iterator for PatternIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + match &self.patterns { + Patterns::Single(pattern) => { + if self.done { + return None; + } + + self.done = true; + Some(pattern.as_str()) + } + Patterns::List(patterns) if patterns.is_empty() => None, + Patterns::List(patterns) => match patterns.get(self.list_idx) { + Some(pattern) => { + self.list_idx += 1; + Some(pattern.as_str()) + } + None => { + // fast path future call + self.done = true; + None + } + }, + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.patterns { + Patterns::Single(_) => (1, Some(1)), + Patterns::List(patterns) => (patterns.len(), Some(patterns.len())), + } + } + } + + PatternIter { + patterns: &self.patterns, + list_idx: 0, + done: false, + } + } + + /// Joins two resources. + /// + /// Resulting resource is prefix if `other` is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let joined = ResourceDef::prefix("/root").join(&ResourceDef::prefix("/seg")); + /// assert_eq!(joined, ResourceDef::prefix("/root/seg")); + /// ``` + pub fn join(&self, other: &ResourceDef) -> ResourceDef { + let patterns = self + .pattern_iter() + .flat_map(move |this| other.pattern_iter().map(move |other| (this, other))) + .map(|(this, other)| [this, other].join("")) + .collect::>(); + + match patterns.len() { + 1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), + _ => ResourceDef::new(patterns), + } + } + + /// Returns `true` if `path` matches this resource. + /// + /// The behavior of this method depends on how the `ResourceDef` was constructed. For example, + /// static resources will not be able to match as many paths as dynamic and prefix resources. + /// See [`ResourceDef`] struct docs for details on resource definition types. + /// + /// This method will always agree with [`find_match`][Self::find_match] on whether the path + /// matches or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert!(resource.is_match("/user")); + /// assert!(!resource.is_match("/users")); + /// assert!(!resource.is_match("/user/123")); + /// assert!(!resource.is_match("/foo")); + /// + /// // dynamic resource + /// let resource = ResourceDef::new("/user/{user_id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// + /// // prefix resource + /// let resource = ResourceDef::prefix("/root"); + /// assert!(resource.is_match("/root")); + /// assert!(resource.is_match("/root/leaf")); + /// assert!(!resource.is_match("/roots")); + /// + /// // more examples are shown in the `ResourceDef` struct docs + /// ``` + #[inline] + pub fn is_match(&self, path: &str) -> bool { + profile_method!(is_match); + + // this function could be expressed as: + // `self.find_match(path).is_some()` + // but this skips some checks and uses potentially faster regex methods + + match self.pat_type { + PatternType::Static(ref s) => s == path, + + PatternType::Prefix(ref prefix) if prefix == path => true, + PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path), + + // dynamic prefix + PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { + match re.find(path) { + // prefix matches exactly + Some(m) if m.end() == path.len() => true, + + // prefix matches part + Some(m) => is_strict_prefix(m.as_str(), path), + + // prefix does not match + None => false, + } + } + + PatternType::Dynamic(ref re, _) => re.is_match(path), + PatternType::DynamicSet(ref re, _) => re.is_match(path), + } + } + + /// Tries to match `path` to this resource, returning the position in the path where the + /// match ends. + /// + /// This method will always agree with [`is_match`][Self::is_match] on whether the path matches + /// or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert!(resource.find_match("/user/").is_none()); + /// assert!(resource.find_match("/user/123").is_none()); + /// assert!(resource.find_match("/foo").is_none()); + /// + /// // constant prefix resource + /// let resource = ResourceDef::prefix("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert_eq!(resource.find_match("/user/"), Some(5)); + /// assert_eq!(resource.find_match("/user/123"), Some(5)); + /// + /// // dynamic prefix resource + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/user/1234/"), Some(10)); + /// assert_eq!(resource.find_match("/user/12345/stars"), Some(11)); + /// assert!(resource.find_match("/user/").is_none()); + /// + /// // multi-pattern resource + /// let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/profile/1234"), Some(13)); + /// ``` + pub fn find_match(&self, path: &str) -> Option { + profile_method!(find_match); + + match &self.pat_type { + PatternType::Static(segment) if path == segment => Some(segment.len()), + PatternType::Static(_) => None, + + PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), + PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()), + PatternType::Prefix(_) => None, + + // dynamic prefix + PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { + match re.find(path) { + // prefix matches exactly + Some(m) if m.end() == path.len() => Some(m.end()), + + // prefix matches part + Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()), + + // prefix does not match + _ => None, + } + } + + PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()), + + PatternType::DynamicSet(re, params) => { + let idx = re.matches(path).into_iter().next()?; + let (ref pattern, _) = params[idx]; + pattern.find(path).map(|m| m.end()) + } + } + } + + /// Collects dynamic segment values into `path`. + /// + /// Returns `true` if `path` matches this resource. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// let mut path = Path::new("/user/123/stars"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("id").unwrap(), "123"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// let resource = ResourceDef::new("/blob/{path}*"); + /// let mut path = Path::new("/blob/HEAD/Cargo.toml"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml"); + /// assert_eq!(path.unprocessed(), ""); + /// ``` + pub fn capture_match_info(&self, path: &mut Path) -> bool { + profile_method!(capture_match_info); + self.capture_match_info_fn(path, |_, _| true, ()) + } + + /// Collects dynamic segment values into `resource` after matching paths and executing + /// check function. + /// + /// The check function is given a reference to the passed resource and optional arbitrary data. + /// This is useful if you want to conditionally match on some non-path related aspect of the + /// resource type. + /// + /// Returns `true` if resource path matches this resource definition _and_ satisfies the + /// given check function. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { + /// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok(); + /// + /// resource.capture_match_info_fn( + /// path, + /// // when env var is not set, reject when path contains "admin" + /// |res, admin_allowed| !res.path().contains("admin"), + /// &admin_allowed + /// ) + /// } + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// + /// // path matches; segment values are collected into path + /// let mut path = Path::new("/user/james/stars"); + /// assert!(try_match(&resource, &mut path)); + /// assert_eq!(path.get("id").unwrap(), "james"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// // path matches but fails check function; no segments are collected + /// let mut path = Path::new("/user/admin/stars"); + /// assert!(!try_match(&resource, &mut path)); + /// assert_eq!(path.unprocessed(), "/user/admin/stars"); + /// ``` + pub fn capture_match_info_fn( + &self, + resource: &mut R, + check_fn: F, + user_data: U, + ) -> bool + where + R: Resource, + T: ResourcePath, + F: FnOnce(&R, U) -> bool, + { + profile_method!(capture_match_info_fn); + + let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); + let path = resource.resource_path(); + let path_str = path.path(); + + let (matched_len, matched_vars) = match &self.pat_type { + PatternType::Static(_) | PatternType::Prefix(_) => { + profile_section!(pattern_static_or_prefix); + + match self.find_match(path_str) { + Some(len) => (len, None), + None => return false, + } + } + + PatternType::Dynamic(re, names) => { + profile_section!(pattern_dynamic); + + let captures = { + profile_section!(pattern_dynamic_regex_exec); + + match re.captures(path.path()) { + Some(captures) => captures, + _ => return false, + } + }; + + { + profile_section!(pattern_dynamic_extract_captures); + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } + } + }; + + (captures[0].len(), Some(names)) + } + + PatternType::DynamicSet(re, params) => { + profile_section!(pattern_dynamic_set); + + let path = path.path(); + let (pattern, names) = match re.matches(path).into_iter().next() { + Some(idx) => ¶ms[idx], + _ => return false, + }; + + let captures = match pattern.captures(path.path()) { + Some(captures) => captures, + _ => return false, + }; + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!("Dynamic path match but not all segments found: {}", name); + return false; + } + } + + (captures[0].len(), Some(names)) + } + }; + + if !check_fn(resource, user_data) { + return false; + } + + // Modify `path` to skip matched part and store matched segments + let path = resource.resource_path(); + + if let Some(vars) = matched_vars { + for i in 0..vars.len() { + path.add(vars[i], mem::take(&mut segments[i])); + } + } + + path.skip(matched_len as u16); + + true + } + + /// Assembles resource path using a closure that maps variable segment names to values. + fn build_resource_path(&self, path: &mut String, mut vars: F) -> bool + where + F: FnMut(&str) -> Option, + I: AsRef, + { + for el in match self.segments { + Some(ref segments) => segments, + None => return false, + } { + match *el { + PatternSegment::Const(ref val) => path.push_str(val), + PatternSegment::Var(ref name) => match vars(name) { + Some(val) => path.push_str(val.as_ref()), + _ => return false, + }, + } + } + + true + } + + /// Assembles full resource path from iterator of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// Resource paths can not be built from multi-pattern resources; this call will always return + /// false and will not add anything to the string buffer. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// assert!(resource.resource_path_from_iter(&mut s, &["123", "my-post"])); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_iter(&self, path: &mut String, values: I) -> bool + where + I: IntoIterator, + I::Item: AsRef, + { + profile_method!(resource_path_from_iter); + let mut iter = values.into_iter(); + self.build_resource_path(path, |_| iter.next()) + } + + /// Assembles resource path from map of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// Resource paths can not be built from multi-pattern resources; this call will always return + /// false and will not add anything to the string buffer. + /// + /// # Examples + /// ``` + /// # use std::collections::HashMap; + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// let mut map = HashMap::new(); + /// map.insert("id", "123"); + /// map.insert("title", "my-post"); + /// + /// assert!(resource.resource_path_from_map(&mut s, &map)); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_map( + &self, + path: &mut String, + values: &HashMap, + ) -> bool + where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + profile_method!(resource_path_from_map); + self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) + } + + /// Parse path pattern and create a new instance. + fn from_single_pattern(pattern: &str, is_prefix: bool) -> Self { + profile_method!(from_single_pattern); + + let pattern = pattern.to_owned(); + let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); + + ResourceDef { + id: 0, + name: None, + patterns: Patterns::Single(pattern), + pat_type, + segments: Some(segments), + } + } + + /// Parses a dynamic segment definition from a pattern. + /// + /// The returned tuple includes: + /// - the segment descriptor, either `Var` or `Tail` + /// - the segment's regex to check values against + /// - the remaining, unprocessed string slice + /// - whether the parsed parameter represents a tail pattern + /// + /// # Panics + /// Panics if given patterns does not contain a dynamic segment. + fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) { + profile_method!(parse_param); + + const DEFAULT_PATTERN: &str = "[^/]+"; + const DEFAULT_PATTERN_TAIL: &str = ".*"; + + let mut params_nesting = 0usize; + let close_idx = pattern + .find(|c| match c { + '{' => { + params_nesting += 1; + false + } + '}' => { + params_nesting -= 1; + params_nesting == 0 + } + _ => false, + }) + .unwrap_or_else(|| { + panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) + }); + + let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); + + // remove outer curly brackets + param = ¶m[1..param.len() - 1]; + + let tail = unprocessed == "*"; + + let (name, pattern) = match param.find(':') { + Some(idx) => { + if tail { + panic!("custom regex is not supported for tail match"); + } + + let (name, pattern) = param.split_at(idx); + (name, &pattern[1..]) + } + None => ( + param, + if tail { + unprocessed = &unprocessed[1..]; + DEFAULT_PATTERN_TAIL + } else { + DEFAULT_PATTERN + }, + ), + }; + + let segment = PatternSegment::Var(name.to_string()); + let regex = format!(r"(?P<{}>{})", &name, &pattern); + + (segment, regex, unprocessed, tail) + } + + /// Parse `pattern` using `is_prefix` and `force_dynamic` flags. + /// + /// Parameters: + /// - `is_prefix`: Use `true` if `pattern` should be treated as a prefix; i.e., a conforming + /// path will be a match even if it has parts remaining to process + /// - `force_dynamic`: Use `true` to disallow the return of static and prefix segments. + /// + /// The returned tuple includes: + /// - the pattern type detected, either `Static`, `Prefix`, or `Dynamic` + /// - a list of segment descriptors from the pattern + fn parse( + pattern: &str, + is_prefix: bool, + force_dynamic: bool, + ) -> (PatternType, Vec) { + profile_method!(parse); + + let mut unprocessed = pattern; + + if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { + // pattern is static + + let tp = if is_prefix { + PatternType::Prefix(unprocessed.to_owned()) + } else { + PatternType::Static(unprocessed.to_owned()) + }; + + return (tp, vec![PatternSegment::Const(unprocessed.to_owned())]); + } + + let mut segments = Vec::new(); + let mut re = format!("{}^", REGEX_FLAGS); + let mut dyn_segment_count = 0; + let mut has_tail_segment = false; + + while let Some(idx) = unprocessed.find('{') { + let (prefix, rem) = unprocessed.split_at(idx); + + segments.push(PatternSegment::Const(prefix.to_owned())); + re.push_str(&escape(prefix)); + + let (param_pattern, re_part, rem, tail) = Self::parse_param(rem); + + if tail { + has_tail_segment = true; + } + + segments.push(param_pattern); + re.push_str(&re_part); + + unprocessed = rem; + dyn_segment_count += 1; + } + + if is_prefix && has_tail_segment { + // tail segments in prefixes have no defined semantics + + #[cfg(not(test))] + log::warn!( + "Prefix resources should not have tail segments. \ + Use `ResourceDef::new` constructor. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("prefix resource definitions should not have tail segments"); + } + + if unprocessed.ends_with('*') { + // unnamed tail segment + + #[cfg(not(test))] + log::warn!( + "Tail segments must have names. \ + Consider `.../{{tail}}*`. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("tail segments must have names"); + } else if !has_tail_segment && !unprocessed.is_empty() { + // prevent `Const("")` element from being added after last dynamic segment + + segments.push(PatternSegment::Const(unprocessed.to_owned())); + re.push_str(&escape(unprocessed)); + } + + if dyn_segment_count > MAX_DYNAMIC_SEGMENTS { + panic!( + "Only {} dynamic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, dyn_segment_count + ); + } + + if !is_prefix && !has_tail_segment { + re.push('$'); + } + + let re = match Regex::new(&re) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", pattern, err), + }; + + // `Bok::leak(Box::new(name))` is an intentional memory leak. In typical applications the + // routing table is only constructed once (per worker) so leak is bounded. If you are + // constructing `ResourceDef`s more than once in your application's lifecycle you would + // expect a linear increase in leaked memory over time. + let names = re + .capture_names() + .filter_map(|name| name.map(|name| Box::leak(Box::new(name.to_owned())).as_str())) + .collect(); + + (PatternType::Dynamic(re, names), segments) + } +} + +impl Eq for ResourceDef {} + +impl PartialEq for ResourceDef { + fn eq(&self, other: &ResourceDef) -> bool { + self.patterns == other.patterns + && match &self.pat_type { + PatternType::Static(_) => matches!(&other.pat_type, PatternType::Static(_)), + PatternType::Prefix(_) => matches!(&other.pat_type, PatternType::Prefix(_)), + PatternType::Dynamic(re, _) => match &other.pat_type { + PatternType::Dynamic(other_re, _) => re.as_str() == other_re.as_str(), + _ => false, + }, + PatternType::DynamicSet(_, _) => { + matches!(&other.pat_type, PatternType::DynamicSet(..)) + } + } + } +} + +impl Hash for ResourceDef { + fn hash(&self, state: &mut H) { + self.patterns.hash(state); + } +} + +impl<'a> From<&'a str> for ResourceDef { + fn from(path: &'a str) -> ResourceDef { + ResourceDef::new(path) + } +} + +impl From for ResourceDef { + fn from(path: String) -> ResourceDef { + ResourceDef::new(path) + } +} + +pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { + profile_fn!(insert_slash); + + if !path.is_empty() && !path.starts_with('/') { + let mut new_path = String::with_capacity(path.len() + 1); + new_path.push('/'); + new_path.push_str(path); + Cow::Owned(new_path) + } else { + Cow::Borrowed(path) + } +} + +/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. +/// +/// The `strict` refers to the fact that this will return `false` if `prefix == path`. +fn is_strict_prefix(prefix: &str, path: &str) -> bool { + path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn equivalence() { + assert_eq!( + ResourceDef::root_prefix("/root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("/{id}"), + ResourceDef::prefix("/{id}") + ); + assert_eq!( + ResourceDef::root_prefix("{id}"), + ResourceDef::prefix("/{id}") + ); + + assert_eq!(ResourceDef::new("/"), ResourceDef::new(["/"])); + assert_eq!(ResourceDef::new("/"), ResourceDef::new(vec!["/"])); + + assert_ne!(ResourceDef::new(""), ResourceDef::prefix("")); + assert_ne!(ResourceDef::new("/"), ResourceDef::prefix("/")); + assert_ne!(ResourceDef::new("/{id}"), ResourceDef::prefix("/{id}")); + } + + #[test] + fn parse_static() { + let re = ResourceDef::new(""); + + assert!(!re.is_prefix()); + + assert!(re.is_match("")); + assert!(!re.is_match("/")); + assert_eq!(re.find_match(""), Some(0)); + assert_eq!(re.find_match("/"), None); + + let re = ResourceDef::new("/"); + assert!(re.is_match("/")); + assert!(!re.is_match("")); + assert!(!re.is_match("/foo")); + + let re = ResourceDef::new("/name"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name/"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::new("/name/"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = ResourceDef::new("/user/profile"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn parse_param() { + let re = ResourceDef::new("/user/{id}"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/v{version}/resource/{id}"); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + assert_eq!(path.unprocessed(), ""); + } + + #[allow(clippy::cognitive_complexity)] + #[test] + fn dynamic_set() { + let re = ResourceDef::new(vec![ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + "/static", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + assert!(re.is_match("/static")); + assert!(!re.is_match("/a/static")); + assert!(!re.is_match("/static/a")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + + let re = ResourceDef::new([ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let re = ResourceDef::new([ + "/user/{id}".to_string(), + "/v{version}/resource/{id}".to_string(), + "/{id:[[:digit:]]{6}}".to_string(), + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + } + + #[test] + fn parse_tail() { + let re = ResourceDef::new("/user/-{id}*"); + + let mut path = Path::new("/user/-profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + + let mut path = Path::new("/user/-2345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/-2345/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/"); + + let mut path = Path::new("/user/-2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/sdg"); + } + + #[test] + fn static_tail() { + let re = ResourceDef::new("/user{tail}*"); + assert!(re.is_match("/users")); + assert!(re.is_match("/user-foo")); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + } + + #[test] + fn dynamic_tail() { + let re = ResourceDef::new("/user/{id}/{tail}*"); + assert!(!re.is_match("/user/2345")); + let mut path = Path::new("/user/2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + assert_eq!(path.get("tail").unwrap(), "sdg"); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn newline_patterns_and_paths() { + let re = ResourceDef::new("/user/a\nb"); + assert!(re.is_match("/user/a\nb")); + assert!(!re.is_match("/user/a\nb/profile")); + + let re = ResourceDef::new("/a{x}b/test/a{y}b"); + let mut path = Path::new("/a\nb/test/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("x").unwrap(), "\n"); + assert_eq!(path.get("y").unwrap(), "\n"); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/a\nb/")); + + let re = ResourceDef::new("/user/{id}*"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + + let re = ResourceDef::new("/user/{id:.*}"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + } + + #[cfg(feature = "http")] + #[test] + fn parse_urlencoded_param() { + use std::convert::TryFrom; + + let re = ResourceDef::new("/user/{id}/test"); + + let mut path = Path::new("/user/2345/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/qwe%25/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let uri = http::Uri::try_from("/user/qwe%25/test").unwrap(); + let mut path = Path::new(uri); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + } + + #[test] + fn prefix_static() { + let re = ResourceDef::prefix("/name"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name")); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/test/test")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/name/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "/test"); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/test/test"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::prefix("/name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "gs"); + + let re = ResourceDef::root_prefix("name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "gs"); + } + + #[test] + fn prefix_dynamic() { + let re = ResourceDef::prefix("/{name}/"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + assert_eq!(re.find_match("/name/"), Some(6)); + assert_eq!(re.find_match("/name/gs"), Some(6)); + assert_eq!(re.find_match("/name"), None); + + let mut path = Path::new("/test2/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/test2/subpath1/subpath2/index.html"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); + + let resource = ResourceDef::prefix("/user"); + // input string shorter than prefix + assert!(resource.find_match("/foo").is_none()); + } + + #[test] + fn build_path_list() { + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter())); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn multi_pattern_cannot_build_path() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + } + + #[test] + fn multi_pattern_capture_segment_values() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + assert!(path.get("uid").is_none()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_none()); + assert!(path.get("uid").is_some()); + } + + #[test] + fn dynamic_prefix_proper_segmentation() { + let resource = ResourceDef::prefix(r"/id/{id:\d{3}}"); + + assert!(resource.is_match("/id/123")); + assert!(resource.is_match("/id/123/foo")); + assert!(!resource.is_match("/id/1234")); + assert!(!resource.is_match("/id/123a")); + + assert_eq!(resource.find_match("/id/123"), Some(7)); + assert_eq!(resource.find_match("/id/123/foo"), Some(7)); + assert_eq!(resource.find_match("/id/1234"), None); + assert_eq!(resource.find_match("/id/123a"), None); + } + + #[test] + fn build_path_map() { + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + + let mut map = HashMap::new(); + map.insert("item1", "item"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_map(&mut s, &map)); + + map.insert("item2", "item2"); + + let mut s = String::new(); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn build_path_tail() { + let resource = ResourceDef::new("/user/{item1}*"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1"); + + let mut s = String::new(); + let mut map = HashMap::new(); + map.insert("item1", "item"); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item"); + } + + #[test] + fn consistent_match_length() { + let result = Some(5); + + let re = ResourceDef::prefix("/abc/"); + assert_eq!(re.find_match("/abc/def"), result); + + let re = ResourceDef::prefix("/{id}/"); + assert_eq!(re.find_match("/abc/def"), result); + } + + #[test] + fn join() { + // test joined defs match the same paths as each component separately + + fn seq_find_match(re1: &ResourceDef, re2: &ResourceDef, path: &str) -> Option { + let len1 = re1.find_match(path)?; + let len2 = re2.find_match(&path[len1..])?; + Some(len1 + len2) + } + + macro_rules! join_test { + ($pat1:expr, $pat2:expr => $($test:expr),+) => {{ + let pat1 = $pat1; + let pat2 = $pat2; + $({ + let _path = $test; + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::new(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(!re1.join(&re2).is_prefix()); + + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::prefix(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, prefix {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(re1.join(&re2).is_prefix()); + })+ + }} + } + + join_test!("", "" => "", "/hello", "/"); + join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); + join_test!("", "/user"=> "", "/user", "foo", "/user11", "user", "user/123"); + join_test!("/user", "/xx"=> "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + } + + #[test] + fn match_methods_agree() { + macro_rules! match_methods_agree { + ($pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::new($pat), $($test),+); + }}; + (prefix $pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::prefix($pat), $($test),+); + }}; + (finish $pat:expr, $re:expr, $($test:expr),+) => {{ + let re = $re; + $({ + let _is = re.is_match($test); + let _find = re.find_match($test).is_some(); + assert_eq!( + _is, _find, + "pattern: {:?}; mismatch on \"{}\"; is={}; find={}", + $pat, $test, _is, _find + ); + })+ + }} + } + + match_methods_agree!("" => "", "/", "/foo"); + match_methods_agree!("/" => "", "/", "/foo"); + match_methods_agree!("/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!("/v{v}" => "v", "/v", "/v1", "/v222", "/foo"); + match_methods_agree!(["/v{v}", "/version/{v}"] => "/v", "/v1", "/version", "/version/1", "/foo"); + + match_methods_agree!("/path{tail}*" => "/path", "/path1", "/path/123"); + match_methods_agree!("/path/{tail}*" => "/path", "/path1", "/path/123"); + + match_methods_agree!(prefix "" => "", "/", "/foo"); + match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_delimiter() { + ResourceDef::new("/user/{username"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_name() { + ResourceDef::new("/user/{}"); + } + + #[test] + #[should_panic] + fn invalid_too_many_dynamic_segments() { + // valid + ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); + + // panics + ResourceDef::new( + "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}", + ); + } + + #[test] + #[should_panic] + fn invalid_custom_regex_for_tail() { + ResourceDef::new(r"/{tail:\d+}*"); + } + + #[test] + #[should_panic] + fn invalid_unnamed_tail_segment() { + ResourceDef::new("/*"); + } + + #[test] + #[should_panic] + fn prefix_plus_tail_match_is_allowed() { + ResourceDef::prefix("/user/{id}*"); + } +} diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs new file mode 100644 index 000000000..f5deb8583 --- /dev/null +++ b/actix-router/src/router.rs @@ -0,0 +1,281 @@ +use firestorm::profile_method; + +use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ResourceId(pub u16); + +/// Information about current resource +#[derive(Clone, Debug)] +pub struct ResourceInfo { + resource: ResourceId, +} + +/// Resource router. +// T is the resource itself +// U is any other data needed for routing like method guards +pub struct Router { + routes: Vec<(ResourceDef, T, Option)>, +} + +impl Router { + pub fn build() -> RouterBuilder { + RouterBuilder { + resources: Vec::new(), + } + } + + pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize); + + for item in self.routes.iter() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_checked); + + for item in self.routes.iter() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut_fn( + &mut self, + resource: &mut R, + check: F, + ) -> Option<(&mut T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut_checked); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } +} + +pub struct RouterBuilder { + resources: Vec<(ResourceDef, T, Option)>, +} + +impl RouterBuilder { + /// Register resource for specified path. + pub fn path( + &mut self, + path: P, + resource: T, + ) -> &mut (ResourceDef, T, Option) { + profile_method!(path); + + self.resources + .push((ResourceDef::new(path), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for specified path prefix. + pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(prefix); + + self.resources + .push((ResourceDef::prefix(prefix), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for ResourceDef + pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(rdef); + + self.resources.push((rdef, resource, None)); + self.resources.last_mut().unwrap() + } + + /// Finish configuration and create router instance. + pub fn finish(self) -> Router { + Router { + routes: self.resources, + } + } +} + +#[cfg(test)] +mod tests { + use crate::path::Path; + use crate::router::{ResourceId, Router}; + + #[allow(clippy::cognitive_complexity)] + #[test] + fn test_recognizer_1() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + router.path("/name/{val}/index.html", 12).0.set_id(2); + router.path("/file/{file}.{ext}", 13).0.set_id(3); + router.path("/v{val}/{val2}/index.html", 14).0.set_id(4); + router.path("/v/{tail:.*}", 15).0.set_id(5); + router.path("/test2/{test}.html", 16).0.set_id(6); + router.path("/{test}/index.html", 17).0.set_id(7); + let mut router = router.finish(); + + let mut path = Path::new("/unknown"); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/name"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + assert_eq!(info, ResourceId(0)); + assert!(path.is_empty()); + + let mut path = Path::new("/name/value"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(info, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + let mut path = Path::new("/name/value2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 12); + assert_eq!(info, ResourceId(2)); + assert_eq!(path.get("val").unwrap(), "value2"); + + let mut path = Path::new("/file/file.gz"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 13); + assert_eq!(info, ResourceId(3)); + assert_eq!(path.get("file").unwrap(), "file"); + assert_eq!(path.get("ext").unwrap(), "gz"); + + let mut path = Path::new("/vtest/ttt/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 14); + assert_eq!(info, ResourceId(4)); + assert_eq!(path.get("val").unwrap(), "test"); + assert_eq!(path.get("val2").unwrap(), "ttt"); + + let mut path = Path::new("/v/blah-blah/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 15); + assert_eq!(info, ResourceId(5)); + assert_eq!(path.get("tail").unwrap(), "blah-blah/index.html"); + + let mut path = Path::new("/test2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 16); + assert_eq!(info, ResourceId(6)); + assert_eq!(path.get("test").unwrap(), "index"); + + let mut path = Path::new("/bbb/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 17); + assert_eq!(info, ResourceId(7)); + assert_eq!(path.get("test").unwrap(), "bbb"); + } + + #[test] + fn test_recognizer_2() { + let mut router = Router::::build(); + router.path("/index.json", 10); + router.path("/{source}.json", 11); + let mut router = router.finish(); + + let mut path = Path::new("/index.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + } + + #[test] + fn test_recognizer_with_prefix() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(5); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test/name"); + path.skip(5); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test/name/value"); + path.skip(5); + let (h, id) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(id, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + // same patterns + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test2/name-test"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name/ttt"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(&path["val"], "ttt"); + } +} diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs new file mode 100644 index 000000000..e08a7171a --- /dev/null +++ b/actix-router/src/url.rs @@ -0,0 +1,288 @@ +use crate::ResourcePath; + +#[allow(dead_code)] +const GEN_DELIMS: &[u8] = b":/?#[]@"; +#[allow(dead_code)] +const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; +#[allow(dead_code)] +const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; +#[allow(dead_code)] +const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; +#[allow(dead_code)] +const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + 1234567890 + -._~"; +const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + 1234567890 + -._~ + !$'()*,"; +const QS: &[u8] = b"+&=;b"; + +#[inline] +fn bit_at(array: &[u8], ch: u8) -> bool { + array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 +} + +#[inline] +fn set_bit(array: &mut [u8], ch: u8) { + array[(ch >> 3) as usize] |= 1 << (ch & 7) +} + +thread_local! { + static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); +} + +#[derive(Default, Clone, Debug)] +pub struct Url { + uri: http::Uri, + path: Option, +} + +impl Url { + pub fn new(uri: http::Uri) -> Url { + let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + + Url { uri, path } + } + + pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { + Url { + path: quoter.requote(uri.path().as_bytes()), + uri, + } + } + + pub fn uri(&self) -> &http::Uri { + &self.uri + } + + pub fn path(&self) -> &str { + if let Some(ref s) = self.path { + s + } else { + self.uri.path() + } + } + + #[inline] + pub fn update(&mut self, uri: &http::Uri) { + self.uri = uri.clone(); + self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + } + + #[inline] + pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { + self.uri = uri.clone(); + self.path = quoter.requote(uri.path().as_bytes()); + } +} + +impl ResourcePath for Url { + #[inline] + fn path(&self) -> &str { + self.path() + } +} + +pub struct Quoter { + safe_table: [u8; 16], + protected_table: [u8; 16], +} + +impl Quoter { + pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { + let mut q = Quoter { + safe_table: [0; 16], + protected_table: [0; 16], + }; + + // prepare safe table + for i in 0..128 { + if ALLOWED.contains(&i) { + set_bit(&mut q.safe_table, i); + } + if QS.contains(&i) { + set_bit(&mut q.safe_table, i); + } + } + + for ch in safe { + set_bit(&mut q.safe_table, *ch) + } + + // prepare protected table + for ch in protected { + set_bit(&mut q.safe_table, *ch); + set_bit(&mut q.protected_table, *ch); + } + + q + } + + pub fn requote(&self, val: &[u8]) -> Option { + let mut has_pct = 0; + let mut pct = [b'%', 0, 0]; + let mut idx = 0; + let mut cloned: Option> = None; + + let len = val.len(); + while idx < len { + let ch = val[idx]; + + if has_pct != 0 { + pct[has_pct] = val[idx]; + has_pct += 1; + if has_pct == 3 { + has_pct = 0; + let buf = cloned.as_mut().unwrap(); + + if let Some(ch) = restore_ch(pct[1], pct[2]) { + if ch < 128 { + if bit_at(&self.protected_table, ch) { + buf.extend_from_slice(&pct); + idx += 1; + continue; + } + + if bit_at(&self.safe_table, ch) { + buf.push(ch); + idx += 1; + continue; + } + } + buf.push(ch); + } else { + buf.extend_from_slice(&pct[..]); + } + } + } else if ch == b'%' { + has_pct = 1; + if cloned.is_none() { + let mut c = Vec::with_capacity(len); + c.extend_from_slice(&val[..idx]); + cloned = Some(c); + } + } else if let Some(ref mut cloned) = cloned { + cloned.push(ch) + } + idx += 1; + } + + cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) + } +} + +#[inline] +fn from_hex(v: u8) -> Option { + if (b'0'..=b'9').contains(&v) { + Some(v - 0x30) // ord('0') == 0x30 + } else if (b'A'..=b'F').contains(&v) { + Some(v - 0x41 + 10) // ord('A') == 0x41 + } else if (b'a'..=b'f').contains(&v) { + Some(v - 0x61 + 10) // ord('a') == 0x61 + } else { + None + } +} + +#[inline] +fn restore_ch(d1: u8, d2: u8) -> Option { + from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2)) +} + +#[cfg(test)] +mod tests { + use http::Uri; + use std::convert::TryFrom; + + use super::*; + use crate::{Path, ResourceDef}; + + const PROTECTED: &[u8] = b"%/+"; + + fn match_url(pattern: &'static str, url: impl AsRef) -> Path { + let re = ResourceDef::new(pattern); + let uri = Uri::try_from(url.as_ref()).unwrap(); + let mut path = Path::new(Url::new(uri)); + assert!(re.capture_match_info(&mut path)); + path + } + + fn percent_encode(data: &[u8]) -> String { + data.iter().map(|c| format!("%{:02X}", c)).collect() + } + + #[test] + fn test_parse_url() { + let re = "/user/{id}/test"; + + let path = match_url(re, "/user/2345/test"); + assert_eq!(path.get("id").unwrap(), "2345"); + + // "%25" should never be decoded into '%' to guarantee the output is a valid + // percent-encoded format + let path = match_url(re, "/user/qwe%25/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let path = match_url(re, "/user/qwe%25rty/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25rty"); + } + + #[test] + fn test_protected_chars() { + let encoded = percent_encode(PROTECTED); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &encoded); + } + + #[test] + fn test_non_protecteed_ascii() { + let nonprotected_ascii = ('\u{0}'..='\u{7F}') + .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) + .collect::(); + let encoded = percent_encode(nonprotected_ascii.as_bytes()); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &nonprotected_ascii); + } + + #[test] + fn test_valid_utf8_multibyte() { + let test = ('\u{FF00}'..='\u{FFFF}').collect::(); + let encoded = percent_encode(test.as_bytes()); + let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); + assert_eq!(path.get("id").unwrap(), &test); + } + + #[test] + fn test_invalid_utf8() { + let invalid_utf8 = percent_encode((0x80..=0xff).collect::>().as_slice()); + let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap(); + let path = Path::new(Url::new(uri)); + + // We should always get a valid utf8 string + assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); + } + + #[test] + fn test_from_hex() { + let hex = b"0123456789abcdefABCDEF"; + + for i in 0..256 { + let c = i as u8; + if hex.contains(&c) { + assert!(from_hex(c).is_some()) + } else { + assert!(from_hex(c).is_none()) + } + } + + let expected = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, + ]; + for i in 0..hex.len() { + assert_eq!(from_hex(hex[i]).unwrap(), expected[i]); + } + } +} diff --git a/src/app.rs b/src/app.rs index 5cff20568..da5b45f3a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -334,7 +334,7 @@ where U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } diff --git a/src/app_service.rs b/src/app_service.rs index 3c1b78474..ce52543b8 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -291,7 +291,7 @@ impl Service for AppRouting { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_checked(&mut req, |req, guards| { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { diff --git a/src/config.rs b/src/config.rs index b072ace16..9e77c0f96 100644 --- a/src/config.rs +++ b/src/config.rs @@ -249,7 +249,7 @@ impl ServiceConfig { U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } diff --git a/src/dev.rs b/src/dev.rs index b8d95efbb..0817d902f 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -28,11 +28,22 @@ pub use actix_service::{ use crate::http::header::ContentEncoding; use actix_http::{Response, ResponseBuilder}; -pub(crate) fn insert_leading_slash(mut patterns: Vec) -> Vec { - for path in &mut patterns { - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; +use actix_router::Patterns; + +pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { + match &mut patterns { + Patterns::Single(pat) => { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + Patterns::List(pats) => { + for pat in pats { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + } } patterns diff --git a/src/request.rs b/src/request.rs index 4b950e758..41c8252a8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -509,7 +509,7 @@ mod tests { #[test] fn test_url_for() { let mut res = ResourceDef::new("/user/{name}.{ext}"); - *res.name_mut() = "index".to_string(); + res.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut res, None); @@ -539,7 +539,7 @@ mod tests { #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); @@ -560,7 +560,7 @@ mod tests { #[test] fn test_match_name() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); @@ -579,7 +579,7 @@ mod tests { fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); - *rdef.name_mut() = "youtube".to_string(); + rdef.set_name("youtube"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); diff --git a/src/resource.rs b/src/resource.rs index 20d1ee17e..851ce0fc9 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::future::Future; use std::rc::Rc; use actix_http::Extensions; -use actix_router::IntoPattern; +use actix_router::{IntoPatterns, Patterns}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ apply, apply_fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -15,7 +15,7 @@ use futures_util::future::join_all; use crate::{ data::Data, - dev::{insert_leading_slash, AppService, HttpServiceFactory, ResourceDef}, + dev::{ensure_leading_slash, AppService, HttpServiceFactory, ResourceDef}, guard::Guard, handler::Handler, responder::Responder, @@ -51,7 +51,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// Default behavior could be overridden with `default_resource()` method. pub struct Resource { endpoint: T, - rdef: Vec, + rdef: Patterns, name: Option, routes: Vec, app_data: Option, @@ -61,7 +61,7 @@ pub struct Resource { } impl Resource { - pub fn new(path: T) -> Resource { + pub fn new(path: T) -> Resource { let fref = Rc::new(RefCell::new(None)); Resource { @@ -391,13 +391,13 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_leading_slash(self.rdef.clone())) + ResourceDef::new(ensure_leading_slash(self.rdef.clone())) } else { ResourceDef::new(self.rdef.clone()) }; if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } *self.factory_ref.borrow_mut() = Some(ResourceFactory { diff --git a/src/rmap.rs b/src/rmap.rs index 3c8805d57..0ee4de47e 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -29,9 +29,8 @@ impl ResourceMap { pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { pattern.set_id(self.patterns.len() as u16); self.patterns.push((pattern.clone(), nested)); - if !pattern.name().is_empty() { - self.named - .insert(pattern.name().to_string(), pattern.clone()); + if let Some(name) = pattern.name() { + self.named.insert(name.to_owned(), pattern.clone()); } } @@ -83,10 +82,10 @@ impl ResourceMap { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); + if let Some(pat_len) = pattern.find_match(path) { + return rmap.has_resource(&path[pat_len..]); } - } else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" { + } else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" { return true; } } @@ -100,14 +99,11 @@ impl ResourceMap { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { + if let Some(plen) = pattern.find_match(path) { return rmap.match_name(&path[plen..]); } } else if pattern.is_match(path) { - return match pattern.name() { - "" => None, - s => Some(s), - }; + return pattern.name(); } } @@ -136,8 +132,9 @@ impl ResourceMap { fn traverse_resource_pattern(&self, remaining: &str) -> String { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(prefix_len) = pattern.is_prefix_match(remaining) { - let prefix = pattern.pattern().to_owned(); + if let Some(prefix_len) = pattern.find_match(remaining) { + // TODO: think about unwrap_or + let prefix = pattern.pattern().unwrap_or("").to_owned(); return [ prefix, @@ -146,7 +143,8 @@ impl ResourceMap { .concat(); } } else if pattern.is_match(remaining) { - return pattern.pattern().to_owned(); + // TODO: think about unwrap_or + return pattern.pattern().unwrap_or("").to_owned(); } } @@ -181,10 +179,15 @@ impl ResourceMap { I: AsRef, { if let Some(pattern) = self.named.get(name) { - if pattern.pattern().starts_with('/') { + if pattern + .pattern() + .map(|pat| pat.starts_with('/')) + .unwrap_or(false) + { self.fill_root(path, elements)?; } - if pattern.resource_path(path, elements) { + + if pattern.resource_path_from_iter(path, elements) { Ok(Some(())) } else { Err(UrlGenerationError::NotEnoughElements) @@ -213,7 +216,8 @@ impl ResourceMap { if let Some(ref parent) = self.parent.borrow().upgrade() { parent.fill_root(path, elements)?; } - if self.root.resource_path(path, elements) { + + if self.root.resource_path_from_iter(path, elements) { Ok(()) } else { Err(UrlGenerationError::NotEnoughElements) @@ -233,7 +237,7 @@ impl ResourceMap { if let Some(ref parent) = self.parent.borrow().upgrade() { if let Some(pattern) = parent.named.get(name) { self.fill_root(path, elements)?; - if pattern.resource_path(path, elements) { + if pattern.resource_path_from_iter(path, elements) { Ok(Some(())) } else { Err(UrlGenerationError::NotEnoughElements) @@ -329,7 +333,7 @@ mod tests { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut rdef = ResourceDef::new("/info"); - *rdef.name_mut() = "root_info".to_owned(); + rdef.set_name("root_info"); root.add(&mut rdef, None); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); @@ -337,7 +341,7 @@ mod tests { user_map.add(&mut rdef, None); let mut rdef = ResourceDef::new("/post/{post_id}"); - *rdef.name_mut() = "user_post".to_owned(); + rdef.set_name("user_post"); user_map.add(&mut rdef, None); root.add( diff --git a/src/scope.rs b/src/scope.rs index aa546c422..97db53eeb 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -530,7 +530,7 @@ impl Service for ScopeService { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_checked(&mut req, |req, guards| { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { diff --git a/src/service.rs b/src/service.rs index 47e7e4acc..148199407 100644 --- a/src/service.rs +++ b/src/service.rs @@ -7,14 +7,14 @@ use actix_http::{ http::{HeaderMap, Method, StatusCode, Uri, Version}, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; -use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; +use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, - dev::insert_leading_slash, + dev::ensure_leading_slash, guard::Guard, info::ConnectionInfo, rmap::ResourceMap, @@ -212,14 +212,14 @@ impl ServiceRequest { self.req.match_pattern() } - #[inline] /// Get a mutable reference to the Path parameters. + #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() } - #[inline] /// Get a reference to a `ResourceMap` of current application. + #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } @@ -459,14 +459,14 @@ where } pub struct WebService { - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } impl WebService { /// Create new `WebService` instance. - pub fn new(path: T) -> Self { + pub fn new(path: T) -> Self { WebService { rdef: path.patterns(), name: None, @@ -528,7 +528,7 @@ impl WebService { struct WebServiceImpl { srv: T, - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } @@ -551,13 +551,15 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_leading_slash(self.rdef)) + ResourceDef::new(ensure_leading_slash(self.rdef)) } else { ResourceDef::new(self.rdef) }; + if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } + config.register_service(rdef, guards, self.srv, None) } } diff --git a/src/types/path.rs b/src/types/path.rs index f2273a59b..4052646e3 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -209,7 +209,7 @@ mod tests { let resource = ResourceDef::new("/{value}/"); let mut req = TestRequest::with_uri("/32/").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); assert_eq!(*Path::::from_request(&req, &mut pl).await.unwrap(), 32); @@ -221,7 +221,7 @@ mod tests { let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) @@ -247,7 +247,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let mut s = Path::::from_request(&req, &mut pl).await.unwrap(); @@ -270,7 +270,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/32/").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let s = Path::::from_request(&req, &mut pl).await.unwrap(); diff --git a/src/web.rs b/src/web.rs index 40ac46275..108ff314f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,10 +1,10 @@ //! Essentials helper functions and types for application registration. -use actix_http::http::Method; -use actix_router::IntoPattern; use std::future::Future; +use actix_http::http::Method; pub use actix_http::Response as HttpResponse; +use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::error::BlockingError; @@ -51,7 +51,7 @@ pub use crate::types::*; /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -pub fn resource(path: T) -> Resource { +pub fn resource(path: T) -> Resource { Resource::new(path) } @@ -268,7 +268,7 @@ where /// .finish(my_service) /// ); /// ``` -pub fn service(path: T) -> WebService { +pub fn service(path: T) -> WebService { WebService::new(path) } From e965d8298f421e9c89fe98b1300b8361e948c324 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 12 Aug 2021 20:18:09 +0100 Subject: [PATCH 2/8] HRS security fixes (#2363) --- actix-http/CHANGES.md | 10 + actix-http/Cargo.toml | 2 +- actix-http/README.md | 4 +- actix-http/src/error.rs | 2 +- actix-http/src/h1/chunked.rs | 432 +++++++++++++++++++++++++++++++ actix-http/src/h1/decoder.rs | 481 ++++++++++------------------------- actix-http/src/h1/encoder.rs | 1 + actix-http/src/h1/mod.rs | 2 + 8 files changed, 583 insertions(+), 351 deletions(-) create mode 100644 actix-http/src/h1/chunked.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 8ead43718..f52f5ba68 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,11 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.8 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + ## 3.0.0-beta.8 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] @@ -210,6 +215,11 @@ [#1878]: https://github.com/actix/actix-web/pull/1878 +## 2.2.1 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + ## 2.2.0 - 2020-11-25 ### Added * HttpResponse builders for 1xx status codes. [#1768] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a12fed4b9..4ce55dca1 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.8" +version = "3.0.0-beta.9" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] diff --git a/actix-http/README.md b/actix-http/README.md index de1ef0a9b..5b06583bc 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http/3.0.0-beta.8) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http/3.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 54666e072..f7d7f696a 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -196,7 +196,7 @@ pub enum ParseError { #[display(fmt = "IO error: {}", _0)] Io(io::Error), - /// Parsing a field as string failed + /// Parsing a field as string failed. #[display(fmt = "UTF8 error: {}", _0)] Utf8(Utf8Error), } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs new file mode 100644 index 000000000..1224ce08c --- /dev/null +++ b/actix-http/src/h1/chunked.rs @@ -0,0 +1,432 @@ +use std::{io, task::Poll}; + +use bytes::{Buf as _, Bytes, BytesMut}; + +macro_rules! byte ( + ($rdr:ident) => ({ + if $rdr.len() > 0 { + let b = $rdr[0]; + $rdr.advance(1); + b + } else { + return Poll::Pending + } + }) +); + +#[derive(Debug, PartialEq, Clone)] +pub(super) enum ChunkedState { + Size, + SizeLws, + Extension, + SizeLf, + Body, + BodyCr, + BodyLf, + EndCr, + EndLf, + End, +} + +impl ChunkedState { + pub(super) fn step( + &self, + body: &mut BytesMut, + size: &mut u64, + buf: &mut Option, + ) -> Poll> { + use self::ChunkedState::*; + match *self { + Size => ChunkedState::read_size(body, size), + SizeLws => ChunkedState::read_size_lws(body), + Extension => ChunkedState::read_extension(body), + SizeLf => ChunkedState::read_size_lf(body, size), + Body => ChunkedState::read_body(body, size, buf), + BodyCr => ChunkedState::read_body_cr(body), + BodyLf => ChunkedState::read_body_lf(body), + EndCr => ChunkedState::read_end_cr(body), + EndLf => ChunkedState::read_end_lf(body), + End => Poll::Ready(Ok(ChunkedState::End)), + } + } + + fn read_size( + rdr: &mut BytesMut, + size: &mut u64, + ) -> Poll> { + let radix = 16; + + let rem = match byte!(rdr) { + b @ b'0'..=b'9' => b - b'0', + b @ b'a'..=b'f' => b + 10 - b'a', + b @ b'A'..=b'F' => b + 10 - b'A', + b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => return Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Invalid Size", + ))); + } + }; + + match size.checked_mul(radix) { + Some(n) => { + *size = n as u64; + *size += rem as u64; + + Poll::Ready(Ok(ChunkedState::Size)) + } + None => { + log::debug!("chunk size would overflow u64"); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Size is too big", + ))) + } + } + } + + fn read_size_lws(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + // LWS can follow the chunk size, but no more digits can come + b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size linear white space", + ))), + } + } + fn read_extension(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + // strictly 0x20 (space) should be disallowed but we don't parse quoted strings here + 0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid character in chunk extension", + ))), + _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions + } + } + fn read_size_lf( + rdr: &mut BytesMut, + size: &mut u64, + ) -> Poll> { + match byte!(rdr) { + b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), + b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size LF", + ))), + } + } + + fn read_body( + rdr: &mut BytesMut, + rem: &mut u64, + buf: &mut Option, + ) -> Poll> { + log::trace!("Chunked read, remaining={:?}", rem); + + let len = rdr.len() as u64; + if len == 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + let slice; + if *rem > len { + slice = rdr.split().freeze(); + *rem -= len; + } else { + slice = rdr.split_to(*rem as usize).freeze(); + *rem = 0; + } + *buf = Some(slice); + if *rem > 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + Poll::Ready(Ok(ChunkedState::BodyCr)) + } + } + } + + fn read_body_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body CR", + ))), + } + } + fn read_body_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::Size)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body LF", + ))), + } + } + fn read_end_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end CR", + ))), + } + } + fn read_end_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::End)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end LF", + ))), + } + } +} + +#[cfg(test)] +mod tests { + use actix_codec::Decoder as _; + use bytes::{Bytes, BytesMut}; + use http::Method; + + use crate::{ + error::ParseError, + h1::decoder::{MessageDecoder, PayloadItem}, + HttpMessage as _, Request, + }; + + macro_rules! parse_ready { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Ok(Some((msg, _))) => msg, + Ok(_) => unreachable!("Eof during parsing http request"), + Err(err) => unreachable!("Error during parsing http request: {:?}", err), + } + }}; + } + + macro_rules! expect_parse_err { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Err(err) => match err { + ParseError::Io(_) => unreachable!("Parse error expected"), + _ => {} + }, + _ => unreachable!("Error expected"), + } + }}; + } + + #[test] + fn test_parse_chunked_payload_chunk_extension() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(msg.chunked().unwrap()); + + buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"data")); + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"line")); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + } + + #[test] + fn test_request_chunked() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + if let Ok(val) = req.chunked() { + assert!(val); + } else { + unreachable!("Error"); + } + + // intentional typo in "chunked" + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chnked\r\n\r\n", + ); + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_chunked_payload() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"data" + ); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"line" + ); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn test_http_request_chunked_payload_and_next_message() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend( + b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ + POST /test2 HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n" + .iter(), + ); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"line"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(req.chunked().unwrap()); + assert_eq!(*req.method(), Method::POST); + assert!(req.chunked().unwrap()); + } + + #[test] + fn test_http_request_chunked_payload_chunks() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\n1111\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"1111"); + + buf.extend(b"4\r\ndata\r"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + + buf.extend(b"\n4"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + buf.extend(b"\n"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"li"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"li"); + + //trailers + //buf.feed_data("test: test\r\n"); + //not_ready!(reader.parse(&mut buf, &mut readbuf)); + + buf.extend(b"ne\r\n0\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"ne"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r\n"); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn chunk_extension_quoted() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;hello=b;one=\"1 2 3\"\r\n\ + xx", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"xx"))); + } + + #[test] + fn hrs_chunk_extension_invalid() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;x\nx\r\n\ + 4c\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid character in chunk extension")); + } + + #[test] + fn hrs_chunk_size_overflow() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + f0000000000000003\r\n\ + abc\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid chunk size line: Size is too big")); + } +} diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index f240710c2..313ffd5e0 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,18 +1,18 @@ -use std::convert::TryFrom; -use std::io; -use std::marker::PhantomData; -use std::task::Poll; +use std::{convert::TryFrom, io, marker::PhantomData, task::Poll}; use actix_codec::Decoder; -use bytes::{Buf, Bytes, BytesMut}; +use bytes::{Bytes, BytesMut}; use http::header::{HeaderName, HeaderValue}; use http::{header, Method, StatusCode, Uri, Version}; use log::{debug, error, trace}; -use crate::error::ParseError; -use crate::header::HeaderMap; -use crate::message::{ConnectionType, ResponseHead}; -use crate::request::Request; +use super::chunked::ChunkedState; +use crate::{ + error::ParseError, + header::HeaderMap, + message::{ConnectionType, ResponseHead}, + request::Request, +}; pub(crate) const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; @@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized { let mut has_upgrade_websocket = false; let mut expect = false; let mut chunked = false; + let mut seen_te = false; let mut content_length = None; { @@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized { }; match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { + header::CONTENT_LENGTH if content_length.is_some() => { + debug!("multiple Content-Length"); + return Err(ParseError::Header); + } + + header::CONTENT_LENGTH => match value.to_str() { + Ok(s) if s.trim().starts_with('+') => { + debug!("illegal Content-Length: {:?}", s); + return Err(ParseError::Header); + } + Ok(s) => { if let Ok(len) = s.parse::() { if len != 0 { content_length = Some(len); @@ -95,15 +105,31 @@ pub(crate) trait MessageType: Sized { debug!("illegal Content-Length: {:?}", s); return Err(ParseError::Header); } - } else { + } + Err(_) => { debug!("illegal Content-Length: {:?}", value); return Err(ParseError::Header); } - } + }, + // transfer-encoding + header::TRANSFER_ENCODING if seen_te => { + debug!("multiple Transfer-Encoding not allowed"); + return Err(ParseError::Header); + } + header::TRANSFER_ENCODING => { + seen_te = true; + if let Ok(s) = value.to_str().map(str::trim) { - chunked = s.eq_ignore_ascii_case("chunked"); + if s.eq_ignore_ascii_case("chunked") { + chunked = true; + } else if s.eq_ignore_ascii_case("identity") { + // allow silently since multiple TE headers are already checked + } else { + debug!("illegal Transfer-Encoding: {:?}", s); + return Err(ParseError::Header); + } } else { return Err(ParseError::Header); } @@ -408,20 +434,6 @@ enum Kind { Eof, } -#[derive(Debug, PartialEq, Clone)] -enum ChunkedState { - Size, - SizeLws, - Extension, - SizeLf, - Body, - BodyCr, - BodyLf, - EndCr, - EndLf, - End, -} - impl Decoder for PayloadDecoder { type Item = PayloadItem; type Error = io::Error; @@ -451,19 +463,23 @@ impl Decoder for PayloadDecoder { Kind::Chunked(ref mut state, ref mut size) => { loop { let mut buf = None; + // advances the chunked state *state = match state.step(src, size, &mut buf) { Poll::Pending => return Ok(None), Poll::Ready(Ok(state)) => state, Poll::Ready(Err(e)) => return Err(e), }; + if *state == ChunkedState::End { trace!("End of chunked stream"); return Ok(Some(PayloadItem::Eof)); } + if let Some(buf) = buf { return Ok(Some(PayloadItem::Chunk(buf))); } + if src.is_empty() { return Ok(None); } @@ -480,201 +496,40 @@ impl Decoder for PayloadDecoder { } } -macro_rules! byte ( - ($rdr:ident) => ({ - if $rdr.len() > 0 { - let b = $rdr[0]; - $rdr.advance(1); - b - } else { - return Poll::Pending - } - }) -); - -impl ChunkedState { - fn step( - &self, - body: &mut BytesMut, - size: &mut u64, - buf: &mut Option, - ) -> Poll> { - use self::ChunkedState::*; - match *self { - Size => ChunkedState::read_size(body, size), - SizeLws => ChunkedState::read_size_lws(body), - Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), - Body => ChunkedState::read_body(body, size, buf), - BodyCr => ChunkedState::read_body_cr(body), - BodyLf => ChunkedState::read_body_lf(body), - EndCr => ChunkedState::read_end_cr(body), - EndLf => ChunkedState::read_end_lf(body), - End => Poll::Ready(Ok(ChunkedState::End)), - } - } - - fn read_size( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - let radix = 16; - match byte!(rdr) { - b @ b'0'..=b'9' => { - *size *= radix; - *size += u64::from(b - b'0'); - } - b @ b'a'..=b'f' => { - *size *= radix; - *size += u64::from(b + 10 - b'a'); - } - b @ b'A'..=b'F' => { - *size *= radix; - *size += u64::from(b + 10 - b'A'); - } - b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => return Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size line: Invalid Size", - ))); - } - } - Poll::Ready(Ok(ChunkedState::Size)) - } - - fn read_size_lws(rdr: &mut BytesMut) -> Poll> { - trace!("read_size_lws"); - match byte!(rdr) { - // LWS can follow the chunk size, but no more digits can come - b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size linear white space", - ))), - } - } - fn read_extension(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions - } - } - fn read_size_lf( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - match byte!(rdr) { - b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), - b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size LF", - ))), - } - } - - fn read_body( - rdr: &mut BytesMut, - rem: &mut u64, - buf: &mut Option, - ) -> Poll> { - trace!("Chunked read, remaining={:?}", rem); - - let len = rdr.len() as u64; - if len == 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - let slice; - if *rem > len { - slice = rdr.split().freeze(); - *rem -= len; - } else { - slice = rdr.split_to(*rem as usize).freeze(); - *rem = 0; - } - *buf = Some(slice); - if *rem > 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - Poll::Ready(Ok(ChunkedState::BodyCr)) - } - } - } - - fn read_body_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body CR", - ))), - } - } - fn read_body_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::Size)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body LF", - ))), - } - } - fn read_end_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - ))), - } - } - fn read_end_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::End)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end LF", - ))), - } - } -} - #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; use http::{Method, Version}; use super::*; - use crate::error::ParseError; - use crate::http::header::{HeaderName, SET_COOKIE}; - use crate::HttpMessage; + use crate::{ + error::ParseError, + http::header::{HeaderName, SET_COOKIE}, + HttpMessage as _, + }; impl PayloadType { - fn unwrap(self) -> PayloadDecoder { + pub(crate) fn unwrap(self) -> PayloadDecoder { match self { PayloadType::Payload(pl) => pl, _ => panic!(), } } - fn is_unhandled(&self) -> bool { + pub(crate) fn is_unhandled(&self) -> bool { matches!(self, PayloadType::Stream(_)) } } impl PayloadItem { - fn chunk(self) -> Bytes { + pub(crate) fn chunk(self) -> Bytes { match self { PayloadItem::Chunk(chunk) => chunk, _ => panic!("error"), } } - fn eof(&self) -> bool { + + pub(crate) fn eof(&self) -> bool { matches!(*self, PayloadItem::Eof) } } @@ -967,34 +822,6 @@ mod tests { assert!(req.upgrade()); } - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // intentional typo in "chunked" - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - #[test] fn test_headers_content_length_err_1() { let mut buf = BytesMut::from( @@ -1112,126 +939,6 @@ mod tests { expect_parse_err!(&mut buf); } - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"data" - ); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"line" - ); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - - let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); - assert!(req.chunked().unwrap()); - assert_eq!(*req.method(), Method::POST); - assert!(req.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - buf.extend(b"\n"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"li"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\ - \r\n", - ); - - let mut reader = MessageDecoder::::default(); - let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(msg.chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - } - #[test] fn test_response_http10_read_until_eof() { let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data"); @@ -1243,4 +950,84 @@ mod tests { let chunk = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data"))); } + + #[test] + fn hrs_multiple_content_length() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 4\r\n\ + Content-Length: 2\r\n\ + \r\n\ + abcd", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_content_length_plus() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: +3\r\n\ + \r\n\ + 000", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_unknown_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: JUNK\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_multiple_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 51\r\n\ + Transfer-Encoding: identity\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 0\r\n\ + \r\n\ + GET /forbidden HTTP/1.1\r\n\ + Host: example.com\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn transfer_encoding_agrees() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: identity\r\n\ + \r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n"))); + } } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 254981123..4e5c9d238 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -81,6 +81,7 @@ pub(crate) trait MessageType: Sized { match length { BodySize::Stream => { if chunked { + skip_len = true; if camel_case { dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") } else { diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 7e6df6ceb..17cbfb90f 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -1,6 +1,8 @@ //! HTTP/1 protocol implementation. + use bytes::{Bytes, BytesMut}; +mod chunked; mod client; mod codec; mod decoder; From 384164cc148e4bf31a8ff3ddffd1139e64a1c15f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 6 Aug 2021 20:10:58 +0100 Subject: [PATCH 3/8] update graphs --- docs/graphs/net-only.dot | 3 +-- docs/graphs/web-focus.dot | 3 ++- docs/graphs/web-only.dot | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot index bee0185ab..8a58ec2b8 100644 --- a/docs/graphs/net-only.dot +++ b/docs/graphs/net-only.dot @@ -4,7 +4,7 @@ digraph { subgraph cluster_net { label="actix-net" "actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service" - "actix-tls" "actix-tracing" "actix-utils" "actix-router" + "actix-tls" "actix-tracing" "actix-utils" } subgraph cluster_other { @@ -25,7 +25,6 @@ digraph { "actix-tls" -> { "tokio-util" }[color="#009900"] "actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" } "actix-rt" -> { "actix-macros" "tokio" } - "actix-router" -> { "bytestring" } "local-channel" -> { "local-waker" } diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index 2c6e2779b..63b3eaa82 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -10,6 +10,7 @@ digraph { "web-actors" "web-codegen" "http-test" + "router" { rank=same; "multipart" "web-actors" "http-test" }; { rank=same; "files" "awc" "web" }; @@ -36,7 +37,7 @@ digraph { "rt" -> { "macros" } { rank=same; "utils" "codec" }; - { rank=same; "rt" "macros" "service" "router" }; + { rank=same; "rt" "macros" "service" }; // actix diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index b0decd818..ee74c292b 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -10,9 +10,10 @@ digraph { "actix-web-codegen" "actix-http-test" "actix-test" + "actix-router" } - "actix-web" -> { "actix-web-codegen" "actix-http" } + "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } "awc" -> { "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-multipart" -> { "actix-web" } From a0c0bff944febe1d984aedc4866acee1bed95bdd Mon Sep 17 00:00:00 2001 From: Thales <46510852+thalesfragoso@users.noreply.github.com> Date: Fri, 13 Aug 2021 14:41:19 -0300 Subject: [PATCH 4/8] Don't create a slice to potential uninit data on h1 encoder (#2364) Co-authored-by: Rob Ede --- actix-http/CHANGES.md | 4 ++++ actix-http/benches/write-camel-case.rs | 10 +++++++--- actix-http/src/h1/encoder.rs | 15 +++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f52f5ba68..9ed28105f 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased - 2021-xx-xx +### Fixed +* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] + +[#2364]: https://github.com/actix/actix-web/pull/2364 ## 3.0.0-beta.8 - 2021-08-09 ### Fixed diff --git a/actix-http/benches/write-camel-case.rs b/actix-http/benches/write-camel-case.rs index fa4930eb9..ccf09b37e 100644 --- a/actix-http/benches/write-camel-case.rs +++ b/actix-http/benches/write-camel-case.rs @@ -18,7 +18,8 @@ fn bench_write_camel_case(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| { b.iter(|| { let mut buf = black_box([0; 24]); - _new::write_camel_case(black_box(bts), &mut buf) + let len = black_box(bts.len()); + _new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len) }); }); } @@ -30,9 +31,12 @@ criterion_group!(benches, bench_write_camel_case); criterion_main!(benches); mod _new { - pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) { + pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + let buffer = unsafe { + std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len); + std::slice::from_raw_parts_mut(buf, len) + }; let mut iter = value.iter(); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 4e5c9d238..5e1d47785 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -175,7 +175,7 @@ pub(crate) trait MessageType: Sized { unsafe { if camel_case { // use Camel-Case headers - write_camel_case(k, from_raw_parts_mut(buf, k_len)); + write_camel_case(k, buf, k_len); } else { write_data(k, buf, k_len); } @@ -473,15 +473,22 @@ impl TransferEncoding { } /// # Safety -/// Callers must ensure that the given length matches given value length. +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { debug_assert_eq!(value.len(), len); copy_nonoverlapping(value.as_ptr(), buf, len); } -fn write_camel_case(value: &[u8], buffer: &mut [u8]) { +/// # Safety +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. +unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + write_data(value, buf, len); + + // SAFETY: We just initialized the buffer with `value` + let buffer = from_raw_parts_mut(buf, len); let mut iter = value.iter(); From 5f412c67db4c65dba51942bd098b58acc8fae035 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 13 Aug 2021 18:49:58 +0100 Subject: [PATCH 5/8] clippy --- actix-files/src/error.rs | 1 + actix-http/src/header/map.rs | 2 +- actix-http/src/lib.rs | 2 +- actix-http/src/message.rs | 4 ++-- actix-router/src/path.rs | 6 +++--- actix-router/src/resource.rs | 6 +++--- src/http/header/content_disposition.rs | 2 +- src/middleware/logger.rs | 2 +- src/request.rs | 4 ++-- src/service.rs | 2 +- src/types/either.rs | 4 ++-- src/types/json.rs | 2 +- 12 files changed, 19 insertions(+), 18 deletions(-) diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index e5f2d4779..f8e32eef7 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -21,6 +21,7 @@ impl ResponseError for FilesError { } } +#[allow(clippy::enum_variant_names)] #[derive(Display, Debug, PartialEq)] pub enum UriSegmentError { /// The segment started with the wrapped invalid character. diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 634d9282f..a8fd9715b 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -684,7 +684,7 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option { // handle in-progress multi value lists first - if let Some((ref name, ref mut vals)) = self.multi_inner { + if let Some((name, ref mut vals)) = self.multi_inner { match vals.get(self.multi_idx) { Some(val) => { self.multi_idx += 1; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index d22e1ee44..17ee3ff29 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -14,7 +14,7 @@ //! [rustls]: https://crates.io/crates/rustls //! [trust-dns]: https://crates.io/crates/trust-dns -#![deny(rust_2018_idioms, nonstandard_style)] +#![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)] #![allow( clippy::type_complexity, clippy::too_many_arguments, diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index e85d686b7..84125fb3a 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -209,7 +209,7 @@ impl RequestHeadType { impl AsRef for RequestHeadType { fn as_ref(&self) -> &RequestHead { match self { - RequestHeadType::Owned(head) => &head, + RequestHeadType::Owned(head) => head, RequestHeadType::Rc(head, _) => head.as_ref(), } } @@ -363,7 +363,7 @@ impl std::ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { - &self.head.as_ref() + self.head.as_ref() } } diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index e29591f96..9af7b0b8b 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -125,7 +125,7 @@ impl Path { for (seg_name, val) in self.segments.iter() { if name == seg_name { return match val { - PathItem::Static(ref s) => Some(&s), + PathItem::Static(ref s) => Some(s), PathItem::Segment(s, e) => { Some(&self.path.path()[(*s as usize)..(*e as usize)]) } @@ -183,7 +183,7 @@ impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { if self.idx < self.params.segment_count() { let idx = self.idx; let res = match self.params.segments[idx].1 { - PathItem::Static(ref s) => &s, + PathItem::Static(ref s) => s, PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], }; self.idx += 1; @@ -207,7 +207,7 @@ impl Index for Path { fn index(&self, idx: usize) -> &str { match self.segments[idx].1 { - PathItem::Static(ref s) => &s, + PathItem::Static(ref s) => s, PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], } } diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 61ff587a5..69e10b2bd 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -276,7 +276,7 @@ impl ResourceDef { let mut pattern_data = Vec::new(); for pattern in &patterns { - match ResourceDef::parse(&pattern, false, true) { + match ResourceDef::parse(pattern, false, true) { (PatternType::Dynamic(re, names), _) => { re_set.push(re.as_str().to_owned()); pattern_data.push((re, names)); @@ -790,7 +790,7 @@ impl ResourceDef { profile_section!(pattern_dynamic_extract_captures); for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { + if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { log::error!( @@ -820,7 +820,7 @@ impl ResourceDef { }; for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { + if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { log::error!("Dynamic path match but not all segments found: {}", name); diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 9f67baffb..6e75fde92 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -457,7 +457,7 @@ impl Header for ContentDisposition { fn parse(msg: &T) -> Result { if let Some(h) = msg.headers().get(&Self::name()) { - Self::from_raw(&h) + Self::from_raw(h) } else { Err(crate::error::ParseError::Header) } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index bbb0e3dc4..0f09b6ad6 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -553,7 +553,7 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(ref peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().remote_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) diff --git a/src/request.rs b/src/request.rs index 41c8252a8..59850b4ca 100644 --- a/src/request.rs +++ b/src/request.rs @@ -184,7 +184,7 @@ impl HttpRequest { U: IntoIterator, I: AsRef, { - self.resource_map().url_for(&self, name, elements) + self.resource_map().url_for(self, name, elements) } /// Generate url for named resource @@ -199,7 +199,7 @@ impl HttpRequest { #[inline] /// Get a reference to a `ResourceMap` of current application. pub fn resource_map(&self) -> &ResourceMap { - &self.app_state().rmap() + self.app_state().rmap() } /// Peer socket address. diff --git a/src/service.rs b/src/service.rs index 148199407..48167e5b3 100644 --- a/src/service.rs +++ b/src/service.rs @@ -117,7 +117,7 @@ impl ServiceRequest { /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.req.head() + self.req.head() } /// This method returns reference to the request head diff --git a/src/types/either.rs b/src/types/either.rs index d3b003587..35e63cec9 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -253,7 +253,7 @@ where Ok(bytes) => { let fallback = bytes.clone(); let left = - L::from_request(&this.req, &mut payload_from_bytes(bytes)); + L::from_request(this.req, &mut payload_from_bytes(bytes)); EitherExtractState::Left { left, fallback } } Err(err) => break Err(EitherExtractError::Bytes(err)), @@ -265,7 +265,7 @@ where Ok(extracted) => break Ok(Either::Left(extracted)), Err(left_err) => { let right = R::from_request( - &this.req, + this.req, &mut payload_from_bytes(mem::take(fallback)), ); EitherExtractState::Right { diff --git a/src/types/json.rs b/src/types/json.rs index fc02c8854..ab9708c53 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -425,7 +425,7 @@ where } } None => { - let json = serde_json::from_slice::(&buf) + let json = serde_json::from_slice::(buf) .map_err(JsonPayloadError::Deserialize)?; return Poll::Ready(Ok(json)); } From ff07816b650997b0050811c1fd300c0da1104b59 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Sun, 29 Aug 2021 08:42:22 +0800 Subject: [PATCH 6/8] update httparse for uninit header parsing (#2374) --- actix-http/Cargo.toml | 2 +- actix-http/src/h1/decoder.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 4ce55dca1..68f980982 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -59,7 +59,7 @@ futures-core = { version = "0.3.7", default-features = false, features = ["alloc futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } h2 = "0.3.1" http = "0.2.2" -httparse = "1.3" +httparse = "1.5.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 313ffd5e0..91a3af44f 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, io, marker::PhantomData, task::Poll}; +use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll}; use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; @@ -212,10 +212,17 @@ impl MessageType for Request { let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + // SAFETY: + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is + // safe because the type we are claiming to have initialized here is a + // bunch of `MaybeUninit`s, which do not require initialization. + let mut parsed = unsafe { + MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() + .assume_init() + }; - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src)? { + let mut req = httparse::Request::new(&mut []); + match req.parse_with_uninit_headers(src, &mut parsed)? { httparse::Status::Complete(len) => { let method = Method::from_bytes(req.method.unwrap().as_bytes()) .map_err(|_| ParseError::Method)?; From f9da6e48e0aef496001528daa68298ff9107a895 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Mon, 30 Aug 2021 22:05:49 +0300 Subject: [PATCH 7/8] ResourceDef: define behavior for prefix with trailing slash (#2355) * ResourceDef: define behavior * fix tests * add scope test * revert firestorm bump * update changelog * fmt Co-authored-by: Rob Ede --- actix-files/src/files.rs | 2 +- actix-router/CHANGES.md | 3 + actix-router/src/resource.rs | 169 +++++++++++++++++++---------------- src/scope.rs | 66 ++++++++++++++ 4 files changed, 163 insertions(+), 77 deletions(-) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 49d81eb03..68879822a 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -106,7 +106,7 @@ impl Files { }; Files { - path: mount_path.to_owned(), + path: mount_path.trim_end_matches('/').to_owned(), directory: dir, index: None, show_index: false, diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index dea7cb76f..140d108e2 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -5,11 +5,14 @@ * Disallow prefix routes with tail segments. [#379] * Enforce path separators on dynamic prefixes. [#378] * Improve malformed path error message. [#384] +* Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] +* Prefix segments with trailing slashes define a trailing empty segment. [#2355] [#378]: https://github.com/actix/actix-net/pull/378 [#379]: https://github.com/actix/actix-net/pull/379 [#380]: https://github.com/actix/actix-net/pull/380 [#384]: https://github.com/actix/actix-net/pull/384 +[#2355]: https://github.com/actix/actix-web/pull/2355 ## 0.5.0-beta.1 - 2021-07-20 diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 69e10b2bd..fbf29cc7a 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -28,9 +28,27 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// regex engine. /// /// +/// # Pattern Format and Matching Behavior +/// +/// Resource pattern is defined as a string of zero or more _segments_ where each segment is +/// preceeded by a slash `/`. +/// +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). +/// This also implies that a trailing slash in pattern defines an empty segment. +/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// +/// A key point to undertand is that `ResourceDef` matches segments, not strings. +/// It matches segments individually. +/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, +/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// +/// This definition is consistent with the definition of absolute URL path in +/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// +/// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a regular string to -/// [new][Self::new]. Conforming paths must match the string exactly. +/// A static resource is the most basic type of definition. Pass a pattern to +/// [new][Self::new]. Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -39,6 +57,7 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// assert!(resource.is_match("/home")); /// +/// assert!(!resource.is_match("/home/")); /// assert!(!resource.is_match("/home/new")); /// assert!(!resource.is_match("/homes")); /// assert!(!resource.is_match("/search")); @@ -85,12 +104,13 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Prefix Resources -/// A prefix resource is defined as pattern that can match just the start of a path. +/// A prefix resource is defined as pattern that can match just the start of a path, up to a +/// segment boundary. /// -/// This library chooses to restrict that definition slightly. In particular, when matching, the -/// prefix must be separated from the remaining part of the path by a `/` character, either at the -/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not -/// much of a limitation. +/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. +/// They define and therefore require an empty segment in order to match. Examples are given below. +/// +/// Empty pattern matches any path as a prefix. /// /// Prefix resources can contain dynamic segments. /// @@ -102,9 +122,12 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(resource.is_match("/home/new")); /// assert!(!resource.is_match("/homes")); /// +/// // prefix pattern with a trailing slash /// let resource = ResourceDef::prefix("/user/{id}/"); /// assert!(resource.is_match("/user/123/")); -/// assert!(resource.is_match("/user/123/stars")); +/// assert!(resource.is_match("/user/123//stars")); +/// assert!(!resource.is_match("/user/123/stars")); +/// assert!(!resource.is_match("/user/123")); /// ``` /// /// @@ -117,6 +140,10 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID /// is numeric. /// +/// The regex could potentially match multiple segments. If this is not wanted, then care must be +/// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a +/// segment boundary; the pattern `r"(/|$)` is always appended to the regex. +/// /// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in /// the earlier section, that segments capture a slice of the path up to the next `/` character. /// @@ -298,7 +325,7 @@ impl ResourceDef { } } - /// Constructs a new resource definition using a string pattern that performs prefix matching. + /// Constructs a new resource definition using a pattern that performs prefix matching. /// /// More specifically, the regular expressions generated for matching are different when using /// this method vs using `new`; they will not be appended with the `$` meta-character that @@ -320,13 +347,6 @@ impl ResourceDef { /// assert!(!resource.is_match("user/123")); /// assert!(!resource.is_match("user/123/stars")); /// assert!(!resource.is_match("/foo")); - /// - /// let resource = ResourceDef::prefix("user/{id}"); - /// assert!(resource.is_match("user/123")); - /// assert!(resource.is_match("user/123/stars")); - /// assert!(!resource.is_match("/user/123")); - /// assert!(!resource.is_match("/user/123/stars")); - /// assert!(!resource.is_match("foo")); /// ``` pub fn prefix(path: &str) -> Self { profile_method!(prefix); @@ -591,24 +611,7 @@ impl ResourceDef { match self.pat_type { PatternType::Static(ref s) => s == path, - - PatternType::Prefix(ref prefix) if prefix == path => true, - PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path), - - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => true, - - // prefix matches part - Some(m) => is_strict_prefix(m.as_str(), path), - - // prefix does not match - None => false, - } - } - + PatternType::Prefix(ref prefix) => is_prefix(prefix, path), PatternType::Dynamic(ref re, _) => re.is_match(path), PatternType::DynamicSet(ref re, _) => re.is_match(path), } @@ -656,30 +659,15 @@ impl ResourceDef { PatternType::Static(segment) if path == segment => Some(segment.len()), PatternType::Static(_) => None, - PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), - PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()), + PatternType::Prefix(prefix) if is_prefix(prefix, path) => Some(prefix.len()), PatternType::Prefix(_) => None, - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => Some(m.end()), - - // prefix matches part - Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()), - - // prefix does not match - _ => None, - } - } - - PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()), + PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), PatternType::DynamicSet(re, params) => { let idx = re.matches(path).into_iter().next()?; let (ref pattern, _) = params[idx]; - pattern.find(path).map(|m| m.end()) + Some(pattern.captures(path)?[1].len()) } } } @@ -802,7 +790,7 @@ impl ResourceDef { } }; - (captures[0].len(), Some(names)) + (captures[1].len(), Some(names)) } PatternType::DynamicSet(re, params) => { @@ -828,7 +816,7 @@ impl ResourceDef { } } - (captures[0].len(), Some(names)) + (captures[1].len(), Some(names)) } }; @@ -1112,8 +1100,16 @@ impl ResourceDef { ); } - if !is_prefix && !has_tail_segment { - re.push('$'); + // Store the pattern in capture group #1 to have context info outside it + let mut re = format!("({})", re); + + // Ensure the match ends at a segment boundary + if !has_tail_segment { + if is_prefix { + re.push_str(r"(/|$)"); + } else { + re.push('$'); + } } let re = match Regex::new(&re) { @@ -1185,10 +1181,12 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { } /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. -/// -/// The `strict` refers to the fact that this will return `false` if `prefix == path`. -fn is_strict_prefix(prefix: &str, path: &str) -> bool { - path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/')) +fn is_prefix(prefix: &str, path: &str) -> bool { + match path.strip_prefix(prefix) { + // Ensure the match ends at segment boundary + Some(rem) if rem.is_empty() || rem.starts_with('/') => true, + _ => false, + } } #[cfg(test)] @@ -1501,54 +1499,70 @@ mod tests { let re = ResourceDef::prefix("/name/"); assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); + assert!(!re.capture_match_info(&mut path)); + + let mut path = Path::new("/name//gs"); assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); + assert_eq!(path.unprocessed(), "/gs"); let re = ResourceDef::root_prefix("name/"); assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); + assert!(!re.capture_match_info(&mut path)); } #[test] fn prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); + let re = ResourceDef::prefix("/{name}"); assert!(re.is_prefix()); assert!(re.is_match("/name/")); assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); + assert!(re.is_match("/name")); - assert_eq!(re.find_match("/name/"), Some(6)); - assert_eq!(re.find_match("/name/gs"), Some(6)); - assert_eq!(re.find_match("/name"), None); + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/gs"), Some(5)); + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match(""), None); let mut path = Path::new("/test2/"); assert!(re.capture_match_info(&mut path)); assert_eq!(&path["name"], "test2"); assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), ""); + assert_eq!(path.unprocessed(), "/"); let mut path = Path::new("/test2/subpath1/subpath2/index.html"); assert!(re.capture_match_info(&mut path)); assert_eq!(&path["name"], "test2"); assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); + assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html"); let resource = ResourceDef::prefix("/user"); // input string shorter than prefix assert!(resource.find_match("/foo").is_none()); } + #[test] + fn prefix_empty() { + let re = ResourceDef::prefix(""); + + assert!(re.is_prefix()); + + assert!(re.is_match("")); + assert!(re.is_match("/")); + assert!(re.is_match("/name/test/test")); + } + #[test] fn build_path_list() { let mut s = String::new(); @@ -1667,14 +1681,17 @@ mod tests { } #[test] - fn consistent_match_length() { - let result = Some(5); + fn prefix_trailing_slash() { + // The prefix "/abc/" matches two segments: ["user", ""] + // These are not prefixes let re = ResourceDef::prefix("/abc/"); - assert_eq!(re.find_match("/abc/def"), result); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); let re = ResourceDef::prefix("/{id}/"); - assert_eq!(re.find_match("/abc/def"), result); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); } #[test] diff --git a/src/scope.rs b/src/scope.rs index 97db53eeb..b2edaedab 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1153,4 +1153,70 @@ mod tests { Bytes::from_static(b"http://localhost:8080/a/b/c/12345") ); } + + #[actix_rt::test] + async fn dynamic_scopes() { + let srv = init_service( + App::new().service( + web::scope("/{a}/").service( + web::scope("/{b}/") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + // note the unintuitive behavior with trailing slashes on scopes with dynamic segments + let req = TestRequest::with_uri("/a//b//c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a//b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a//b//").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a//b//c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let srv = init_service( + App::new().service( + web::scope("/{a}").service( + web::scope("/{b}") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + let req = TestRequest::with_uri("/a/b/c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a/b").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a/b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a/b/c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } } From 4bb32fb19b5fb4105e7ca2b7371557d0d21b0346 Mon Sep 17 00:00:00 2001 From: Sam De Roeck <31270289+sadroeck@users.noreply.github.com> Date: Mon, 30 Aug 2021 21:07:12 +0200 Subject: [PATCH 8/8] [fix] Bump actix-http dependency to 3.0.0-beta.9, up from 3.0.0-beta.8 (#2360) Fixes https://rustsec.org/advisories/RUSTSEC-2021-0081 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ff3321f47..f2ce46ee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.2" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.9" ahash = "0.7" bytes = "1"