From f6e69919ede4872c1d987b4b932c44580190971c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 6 Aug 2021 22:42:31 +0100 Subject: [PATCH 01/99] 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 7556bd8d..ff3321f4 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 00000000..dea7cb76 --- /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 00000000..2a2ce1cc --- /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 00000000..965b606f --- /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 00000000..76219eb7 --- /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 00000000..a428b9f1 --- /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 00000000..798cc22d --- /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 00000000..775c48b8 --- /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 00000000..463e59e4 --- /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 00000000..e29591f9 --- /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 00000000..61ff587a --- /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 00000000..f5deb858 --- /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 00000000..e08a7171 --- /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 5cff2056..da5b45f3 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 3c1b7847..ce52543b 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 b072ace1..9e77c0f9 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 b8d95efb..0817d902 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 4b950e75..41c8252a 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 20d1ee17..851ce0fc 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 3c8805d5..0ee4de47 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 aa546c42..97db53ee 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 47e7e4ac..14819940 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 f2273a59..4052646e 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 40ac4627..108ff314 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 02/99] 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 8ead4371..f52f5ba6 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 a12fed4b..4ce55dca 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 de1ef0a9..5b06583b 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 54666e07..f7d7f696 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 00000000..1224ce08 --- /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 f240710c..313ffd5e 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 25498112..4e5c9d23 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 7e6df6ce..17cbfb90 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 03/99] 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 bee0185a..8a58ec2b 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 2c6e2779..63b3eaa8 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 b0decd81..ee74c292 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 04/99] 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 f52f5ba6..9ed28105 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 fa4930eb..ccf09b37 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 4e5c9d23..5e1d4778 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 05/99] 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 e5f2d477..f8e32eef 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 634d9282..a8fd9715 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 d22e1ee4..17ee3ff2 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 e85d686b..84125fb3 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 e29591f9..9af7b0b8 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 61ff587a..69e10b2b 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 9f67baff..6e75fde9 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 bbb0e3dc..0f09b6ad 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 41c8252a..59850b4c 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 14819940..48167e5b 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 d3b00358..35e63cec 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 fc02c885..ab9708c5 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 06/99] 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 4ce55dca..68f98098 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 313ffd5e..91a3af44 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 07/99] 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 49d81eb0..68879822 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 dea7cb76..140d108e 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 69e10b2b..fbf29cc7 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 97db53ee..b2edaeda 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 08/99] [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 ff3321f4..f2ce46ee 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" From 168b2f227d1959252dc47518641da7d214a81ed3 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 31 Aug 2021 02:20:40 +0530 Subject: [PATCH 09/99] compile time validation of path (#2350) * compile time validation of path * added trybuild err message * Update Cargo.toml * add changelog entry * test more cases of path validation * fmt Co-authored-by: Rob Ede --- actix-router/src/resource.rs | 5 ++- actix-web-codegen/CHANGES.md | 3 ++ actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/src/route.rs | 2 + actix-web-codegen/tests/trybuild.rs | 1 + .../trybuild/route-malformed-path-fail.rs | 33 +++++++++++++++ .../trybuild/route-malformed-path-fail.stderr | 42 +++++++++++++++++++ 7 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index fbf29cc7..57ce3680 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -967,7 +967,10 @@ impl ResourceDef { _ => false, }) .unwrap_or_else(|| { - panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) + panic!( + r#"pattern "{}" contains malformed dynamic segment"#, + pattern + ) }); let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index a8a901f7..4fd393b4 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* In routing macros, paths are now validated at compile time. [#2350] + +[#2350]: https://github.com/actix/actix-web/pull/2350 ## 0.5.0-beta.3 - 2021-06-17 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 4d0fd5e2..66f7acf6 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" +actix-router = "0.5.0-beta.1" [dev-dependencies] actix-rt = "2.2" diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 74704252..c2f851a0 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,6 +3,7 @@ extern crate proc_macro; use std::collections::HashSet; use std::convert::TryFrom; +use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; @@ -101,6 +102,7 @@ impl Args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { None => { + let _ = ResourceDef::new(lit.value()); path = Some(lit); } _ => { diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 12e848cf..c97211e9 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -10,6 +10,7 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.pass("tests/trybuild/docstring-ok.rs"); } diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs new file mode 100644 index 00000000..1258a6f2 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs @@ -0,0 +1,33 @@ +use actix_web_codegen::get; + +#[get("/{")] +async fn zero() -> &'static str { + "malformed resource def" +} + +#[get("/{foo")] +async fn one() -> &'static str { + "malformed resource def" +} + +#[get("/{}")] +async fn two() -> &'static str { + "malformed resource def" +} + +#[get("/*")] +async fn three() -> &'static str { + "malformed resource def" +} + +#[get("/{tail:\\d+}*")] +async fn four() -> &'static str { + "malformed resource def" +} + +#[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] +async fn five() -> &'static str { + "malformed resource def" +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr new file mode 100644 index 00000000..93c51010 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr @@ -0,0 +1,42 @@ +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:3:1 + | +3 | #[get("/{")] + | ^^^^^^^^^^^^ + | + = help: message: pattern "{" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:8:1 + | +8 | #[get("/{foo")] + | ^^^^^^^^^^^^^^^ + | + = help: message: pattern "{foo" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:13:1 + | +13 | #[get("/{}")] + | ^^^^^^^^^^^^^ + | + = help: message: Wrong path pattern: "/{}" regex parse error: + ((?s-m)^/(?P<>[^/]+))$ + ^ + error: empty capture group name + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:23:1 + | +23 | #[get("/{tail:\\d+}*")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: custom regex is not supported for tail match + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:28:1 + | +28 | #[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: Only 16 dynamic segments are allowed, provided: 17 From 5128b1bdfc0c47fc744f2bc1f417ef5fd0e7f3c1 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 30 Aug 2021 23:19:03 +0100 Subject: [PATCH 10/99] bump msrv to 1.51 --- .github/workflows/ci.yml | 2 +- CHANGES.md | 3 +++ README.md | 4 ++-- actix-files/CHANGES.md | 1 + actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 1 + actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 3 +++ actix-http/README.md | 4 ++-- actix-http/src/body/mod.rs | 2 +- actix-http/src/h1/chunked.rs | 8 ++++---- actix-http/src/h1/dispatcher.rs | 2 +- actix-multipart/CHANGES.md | 1 + actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 1 + actix-test/CHANGES.md | 1 + actix-web-actors/CHANGES.md | 1 + actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 1 + actix-web-codegen/README.md | 4 ++-- actix-web-codegen/tests/trybuild.rs | 2 +- awc/README.md | 2 +- clippy.toml | 2 +- src/lib.rs | 2 +- src/responder.rs | 6 +++--- src/types/query.rs | 4 ++-- 26 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22b92759..221d2fb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.46.0 # MSRV + - 1.51.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index 88295ec1..5325caf4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] +### Changes +* Minimum supported Rust version (MSRV) is now 1.51. + [#2325]: https://github.com/actix/actix-web/pull/2325 diff --git a/README.md b/README.md index 309a1846..33784d66 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8)
@@ -32,7 +32,7 @@ * SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Includes an async [HTTP client](https://docs.rs/awc/) -* Runs on stable Rust 1.46+ +* Runs on stable Rust 1.51+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index db047c44..533f7229 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.6.0-beta.6 - 2021-06-26 diff --git a/actix-files/README.md b/actix-files/README.md index 13c301c5..5815ef56 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- Minimum supported Rust version: 1.46 or later +- Minimum supported Rust version: 1.51 or later diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 1dbd9a15..39b6a3a6 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 3.0.0-beta.4 - 2021-04-02 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 74260a35..099fb385 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9ed28105..57c09d2d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,12 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx +### Changes +* Minimum supported Rust version (MSRV) is now 1.51. ### 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 * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) diff --git a/actix-http/README.md b/actix-http/README.md index 5b06583b..c509eaff 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -4,7 +4,7 @@ [![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.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) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.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.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 ## Example diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 8a08dbd2..a60a8895 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -80,7 +80,7 @@ mod tests { impl Body { pub(crate) fn get_ref(&self) -> &[u8] { match *self { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => panic!(), } } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index 1224ce08..e5b734ff 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -40,7 +40,7 @@ impl ChunkedState { Size => ChunkedState::read_size(body, size), SizeLws => ChunkedState::read_size_lws(body), Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), + 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), @@ -113,11 +113,11 @@ impl ChunkedState { } fn read_size_lf( rdr: &mut BytesMut, - size: &mut u64, + size: 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)), + 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", diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index deb25763..aef765b8 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1060,7 +1060,7 @@ mod tests { fn stabilize_date_header(payload: &mut [u8]) { let mut from = 0; - while let Some(pos) = find_slice(&payload, b"date", from) { + while let Some(pos) = find_slice(payload, b"date", from) { payload[(from + pos)..(from + pos + 35)] .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); from += 35; diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 0b6affa3..1e768ddf 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.4.0-beta.5 - 2021-06-17 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 78855b81..aed16721 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 140d108e..804f7778 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -7,6 +7,7 @@ * 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] +* Minimum supported Rust version (MSRV) is now 1.51. [#378]: https://github.com/actix/actix-net/pull/378 [#379]: https://github.com/actix/actix-net/pull/379 diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index fa554ba2..dc76ba3f 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.1.0-beta.3 - 2021-06-20 diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index bf642ef9..084e7b27 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 4.0.0-beta.6 - 2021-06-26 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 5f8f78bd..2858d3f2 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum supported Rust version: 1.46 or later +- Minimum supported Rust version: 1.51 or later diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 4fd393b4..f0a56b30 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx * In routing macros, paths are now validated at compile time. [#2350] +* Minimum supported Rust version (MSRV) is now 1.51. [#2350]: https://github.com/actix/actix-web/pull/2350 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 96e4cb51..e69cfbbe 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum supported Rust version: 1.46 or later. +- Minimum supported Rust version: 1.51 or later. ## Compile Testing diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index c97211e9..54bc1cae 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.46)] // MSRV +#[rustversion::stable(1.51)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/awc/README.md b/awc/README.md index dd08c6e1..fe91383c 100644 --- a/awc/README.md +++ b/awc/README.md @@ -12,7 +12,7 @@ - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 ## Example diff --git a/clippy.toml b/clippy.toml index eb66960a..829dd1c5 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.46" +msrv = "1.51" diff --git a/src/lib.rs b/src/lib.rs index 714c759c..e7cf4636 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.46+ +//! * Runs on stable Rust 1.51+ //! //! # Crate Features //! * `cookies` - cookies support (enabled by default) diff --git a/src/responder.rs b/src/responder.rs index c5852a50..005bff03 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -270,7 +270,7 @@ pub(crate) mod tests { impl BodyTest for Body { fn bin_ref(&self) -> &[u8] { match self { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } @@ -283,11 +283,11 @@ pub(crate) mod tests { fn bin_ref(&self) -> &[u8] { match self { ResponseBody::Body(ref b) => match b { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), }, ResponseBody::Other(ref b) => match b { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), }, } diff --git a/src/types/query.rs b/src/types/query.rs index 8762547e..1e6f1111 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -213,10 +213,10 @@ mod tests { #[actix_rt::test] async fn test_service_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); - assert!(Query::::from_query(&req.query_string()).is_err()); + assert!(Query::::from_query(req.query_string()).is_err()); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - let mut s = Query::::from_query(&req.query_string()).unwrap(); + let mut s = Query::::from_query(req.query_string()).unwrap(); assert_eq!(s.id, "test"); assert_eq!( From ae35e69382805164704d8d7c79f41c85089b3d36 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 02:52:29 +0100 Subject: [PATCH 11/99] use rust 1.51 features --- Cargo.toml | 1 + actix-http/src/body/body.rs | 24 +++++++++--------------- actix-http/src/body/response_body.rs | 9 ++------- actix-http/src/encoding/encoder.rs | 13 ++----------- actix-http/src/h1/utils.rs | 5 +---- src/middleware/logger.rs | 1 - 6 files changed, 15 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2ce46ee..cee40136 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ name = "actix_web" path = "src/lib.rs" [workspace] +resolver = "2" members = [ ".", "awc", diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index f04837d0..cd3e4c5c 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -7,7 +7,7 @@ use std::{ }; use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; +use futures_core::Stream; use crate::error::Error; @@ -74,14 +74,10 @@ impl MessageBody for AnyBody { } } - // TODO: MSRV 1.51: poll_map_err - AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => { - Poll::Ready(Some(Err(Error::new_body().with_cause(err)))) - } - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + AnyBody::Message(body) => body + .as_pin_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), } } } @@ -223,11 +219,9 @@ impl MessageBody for BoxAnyBody { mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - // TODO: MSRV 1.51: poll_map_err - match ready!(self.0.as_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - } + self.0 + .as_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)) } } diff --git a/actix-http/src/body/response_body.rs b/actix-http/src/body/response_body.rs index 855c742f..699ea938 100644 --- a/actix-http/src/body/response_body.rs +++ b/actix-http/src/body/response_body.rs @@ -5,7 +5,7 @@ use std::{ }; use bytes::Bytes; -use futures_core::{ready, Stream}; +use futures_core::Stream; use pin_project::pin_project; use crate::error::Error; @@ -77,12 +77,7 @@ where cx: &mut Context<'_>, ) -> Poll> { match self.project() { - // TODO: MSRV 1.51: poll_map_err - ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into), ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 1e69990a..c39c0e88 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -131,18 +131,9 @@ where Poll::Ready(Some(Ok(std::mem::take(b)))) } } - // TODO: MSRV 1.51: poll_map_err - EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), EncoderBodyProj::BoxedStream(ref mut b) => { - match ready!(b.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - } + b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed) } } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 523e652f..5fd3cc21 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -63,12 +63,9 @@ where .is_write_buf_full() { let next = - // TODO: MSRV 1.51: poll_map_err match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), - Poll::Ready(Some(Err(err))) => { - return Poll::Ready(Err(err.into())) - } + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())), Poll::Ready(None) => Poll::Ready(None), Poll::Pending => Poll::Pending, }; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 0f09b6ad..9574b02f 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -341,7 +341,6 @@ where ) -> Poll>> { let this = self.project(); - // TODO: MSRV 1.51: poll_map_err match ready!(this.body.poll_next(cx)) { Some(Ok(chunk)) => { *this.size += chunk.len(); From dade818ebaab441e8cc5d359068209daa002b488 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 04:18:54 +0100 Subject: [PATCH 12/99] add middleware composition tests (#2375) --- actix-http/CHANGES.md | 2 ++ actix-http/src/body/message_body.rs | 4 --- actix-http/src/encoding/encoder.rs | 4 +-- actix-http/src/h1/utils.rs | 4 ++- src/middleware/mod.rs | 40 +++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 57c09d2d..63172e56 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -6,8 +6,10 @@ ### Fixed * Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] +* Remove `Into` bound on `Encoder` body types. [#2375] [#2364]: https://github.com/actix/actix-web/pull/2364 +[#2375]: https://github.com/actix/actix-web/pull/2375 ## 3.0.0-beta.8 - 2021-08-09 diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 2d2642ba..edb4c550 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -11,8 +11,6 @@ use bytes::{Bytes, BytesMut}; use futures_core::ready; use pin_project_lite::pin_project; -use crate::error::Error; - use super::BodySize; /// An interface for response bodies. @@ -47,7 +45,6 @@ impl MessageBody for () { impl MessageBody for Box where B: MessageBody + Unpin, - B::Error: Into, { type Error = B::Error; @@ -66,7 +63,6 @@ where impl MessageBody for Pin> where B: MessageBody, - B::Error: Into, { type Error = B::Error; diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index c39c0e88..abd8cedb 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -29,7 +29,7 @@ use crate::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, }, - Error, ResponseHead, + ResponseHead, }; use super::Writer; @@ -107,7 +107,6 @@ enum EncoderBody { impl MessageBody for EncoderBody where B: MessageBody, - B::Error: Into, { type Error = EncoderError; @@ -142,7 +141,6 @@ where impl MessageBody for Encoder where B: MessageBody, - B::Error: Into, { type Error = EncoderError; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 5fd3cc21..2547f449 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -65,7 +65,9 @@ where let next = match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), - Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())), + Poll::Ready(Some(Err(err))) => { + return Poll::Ready(Err(err.into())) + } Poll::Ready(None) => Poll::Ready(None), Poll::Pending => Poll::Pending, }; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 96a361fc..d19cb64e 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -19,3 +19,43 @@ mod compress; #[cfg(feature = "__compress")] pub use self::compress::Compress; + +#[cfg(test)] +mod tests { + use crate::{http::StatusCode, App}; + + use super::*; + + #[test] + fn common_combinations() { + // ensure there's no reason that the built-in middleware cannot compose + + let _ = App::new() + .wrap(Compat::new(Logger::default())) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(Logger::default()) + .wrap(NormalizePath::new(TrailingSlash::Trim)); + + let _ = App::new() + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .wrap(Logger::default()) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(Compat::new(Logger::default())); + + #[cfg(feature = "__compress")] + { + let _ = App::new().wrap(Compress::default()).wrap(Logger::default()); + let _ = App::new().wrap(Logger::default()).wrap(Compress::default()); + let _ = App::new().wrap(Compat::new(Compress::default())); + let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default()))); + } + } +} From c50eef61664e0614255d02924d43f18c4630e447 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 04:07:53 +0100 Subject: [PATCH 13/99] "deprecate" calls to NormalizePath::default --- MIGRATION.md | 3 ++- src/middleware/normalize.rs | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 78597436..9a70adb9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,7 +3,8 @@ * The default `NormalizePath` behavior now strips trailing slashes by default. This was previously documented to be the case in v3 but the behavior now matches. The effect is that routes defined with trailing slashes will become inaccessible when - using `NormalizePath::default()`. + using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. + It is advised that the `new` method be used instead. Before: `#[get("/test/")]` After: `#[get("/test")]` diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 219af1c6..8ad0bb3f 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -59,7 +59,7 @@ impl Default for TrailingSlash { /// /// # actix_web::rt::System::new().block_on(async { /// let app = App::new() -/// .wrap(middleware::NormalizePath::default()) +/// .wrap(middleware::NormalizePath::trim()) /// .route("/test", web::get().to(|| async { "test" })) /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" })); /// @@ -85,13 +85,31 @@ impl Default for TrailingSlash { /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// # }) /// ``` -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct NormalizePath(TrailingSlash); +impl Default for NormalizePath { + fn default() -> Self { + log::warn!( + "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \ + in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`." + ); + + Self(TrailingSlash::Trim) + } +} + impl NormalizePath { /// Create new `NormalizePath` middleware with the specified trailing slash style. pub fn new(trailing_slash_style: TrailingSlash) -> Self { - NormalizePath(trailing_slash_style) + Self(trailing_slash_style) + } + + /// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics. + /// + /// Use this instead of `NormalizePath::default()` to avoid deprecation warning. + pub fn trim() -> Self { + Self::new(TrailingSlash::Trim) } } From 7d01ece3556e77c0555f4e7da6c8699d8fc34fb1 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Tue, 31 Aug 2021 16:15:22 +0300 Subject: [PATCH 14/99] ResourceDef: support multiple-patterns as prefix (#2356) Co-authored-by: Rob Ede --- actix-router/CHANGES.md | 4 + actix-router/src/resource.rs | 253 +++++++++++++++++------------------ 2 files changed, 128 insertions(+), 129 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 804f7778..99038251 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -7,6 +7,9 @@ * 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] +* Support multi-pattern prefixes and joins. [#2356] +* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] +* Support `build_resource_path` on multi-pattern resources. [#2356] * Minimum supported Rust version (MSRV) is now 1.51. [#378]: https://github.com/actix/actix-net/pull/378 @@ -14,6 +17,7 @@ [#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 +[#2356]: https://github.com/actix/actix-web/pull/2356 ## 0.5.0-beta.1 - 2021-07-20 diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 57ce3680..be54336e 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -31,13 +31,13 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// # 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 `/`. +/// preceded 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. +/// A key point to underhand 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"]`. @@ -220,17 +220,15 @@ pub struct ResourceDef { name: Option, /// Pattern that generated the resource definition. - /// - /// `None` when pattern type is `DynamicSet`. patterns: Patterns, + is_prefix: bool, + /// Pattern type. pat_type: PatternType, /// List of segments that compose the pattern, in order. - /// - /// `None` when pattern type is `DynamicSet`. - segments: Option>, + segments: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -248,9 +246,6 @@ 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>), @@ -284,45 +279,7 @@ impl ResourceDef { /// ``` 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, - } - } - } + Self::new2(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -348,9 +305,9 @@ impl ResourceDef { /// assert!(!resource.is_match("user/123/stars")); /// assert!(!resource.is_match("/foo")); /// ``` - pub fn prefix(path: &str) -> Self { + pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::from_single_pattern(path, true) + ResourceDef::new2(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, @@ -375,7 +332,7 @@ impl ResourceDef { /// ``` pub fn root_prefix(path: &str) -> Self { profile_method!(root_prefix); - ResourceDef::prefix(&insert_slash(path)) + ResourceDef::prefix(insert_slash(path).into_owned()) } /// Returns a numeric resource ID. @@ -453,17 +410,14 @@ impl ResourceDef { /// 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, - } + self.is_prefix } /// Returns the pattern string that generated the resource definition. /// - /// Returns `None` if definition was constructed with multiple patterns. - /// See [`patterns_iter`][Self::pattern_iter]. + /// If definition is constructed with multiple patterns, the first pattern is returned. To get + /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns, + /// returns `None`. /// /// # Examples /// ``` @@ -472,11 +426,11 @@ impl ResourceDef { /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); /// /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); - /// assert!(resource.pattern().is_none()); + /// assert_eq!(resource.pattern(), Some("/profile")); pub fn pattern(&self) -> Option<&str> { match &self.patterns { Patterns::Single(pattern) => Some(pattern.as_str()), - Patterns::List(_) => None, + Patterns::List(patterns) => patterns.first().map(AsRef::as_ref), } } @@ -563,8 +517,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), - _ => ResourceDef::new(patterns), + 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), + _ => ResourceDef::new2(patterns, other.is_prefix()), } } @@ -609,11 +563,10 @@ impl ResourceDef { // `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) => is_prefix(prefix, path), - PatternType::Dynamic(ref re, _) => re.is_match(path), - PatternType::DynamicSet(ref re, _) => re.is_match(path), + match &self.pat_type { + PatternType::Static(pattern) => self.static_match(pattern, path).is_some(), + PatternType::Dynamic(re, _) => re.is_match(path), + PatternType::DynamicSet(re, _) => re.is_match(path), } } @@ -656,11 +609,7 @@ impl ResourceDef { profile_method!(find_match); match &self.pat_type { - PatternType::Static(segment) if path == segment => Some(segment.len()), - PatternType::Static(_) => None, - - PatternType::Prefix(prefix) if is_prefix(prefix, path) => Some(prefix.len()), - PatternType::Prefix(_) => None, + PatternType::Static(pattern) => self.static_match(pattern, path), PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), @@ -753,10 +702,10 @@ impl ResourceDef { let path_str = path.path(); let (matched_len, matched_vars) = match &self.pat_type { - PatternType::Static(_) | PatternType::Prefix(_) => { + PatternType::Static(pattern) => { profile_section!(pattern_static_or_prefix); - match self.find_match(path_str) { + match self.static_match(pattern, path_str) { Some(len) => (len, None), None => return false, } @@ -844,13 +793,10 @@ impl ResourceDef { 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) { + for segment in &self.segments { + match segment { + PatternSegment::Const(val) => path.push_str(val), + PatternSegment::Var(name) => match vars(name) { Some(val) => path.push_str(val.as_ref()), _ => return false, }, @@ -864,8 +810,8 @@ impl ResourceDef { /// /// 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. + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. /// /// # Examples /// ``` @@ -890,8 +836,8 @@ impl ResourceDef { /// /// 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. + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. /// /// # Examples /// ``` @@ -921,19 +867,69 @@ impl ResourceDef { 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); + /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. + fn static_match(&self, pattern: &str, path: &str) -> Option { + let rem = path.strip_prefix(pattern)?; - let pattern = pattern.to_owned(); - let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); + match self.is_prefix { + // resource is not a prefix so an exact match is needed + false if rem.is_empty() => Some(pattern.len()), + + // resource is a prefix so rem should start with a path delimiter + true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()), + + // otherwise, no match + _ => None, + } + } + + fn new2(paths: T, is_prefix: bool) -> Self { + profile_method!(new2); + + let patterns = paths.patterns(); + let (pat_type, segments) = match &patterns { + Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), + + // since zero length pattern sets are possible + // just return a useless `ResourceDef` + Patterns::List(patterns) if patterns.is_empty() => ( + PatternType::DynamicSet(RegexSet::empty(), Vec::new()), + Vec::new(), + ), + + Patterns::List(patterns) => { + let mut re_set = Vec::with_capacity(patterns.len()); + let mut pattern_data = Vec::new(); + let mut segments = None; + + for pattern in patterns { + match ResourceDef::parse(pattern, is_prefix, true) { + (PatternType::Dynamic(re, names), segs) => { + re_set.push(re.as_str().to_owned()); + pattern_data.push((re, names)); + segments.get_or_insert(segs); + } + _ => unreachable!(), + } + } + + let pattern_re_set = RegexSet::new(re_set).unwrap(); + let segments = segments.unwrap_or_else(Vec::new); + + ( + PatternType::DynamicSet(pattern_re_set, pattern_data), + segments, + ) + } + }; ResourceDef { id: 0, name: None, - patterns: Patterns::Single(pattern), + patterns, + is_prefix, pat_type, - segments: Some(segments), + segments, } } @@ -1023,20 +1019,15 @@ impl ResourceDef { ) -> (PatternType, Vec) { profile_method!(parse); - let mut unprocessed = pattern; - - if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { + if !force_dynamic && pattern.find('{').is_none() && !pattern.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())]); + return ( + PatternType::Static(pattern.to_owned()), + vec![PatternSegment::Const(pattern.to_owned())], + ); } + let mut unprocessed = pattern; let mut segments = Vec::new(); let mut re = format!("{}^", REGEX_FLAGS); let mut dyn_segment_count = 0; @@ -1137,18 +1128,7 @@ 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(..)) - } - } + self.patterns == other.patterns && self.is_prefix == other.is_prefix } } @@ -1183,15 +1163,6 @@ 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`. -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)] mod tests { use super::*; @@ -1376,6 +1347,24 @@ mod tests { assert!(!re.is_match("/user/2345/sdg")); } + #[test] + fn dynamic_set_prefix() { + let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]); + + assert_eq!(re.find_match("/u/abc"), Some(6)); + assert_eq!(re.find_match("/u/abc/123"), Some(6)); + assert_eq!(re.find_match("/s/user/profile"), None); + + assert_eq!(re.find_match("/123"), Some(4)); + assert_eq!(re.find_match("/123/456"), Some(4)); + assert_eq!(re.find_match("/12345"), None); + + let mut path = Path::new("/151/res"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "151"); + assert_eq!(path.unprocessed(), "/res"); + } + #[test] fn parse_tail() { let re = ResourceDef::new("/user/-{id}*"); @@ -1602,10 +1591,11 @@ mod tests { } #[test] - fn multi_pattern_cannot_build_path() { + fn multi_pattern_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())); + assert!(resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + assert_eq!(s, "/user/123"); } #[test] @@ -1738,8 +1728,12 @@ mod tests { 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"); + join_test!("", "/user" => "", "/user", "foo", "/user11", "user", "user/123"); + join_test!("/user", "/xx" => "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + + join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc", + "/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def", + "/ver1/req/abc/def", "", "/", "/v1/"); } #[test] @@ -1777,6 +1771,7 @@ mod tests { 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"); + match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); } #[test] From 373b3f91dff58ac6c5b1158206e7380376906541 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 1 Sep 2021 06:48:43 +0300 Subject: [PATCH 15/99] rework `ResourceMap` internals (#2337) --- src/app_service.rs | 4 +- src/request.rs | 9 +- src/rmap.rs | 422 ++++++++++++++++++++++++--------------------- 3 files changed, 233 insertions(+), 202 deletions(-) diff --git a/src/app_service.rs b/src/app_service.rs index ce52543b..cf34b302 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -79,7 +79,7 @@ where .into_iter() .for_each(|mut srv| srv.register(&mut config)); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); let (config, services) = config.into_services(); @@ -104,7 +104,7 @@ where // complete ResourceMap tree creation let rmap = Rc::new(rmap); - rmap.finish(rmap.clone()); + ResourceMap::finish(&rmap); // construct all async data factory futures let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); diff --git a/src/request.rs b/src/request.rs index 59850b4c..c25a5397 100644 --- a/src/request.rs +++ b/src/request.rs @@ -511,7 +511,7 @@ mod tests { let mut res = ResourceDef::new("/user/{name}.{ext}"); res.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut res, None); assert!(rmap.has_resource("/user/test.html")); assert!(!rmap.has_resource("/test/unknown")); @@ -541,7 +541,7 @@ mod tests { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -562,7 +562,7 @@ mod tests { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -581,9 +581,8 @@ mod tests { rdef.set_name("youtube"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); - assert!(rmap.has_resource("https://youtube.com/watch/unknown")); let req = TestRequest::default().rmap(rmap).to_http_request(); let url = req.url_for("youtube", &["oHg5SJYRHA0"]); diff --git a/src/rmap.rs b/src/rmap.rs index 0ee4de47..8466eda2 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -10,43 +10,75 @@ use crate::request::HttpRequest; #[derive(Clone, Debug)] pub struct ResourceMap { - root: ResourceDef, + pattern: ResourceDef, + + /// Named resources within the tree or, for external resources, + /// it points to isolated nodes outside the tree. + named: AHashMap>, + parent: RefCell>, - named: AHashMap, - patterns: Vec<(ResourceDef, Option>)>, + + /// Must be `None` for "edge" nodes. + nodes: Option>>, } impl ResourceMap { + /// Creates a _container_ node in the `ResourceMap` tree. pub fn new(root: ResourceDef) -> Self { ResourceMap { - root, - parent: RefCell::new(Weak::new()), + pattern: root, named: AHashMap::default(), - patterns: Vec::new(), + parent: RefCell::new(Weak::new()), + nodes: Some(Vec::new()), } } + /// Adds a (possibly nested) resource. + /// + /// To add a non-prefix pattern, `nested` must be `None`. + /// To add external resource, supply a pattern without a leading `/`. + /// The root pattern of `nested`, if present, should match `pattern`. 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 let Some(name) = pattern.name() { - self.named.insert(name.to_owned(), pattern.clone()); + pattern.set_id(self.nodes.as_ref().unwrap().len() as u16); + + if let Some(new_node) = nested { + assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch"); + self.named.extend(new_node.named.clone().into_iter()); + self.nodes.as_mut().unwrap().push(new_node); + } else { + let new_node = Rc::new(ResourceMap { + pattern: pattern.clone(), + named: AHashMap::default(), + parent: RefCell::new(Weak::new()), + nodes: None, + }); + + if let Some(name) = pattern.name() { + self.named.insert(name.to_owned(), Rc::clone(&new_node)); + } + + let is_external = match pattern.pattern() { + Some(p) => !p.is_empty() && !p.starts_with('/'), + None => false, + }; + + // Don't add external resources to the tree + if !is_external { + self.nodes.as_mut().unwrap().push(new_node); + } } } - pub(crate) fn finish(&self, current: Rc) { - for (_, nested) in &self.patterns { - if let Some(ref nested) = nested { - *nested.parent.borrow_mut() = Rc::downgrade(¤t); - nested.finish(nested.clone()); - } + pub(crate) fn finish(self: &Rc) { + for node in self.nodes.iter().flatten() { + node.parent.replace(Rc::downgrade(self)); + ResourceMap::finish(node); } } /// Generate url for named resource /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. + /// Check [`HttpRequest::url_for`] for detailed information. pub fn url_for( &self, req: &HttpRequest, @@ -57,197 +89,97 @@ impl ResourceMap { U: IntoIterator, I: AsRef, { - let mut path = String::new(); let mut elements = elements.into_iter(); - if self.patterns_for(name, &mut path, &mut elements)?.is_some() { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } + let path = self + .named + .get(name) + .ok_or(UrlGenerationError::ResourceNotFound)? + .root_rmap_fn(String::with_capacity(24), |mut acc, node| { + node.pattern + .resource_path_from_iter(&mut acc, &mut elements) + .then(|| acc) + }) + .ok_or(UrlGenerationError::NotEnoughElements)?; + + if path.starts_with('/') { + let conn = req.connection_info(); + Ok(Url::parse(&format!( + "{}://{}{}", + conn.scheme(), + conn.host(), + path + ))?) } else { - Err(UrlGenerationError::ResourceNotFound) + Ok(Url::parse(&path)?) } } pub fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(pat_len) = pattern.find_match(path) { - return rmap.has_resource(&path[pat_len..]); - } - } else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" { - return true; - } - } - false + self.find_matching_node(path).is_some() } /// Returns the name of the route that matches the given path or None if no full match - /// is possible. + /// is possible or the matching resource is not named. pub fn match_name(&self, path: &str) -> Option<&str> { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.find_match(path) { - return rmap.match_name(&path[plen..]); - } - } else if pattern.is_match(path) { - return pattern.name(); - } - } - - None + self.find_matching_node(path)?.pattern.name() } /// Returns the full resource pattern matched against a path or None if no full match /// is possible. pub fn match_pattern(&self, path: &str) -> Option { - let path = if path.is_empty() { "/" } else { path }; - - // ensure a full match exists - if !self.has_resource(path) { - return None; - } - - Some(self.traverse_resource_pattern(path)) + self.find_matching_node(path)?.root_rmap_fn( + String::with_capacity(24), + |mut acc, node| { + acc.push_str(node.pattern.pattern()?); + Some(acc) + }, + ) } - /// Takes remaining path and tries to match it up against a resource definition within the - /// current resource map recursively, returning a concatenation of all resource prefixes and - /// patterns matched in the tree. - /// - /// Should only be used after checking the resource exists in the map so that partial match - /// patterns are not returned. - 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.find_match(remaining) { - // TODO: think about unwrap_or - let prefix = pattern.pattern().unwrap_or("").to_owned(); - - return [ - prefix, - rmap.traverse_resource_pattern(&remaining[prefix_len..]), - ] - .concat(); - } - } else if pattern.is_match(remaining) { - // TODO: think about unwrap_or - return pattern.pattern().unwrap_or("").to_owned(); - } - } - - String::new() + fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> { + self._find_matching_node(path).flatten() } - fn patterns_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Returns `None` if root pattern doesn't match; + /// `Some(None)` if root pattern matches but there is no matching child pattern. + /// Don't search sideways when `Some(none)` is returned. + fn _find_matching_node(&self, path: &str) -> Option> { + let matched_len = self.pattern.find_match(path)?; + let path = &path[matched_len..]; + + Some(match &self.nodes { + // find first sub-node to match remaining path + Some(nodes) => nodes + .iter() + .filter_map(|node| node._find_matching_node(path)) + .next() + .flatten(), + + // only terminate at edge nodes + None => Some(self), + }) + } + + /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context. + fn root_rmap_fn(&self, init: B, mut f: F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } + self._root_rmap_fn(init, &mut f) } - fn pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map. + fn _root_rmap_fn(&self, init: B, f: &mut F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if let Some(pattern) = self.named.get(name) { - if pattern - .pattern() - .map(|pat| pat.starts_with('/')) - .unwrap_or(false) - { - self.fill_root(path, elements)?; - } + let data = match self.parent.borrow().upgrade() { + Some(ref parent) => parent._root_rmap_fn(init, f)?, + None => init, + }; - if pattern.resource_path_from_iter(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - for (_, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - } - Ok(None) - } - } - - fn fill_root( - &self, - path: &mut String, - elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = self.parent.borrow().upgrade() { - parent.fill_root(path, elements)?; - } - - if self.root.resource_path_from_iter(path, elements) { - Ok(()) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } - - fn parent_pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - 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_from_iter(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } + f(data, self) } } @@ -259,7 +191,7 @@ mod tests { fn extract_matched_pattern() { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -275,9 +207,10 @@ mod tests { &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); + root.add(&mut ResourceDef::new("/info"), None); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup @@ -288,7 +221,7 @@ mod tests { assert!(root.has_resource("/v2")); assert!(!root.has_resource("/v33")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/profile")); @@ -336,7 +269,7 @@ mod tests { rdef.set_name("root_info"); root.add(&mut rdef, None); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); let mut rdef = ResourceDef::new("/"); user_map.add(&mut rdef, None); @@ -350,14 +283,14 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup assert!(root.has_resource("/info")); assert!(!root.has_resource("/bar")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/post/55")); @@ -377,7 +310,7 @@ mod tests { // ref: https://github.com/actix/actix-web/issues/1582 let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -393,20 +326,119 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // check root has no parent assert!(root.parent.borrow().upgrade().is_none()); // check child has parent reference - assert!(root.patterns[0].1.is_some()); + assert!(root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .is_some()); // check child's parent root id matches root's root id - assert_eq!( - root.patterns[0].1.as_ref().unwrap().root.id(), - root.root.id() - ); + assert!(Rc::ptr_eq( + &root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .unwrap(), + &root + )); let output = format!("{:?}", root); assert!(output.starts_with("ResourceMap {")); assert!(output.ends_with(" }")); } + + #[test] + fn short_circuit() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_root = ResourceDef::prefix("/user"); + let mut user_map = ResourceMap::new(user_root.clone()); + user_map.add(&mut ResourceDef::new("/u1"), None); + user_map.add(&mut ResourceDef::new("/u2"), None); + + root.add(&mut ResourceDef::new("/user/u3"), None); + root.add(&mut user_root, Some(Rc::new(user_map))); + root.add(&mut ResourceDef::new("/user/u4"), None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(rmap.has_resource("/user/u1")); + assert!(rmap.has_resource("/user/u2")); + assert!(rmap.has_resource("/user/u3")); + assert!(!rmap.has_resource("/user/u4")); + } + + #[test] + fn url_for() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_scope_rdef = ResourceDef::prefix("/user"); + let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone()); + + let mut user_rdef = ResourceDef::new("/{user_id}"); + let mut user_map = ResourceMap::new(user_rdef.clone()); + + let mut post_rdef = ResourceDef::new("/post/{sub_id}"); + post_rdef.set_name("post"); + + user_map.add(&mut post_rdef, None); + user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map))); + root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map))); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + let url = rmap + .url_for(&req, "post", &["u123", "foobar"]) + .unwrap() + .to_string(); + assert_eq!(url, "http://localhost:8888/user/u123/post/foobar"); + + assert!(rmap.url_for(&req, "missing", &["u123"]).is_err()); + } + + #[test] + fn external_resource_with_no_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + } + + #[test] + fn external_resource_with_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + rdef.set_name("duck"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + assert_eq!( + rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(), + "https://duck.com/abcd" + ); + } } From ddc8c16cb34c9730b9a4922413fbf027e401052d Mon Sep 17 00:00:00 2001 From: Arthur Le Moigne Date: Wed, 1 Sep 2021 10:08:29 +0200 Subject: [PATCH 16/99] Fix quality parse error in Accept-Encoding HTTP header (#2344) --- CHANGES.md | 7 +- actix-http/CHANGES.md | 5 +- actix-http/src/encoding/decoder.rs | 3 +- .../src/header/shared/content_encoding.rs | 56 ++--- actix-http/src/header/shared/quality_item.rs | 29 ++- src/middleware/compress.rs | 217 +++++++++++++++--- tests/test_server.rs | 19 ++ 7 files changed, 259 insertions(+), 77 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5325caf4..217ec4f7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,15 @@ ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] -### Changes +### Changed * Minimum supported Rust version (MSRV) is now 1.51. +* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] + +### Fixed +* Fix quality parse error in Accept-Encoding header. [#2344] [#2325]: https://github.com/actix/actix-web/pull/2325 +[#2344]: https://github.com/actix/actix-web/pull/2344 ## 4.0.0-beta.8 - 2021-06-26 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 63172e56..f4efef54 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,22 +1,23 @@ # Changes ## Unreleased - 2021-xx-xx -### Changes +### Changed * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] * Remove `Into` bound on `Encoder` body types. [#2375] +* Fix quality parse error in Accept-Encoding header. [#2344] [#2364]: https://github.com/actix/actix-web/pull/2364 [#2375]: https://github.com/actix/actix-web/pull/2375 +[#2344]: https://github.com/actix/actix-web/pull/2344 ## 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] diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index d3e30483..81e97d91 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,6 +1,7 @@ //! Stream decoders. use std::{ + convert::TryFrom, future::Future, io::{self, Write as _}, pin::Pin, @@ -80,7 +81,7 @@ where let encoding = headers .get(&CONTENT_ENCODING) .and_then(|val| val.to_str().ok()) - .map(ContentEncoding::from) + .and_then(|x| ContentEncoding::try_from(x).ok()) .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index b9c1d279..375e8c2f 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, str::FromStr}; +use std::{convert::TryFrom, error, fmt, str::FromStr}; use http::header::InvalidHeaderValue; @@ -8,6 +8,20 @@ use crate::{ HttpMessage, }; +/// Error return when a content encoding is unknown. +/// +/// Example: 'compress' +#[derive(Debug)] +pub struct ContentEncodingParseError; + +impl fmt::Display for ContentEncodingParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unsupported content encoding") + } +} + +impl error::Error for ContentEncodingParseError {} + /// Represents a supported content encoding. #[derive(Copy, Clone, PartialEq, Debug)] pub enum ContentEncoding { @@ -37,7 +51,7 @@ impl ContentEncoding { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } - /// Convert content encoding to string + /// Convert content encoding to string. #[inline] pub fn as_str(self) -> &'static str { match self { @@ -48,18 +62,6 @@ impl ContentEncoding { ContentEncoding::Identity | ContentEncoding::Auto => "identity", } } - - /// Default Q-factor (quality) value. - #[inline] - pub fn quality(self) -> f64 { - match self { - ContentEncoding::Br => 1.1, - ContentEncoding::Gzip => 1.0, - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - ContentEncoding::Zstd => 0.0, - } - } } impl Default for ContentEncoding { @@ -69,31 +71,33 @@ impl Default for ContentEncoding { } impl FromStr for ContentEncoding { - type Err = Infallible; + type Err = ContentEncodingParseError; fn from_str(val: &str) -> Result { - Ok(Self::from(val)) - } -} - -impl From<&str> for ContentEncoding { - fn from(val: &str) -> ContentEncoding { let val = val.trim(); if val.eq_ignore_ascii_case("br") { - ContentEncoding::Br + Ok(ContentEncoding::Br) } else if val.eq_ignore_ascii_case("gzip") { - ContentEncoding::Gzip + Ok(ContentEncoding::Gzip) } else if val.eq_ignore_ascii_case("deflate") { - ContentEncoding::Deflate + Ok(ContentEncoding::Deflate) } else if val.eq_ignore_ascii_case("zstd") { - ContentEncoding::Zstd + Ok(ContentEncoding::Zstd) } else { - ContentEncoding::default() + Err(ContentEncodingParseError) } } } +impl TryFrom<&str> for ContentEncoding { + type Error = ContentEncodingParseError; + + fn try_from(val: &str) -> Result { + val.parse() + } +} + impl IntoHeaderValue for ContentEncoding { type Error = InvalidHeaderValue; diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 240a0afa..63fa02e7 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,11 +1,14 @@ use std::{ cmp, convert::{TryFrom, TryInto}, - fmt, str, + fmt, + str::{self, FromStr}, }; use derive_more::{Display, Error}; +use crate::error::ParseError; + const MAX_QUALITY: u16 = 1000; const MAX_FLOAT_QUALITY: f32 = 1.0; @@ -113,12 +116,12 @@ impl fmt::Display for QualityItem { } } -impl str::FromStr for QualityItem { - type Err = crate::error::ParseError; +impl FromStr for QualityItem { + type Err = ParseError; - fn from_str(qitem_str: &str) -> Result, crate::error::ParseError> { + fn from_str(qitem_str: &str) -> Result { if !qitem_str.is_ascii() { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } // Set defaults used if parsing fails. @@ -139,7 +142,7 @@ impl str::FromStr for QualityItem { if parts[0].len() < 2 { // Can't possibly be an attribute since an attribute needs at least a name followed // by an equals sign. And bare identifiers are forbidden. - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } let start = &parts[0][0..2]; @@ -148,25 +151,21 @@ impl str::FromStr for QualityItem { let q_val = &parts[0][2..]; if q_val.len() > 5 { // longer than 5 indicates an over-precise q-factor - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } - let q_value = q_val - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let q_value = q_val.parse::().map_err(|_| ParseError::Header)?; if (0f32..=1f32).contains(&q_value) { quality = q_value; raw_item = parts[1]; } else { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } } } - let item = raw_item - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let item = raw_item.parse::().map_err(|_| ParseError::Header)?; // we already checked above that the quality is within range Ok(QualityItem::new(item, Quality::from_f32(quality))) @@ -224,7 +223,7 @@ mod tests { } } - impl str::FromStr for Encoding { + impl FromStr for Encoding { type Err = crate::error::ParseError; fn from_str(s: &str) -> Result { use Encoding::*; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index a9128bc4..0e61a8e7 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -2,10 +2,10 @@ use std::{ cmp, + convert::TryFrom, future::Future, marker::PhantomData, pin::Pin, - str::FromStr, task::{Context, Poll}, }; @@ -13,16 +13,18 @@ use actix_http::{ body::{MessageBody, ResponseBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, + StatusCode, }; use actix_service::{Service, Transform}; -use actix_utils::future::{ok, Ready}; +use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; +use once_cell::sync::Lazy; use pin_project::pin_project; use crate::{ dev::BodyEncoding, service::{ServiceRequest, ServiceResponse}, - Error, + Error, HttpResponse, }; /// Middleware for compressing response payloads. @@ -78,34 +80,78 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } +static SUPPORTED_ALGORITHM_NAMES: Lazy = Lazy::new(|| { + let mut encoding = vec![]; + + #[cfg(feature = "compress-brotli")] + { + encoding.push("br"); + } + + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); + } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled by itself" + ); + + encoding.join(", ") +}); + impl Service for CompressMiddleware where - B: MessageBody, S: Service, Error = Error>, + B: MessageBody, { type Response = ServiceResponse>>; type Error = Error; - type Future = CompressResponse; + type Future = Either, Ready>>; actix_service::forward_ready!(service); #[allow(clippy::borrow_interior_mutable_const)] fn call(&self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding - let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc, self.encoding) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - }; + let encoding_result = req + .headers() + .get(&ACCEPT_ENCODING) + .and_then(|val| val.to_str().ok()) + .map(|enc| AcceptEncoding::try_parse(enc, self.encoding)); - CompressResponse { - encoding, - fut: self.service.call(req), - _phantom: PhantomData, + match encoding_result { + // Missing header => fallback to identity + None => Either::left(CompressResponse { + encoding: ContentEncoding::Identity, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // Valid encoding + Some(Ok(encoding)) => Either::left(CompressResponse { + encoding, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // There is an HTTP header but we cannot match what client as asked for + Some(Err(_)) => { + let res = HttpResponse::with_body( + StatusCode::NOT_ACCEPTABLE, + SUPPORTED_ALGORITHM_NAMES.as_str(), + ); + let enc = ContentEncoding::Identity; + + Either::right(ok(req.into_response(res.map_body(move |head, body| { + Encoder::response(enc, head, ResponseBody::Other(body.into())) + })))) + } } } } @@ -114,7 +160,6 @@ where pub struct CompressResponse where S: Service, - B: MessageBody, { #[pin] fut: S::Future, @@ -151,6 +196,7 @@ where struct AcceptEncoding { encoding: ContentEncoding, + // TODO: use Quality or QualityItem quality: f64, } @@ -177,26 +223,56 @@ impl PartialOrd for AcceptEncoding { impl PartialEq for AcceptEncoding { fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality + self.encoding == other.encoding && self.quality == other.quality } } +/// Parse q-factor from quality strings. +/// +/// If parse fail, then fallback to default value which is 1. +/// More details available here: +fn parse_quality(parts: &[&str]) -> f64 { + for part in parts { + if part.trim().starts_with("q=") { + return part[2..].parse().unwrap_or(1.0); + } + } + + 1.0 +} + +#[derive(Debug, PartialEq, Eq)] +enum AcceptEncodingError { + /// This error occurs when client only support compressed response and server do not have any + /// algorithm that match client accepted algorithms. + CompressionAlgorithmMismatch, +} + impl AcceptEncoding { fn new(tag: &str) -> Option { let parts: Vec<&str> = tag.split(';').collect(); let encoding = match parts.len() { 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => f64::from_str(parts[1]).unwrap_or(0.0), + _ => match ContentEncoding::try_from(parts[0]) { + Err(_) => return None, + Ok(x) => x, + }, }; + + let quality = parse_quality(&parts[1..]); + if quality <= 0.0 || quality > 1.0 { + return None; + } + Some(AcceptEncoding { encoding, quality }) } - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { + /// Parse a raw Accept-Encoding header value into an ordered list then return the best match + /// based on middleware configuration. + pub fn try_parse( + raw: &str, + encoding: ContentEncoding, + ) -> Result { let mut encodings = raw .replace(' ', "") .split(',') @@ -206,13 +282,90 @@ impl AcceptEncoding { encodings.sort(); for enc in encodings { - if encoding == ContentEncoding::Auto { - return enc.encoding; - } else if encoding == enc.encoding { - return encoding; + if encoding == ContentEncoding::Auto || encoding == enc.encoding { + return Ok(enc.encoding); } } - ContentEncoding::Identity + // Special case if user cannot accept uncompressed data. + // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding + // TODO: account for whitespace + if raw.contains("*;q=0") || raw.contains("identity;q=0") { + return Err(AcceptEncodingError::CompressionAlgorithmMismatch); + } + + Ok(ContentEncoding::Identity) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_parse_eq { + ($raw:expr, $result:expr) => { + assert_eq!( + AcceptEncoding::try_parse($raw, ContentEncoding::Auto), + Ok($result) + ); + }; + } + + macro_rules! assert_parse_fail { + ($raw:expr) => { + assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); + }; + } + + #[test] + fn test_parse_encoding() { + // Test simple case + assert_parse_eq!("br", ContentEncoding::Br); + assert_parse_eq!("gzip", ContentEncoding::Gzip); + assert_parse_eq!("deflate", ContentEncoding::Deflate); + assert_parse_eq!("zstd", ContentEncoding::Zstd); + + // Test space, trim, missing values + assert_parse_eq!("br,,,,", ContentEncoding::Br); + assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip); + + // Test float number parsing + assert_parse_eq!("br;q=1 ,", ContentEncoding::Br); + assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br); + + // Test wildcard + assert_parse_eq!("*", ContentEncoding::Identity); + assert_parse_eq!("*;q=1.0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_encoding_qfactor_ordering() { + assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip); + assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd); + + assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br); + assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip); + } + + #[test] + fn test_parse_encoding_qfactor_invalid() { + // Out of range + assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity); + assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity); + + // Disabled + assert_parse_eq!("gzip;q=0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_compression_required() { + // Check we fallback to identity if there is an unsupported compression algorithm + assert_parse_eq!("compress", ContentEncoding::Identity); + + // User do not want any compression + assert_parse_fail!("compress, identity;q=0"); + assert_parse_fail!("compress, identity;q=0.0"); + assert_parse_fail!("compress, *;q=0"); + assert_parse_fail!("compress, *;q=0.0"); } } diff --git a/tests/test_server.rs b/tests/test_server.rs index afea39dd..beb8ff0f 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1077,3 +1077,22 @@ async fn test_data_drop() { assert_eq!(num.load(Ordering::SeqCst), 0); } + +#[actix_rt::test] +async fn test_accept_encoding_no_match() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::default()) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish()))) + }); + + let response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "compress, identity;q=0")) + .no_decompress() + .send() + .await + .unwrap(); + + assert_eq!(response.status().as_u16(), 406); +} From 93112644d3da17833ea03fc7856329ec2f35ba1c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 1 Sep 2021 09:53:26 +0100 Subject: [PATCH 17/99] non exhaustive content encoding (#2377) --- Cargo.toml | 2 +- actix-http/CHANGES.md | 3 ++ actix-http/Cargo.toml | 2 +- actix-http/src/encoding/decoder.rs | 3 +- .../src/header/shared/content_encoding.rs | 17 ++++------- src/scope.rs | 4 +-- src/test.rs | 2 +- src/types/form.rs | 2 +- src/types/json.rs | 30 ++++--------------- src/types/query.rs | 30 ++++++++----------- 10 files changed, 35 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cee40136..699717b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ regex = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.7" -smallvec = "1.6" +smallvec = "1.6.1" socket2 = "0.4.0" time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f4efef54..65206cf9 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx ### Changed +* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed @@ -12,12 +13,14 @@ [#2364]: https://github.com/actix/actix-web/pull/2364 [#2375]: https://github.com/actix/actix-web/pull/2375 [#2344]: https://github.com/actix/actix-web/pull/2344 +[#2377]: https://github.com/actix/actix-web/pull/2377 ## 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] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 68f98098..54505a21 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -73,7 +73,7 @@ rand = "0.8" regex = "1.3" serde = "1.0" sha-1 = "0.9" -smallvec = "1.6" +smallvec = "1.6.1" time = { version = "0.2.23", default-features = false, features = ["std"] } tokio = { version = "1.2", features = ["sync"] } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 81e97d91..c32983fc 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,7 +1,6 @@ //! Stream decoders. use std::{ - convert::TryFrom, future::Future, io::{self, Write as _}, pin::Pin, @@ -81,7 +80,7 @@ where let encoding = headers .get(&CONTENT_ENCODING) .and_then(|val| val.to_str().ok()) - .and_then(|x| ContentEncoding::try_from(x).ok()) + .and_then(|x| x.parse().ok()) .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index 375e8c2f..1af109c0 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,5 +1,6 @@ -use std::{convert::TryFrom, error, fmt, str::FromStr}; +use std::{convert::TryFrom, str::FromStr}; +use derive_more::{Display, Error}; use http::header::InvalidHeaderValue; use crate::{ @@ -11,19 +12,13 @@ use crate::{ /// Error return when a content encoding is unknown. /// /// Example: 'compress' -#[derive(Debug)] +#[derive(Debug, Display, Error)] +#[display(fmt = "unsupported content encoding")] pub struct ContentEncodingParseError; -impl fmt::Display for ContentEncodingParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Unsupported content encoding") - } -} - -impl error::Error for ContentEncodingParseError {} - /// Represents a supported content encoding. -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] pub enum ContentEncoding { /// Automatically select encoding based on encoding negotiation. Auto, diff --git a/src/scope.rs b/src/scope.rs index b2edaeda..7d914f58 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -41,9 +41,9 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// fn main() { /// let app = App::new().service( /// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/path1").to(|| async { "OK" })) /// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) /// ); /// } /// ``` diff --git a/src/test.rs b/src/test.rs index 634826d1..34dd6f2d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -56,7 +56,7 @@ pub fn default_service( /// async fn test_init_service() { /// let app = test::init_service( /// App::new() -/// .service(web::resource("/test").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/test").to(|| async { "OK" })) /// ).await; /// /// // Create request object diff --git a/src/types/form.rs b/src/types/form.rs index c81f7355..2ace0e06 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -30,7 +30,7 @@ use crate::{ /// /// # Extractor /// To extract typed data from a request body, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// /// Use [`FormConfig`] to configure extraction process. /// diff --git a/src/types/json.rs b/src/types/json.rs index ab9708c5..8c2f51a6 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -97,19 +97,13 @@ impl ops::DerefMut for Json { } } -impl fmt::Display for Json -where - T: fmt::Display, -{ +impl fmt::Display for Json { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } -impl Serialize for Json -where - T: Serialize, -{ +impl Serialize for Json { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -133,10 +127,7 @@ impl Responder for Json { } /// See [here](#extractor) for example of usage as an extractor. -impl FromRequest for Json -where - T: DeserializeOwned + 'static, -{ +impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; type Config = JsonConfig; @@ -166,10 +157,7 @@ pub struct JsonExtractFut { err_handler: JsonErrorHandler, } -impl Future for JsonExtractFut -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -311,10 +299,7 @@ pub enum JsonBody { impl Unpin for JsonBody {} -impl JsonBody -where - T: DeserializeOwned + 'static, -{ +impl JsonBody { /// Create a new future to decode a JSON request payload. #[allow(clippy::borrow_interior_mutable_const)] pub fn new( @@ -395,10 +380,7 @@ where } } -impl Future for JsonBody -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { diff --git a/src/types/query.rs b/src/types/query.rs index 1e6f1111..73d08d09 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -3,14 +3,14 @@ use std::{fmt, ops, sync::Arc}; use actix_utils::future::{err, ok, Ready}; -use serde::de; +use serde::de::DeserializeOwned; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; /// Extract typed information from the request's query. /// /// To extract typed data from the URL query string, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// /// Use [`QueryConfig`] to configure extraction process. /// @@ -46,18 +46,18 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// // To access the entire underlying query struct, use `.into_inner()`. /// #[get("/debug1")] /// async fn debug1(info: web::Query) -> String { -/// dbg!("Authorization object={:?}", info.into_inner()); +/// dbg!("Authorization object = {:?}", info.into_inner()); /// "OK".to_string() /// } /// -/// // Or use `.0`, which is equivalent to `.into_inner()`. +/// // Or use destructuring, which is equivalent to `.into_inner()`. /// #[get("/debug2")] -/// async fn debug2(info: web::Query) -> String { -/// dbg!("Authorization object={:?}", info.0); +/// async fn debug2(web::Query(info): web::Query) -> String { +/// dbg!("Authorization object = {:?}", info); /// "OK".to_string() /// } /// ``` -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Query(pub T); impl Query { @@ -65,8 +65,10 @@ impl Query { pub fn into_inner(self) -> T { self.0 } +} - /// Deserialize `T` from a URL encoded query parameter string. +impl Query { + /// Deserialize a `T` from the URL encoded query parameter string. /// /// ``` /// # use std::collections::HashMap; @@ -76,10 +78,7 @@ impl Query { /// assert_eq!(numbers.get("two"), Some(&2)); /// assert!(numbers.get("three").is_none()); /// ``` - pub fn from_query(query_str: &str) -> Result - where - T: de::DeserializeOwned, - { + pub fn from_query(query_str: &str) -> Result { serde_urlencoded::from_str::(query_str) .map(Self) .map_err(QueryPayloadError::Deserialize) @@ -107,10 +106,7 @@ impl fmt::Display for Query { } /// See [here](#usage) for example of usage as an extractor. -impl FromRequest for Query -where - T: de::DeserializeOwned, -{ +impl FromRequest for Query { type Error = Error; type Future = Ready>; type Config = QueryConfig; @@ -165,7 +161,7 @@ where /// let query_cfg = web::QueryConfig::default() /// // use custom error handler /// .error_handler(|err, req| { -/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() +/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() /// }); /// /// App::new() From 53ec66caf47592b2bdfbbab2936d7ac727bcf315 Mon Sep 17 00:00:00 2001 From: Omid Rad Date: Wed, 1 Sep 2021 21:16:41 +0200 Subject: [PATCH 18/99] Send headers within the redirect requests. (#2310) --- awc/CHANGES.md | 3 + awc/src/middleware/redirect.rs | 365 +++++++++++++++++++++++++++------ 2 files changed, 303 insertions(+), 65 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 16132be1..9c6f258a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Send headers within the redirect requests. [#2310] +[#2310]: https://github.com/actix/actix-web/pull/2310 ## 3.0.0-beta.7 - 2021-06-26 ### Changed diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index ae09edf9..a8c14d54 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -85,10 +85,12 @@ where let max_redirect_times = self.max_redirect_times; // backup the uri and method for reuse schema and authority. - let (uri, method) = match head { - RequestHeadType::Owned(ref head) => (head.uri.clone(), head.method.clone()), + let (uri, method, headers) = match head { + RequestHeadType::Owned(ref head) => { + (head.uri.clone(), head.method.clone(), head.headers.clone()) + } RequestHeadType::Rc(ref head, ..) => { - (head.uri.clone(), head.method.clone()) + (head.uri.clone(), head.method.clone(), head.headers.clone()) } }; @@ -104,6 +106,7 @@ where max_redirect_times, uri: Some(uri), method: Some(method), + headers: Some(headers), body: body_opt, addr, connector: Some(connector), @@ -127,9 +130,10 @@ pin_project_lite::pin_project! { max_redirect_times: u8, uri: Option, method: Option, + headers: Option, body: Option, addr: Option, - connector: Option> + connector: Option>, } } } @@ -148,6 +152,7 @@ where max_redirect_times, uri, method, + headers, body, addr, connector, @@ -156,79 +161,60 @@ where StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::SEE_OTHER + | StatusCode::TEMPORARY_REDIRECT + | StatusCode::PERMANENT_REDIRECT if *max_redirect_times > 0 => { - let org_uri = uri.take().unwrap(); - // rebuild uri from the location header value. - let uri = rebuild_uri(&res, org_uri)?; + let is_redirect = res.head().status == StatusCode::TEMPORARY_REDIRECT + || res.head().status == StatusCode::PERMANENT_REDIRECT; - // reset method - let method = method.take().unwrap(); - let method = match method { - Method::GET | Method::HEAD => method, - _ => Method::GET, - }; + let prev_uri = uri.take().unwrap(); + + // rebuild uri from the location header value. + let next_uri = build_next_uri(&res, &prev_uri)?; // take ownership of states that could be reused let addr = addr.take(); let connector = connector.take(); - let mut max_redirect_times = *max_redirect_times; - // use a new request head. - let mut head = RequestHead::default(); - head.uri = uri.clone(); - head.method = method.clone(); - - let head = RequestHeadType::Owned(head); - - max_redirect_times -= 1; - - let fut = connector - .as_ref() - .unwrap() - // remove body - .call(ConnectRequest::Client(head, Body::None, addr)); - - self.set(RedirectServiceFuture::Client { - fut, - max_redirect_times, - uri: Some(uri), - method: Some(method), - // body is dropped on 301,302,303 - body: None, - addr, - connector, - }); - - self.poll(cx) - } - StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT - if *max_redirect_times > 0 => - { - let org_uri = uri.take().unwrap(); - // rebuild uri from the location header value. - let uri = rebuild_uri(&res, org_uri)?; - - // try to reuse body - let body = body.take(); - let body_new = match body { - Some(ref bytes) => Body::Bytes(bytes.clone()), - // TODO: should this be Body::Empty or Body::None. - _ => Body::Empty, + // reset method + let method = if is_redirect { + method.take().unwrap() + } else { + let method = method.take().unwrap(); + match method { + Method::GET | Method::HEAD => method, + _ => Method::GET, + } }; - let addr = addr.take(); - let method = method.take().unwrap(); - let connector = connector.take(); - let mut max_redirect_times = *max_redirect_times; + let mut body = body.take(); + let body_new = if is_redirect { + // try to reuse body + match body { + Some(ref bytes) => Body::Bytes(bytes.clone()), + // TODO: should this be Body::Empty or Body::None. + _ => Body::Empty, + } + } else { + body = None; + // remove body + Body::None + }; + + let mut headers = headers.take().unwrap(); + + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); // use a new request head. let mut head = RequestHead::default(); - head.uri = uri.clone(); + head.uri = next_uri.clone(); head.method = method.clone(); + head.headers = headers.clone(); let head = RequestHeadType::Owned(head); + let mut max_redirect_times = *max_redirect_times; max_redirect_times -= 1; let fut = connector @@ -239,8 +225,9 @@ where self.set(RedirectServiceFuture::Client { fut, max_redirect_times, - uri: Some(uri), + uri: Some(next_uri), method: Some(method), + headers: Some(headers), body, addr, connector, @@ -256,7 +243,7 @@ where } } -fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result { +fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { let uri = res .headers() .get(header::LOCATION) @@ -266,8 +253,8 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result(uri) @@ -281,12 +268,25 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result HttpResponse { + HttpResponse::TemporaryRedirect() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if req.method() == Method::POST && !body.is_empty() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_status_kind_301_302_303() { + let srv = actix_test::start(|| { + async fn root() -> HttpResponse { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if (req.method() == Method::GET || req.method() == Method::HEAD) + && body.is_empty() + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let res = srv.post("/").send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_headers() { + let srv = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let client = ClientBuilder::new() + .header("custom", "value") + .disable_redirects() + .finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 302); + + let client = ClientBuilder::new().header("custom", "value").finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let client = ClientBuilder::new().finish(); + let res = client + .get(srv.url("/")) + .insert_header(("custom", "value")) + .send() + .await + .unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_cross_origin_headers() { + // defining two services to have two different origins + let srv2 = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_none() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new().service(web::resource("/").route(web::to(root))) + }); + let srv2_port: u16 = srv2.addr().port(); + + let srv1 = actix_test::start(move || { + async fn root(req: HttpRequest) -> HttpResponse { + let port = *req.app_data::().unwrap(); + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(( + "location", + format!("http://localhost:{}/", port).as_str(), + )) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test1(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(("location", "/test2")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test2(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .app_data(srv2_port) + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test1").route(web::to(test1))) + .service(web::resource("/test2").route(web::to(test2))) + }); + + // send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header + let client = ClientBuilder::new() + .header(header::AUTHORIZATION, "auth_key_value") + .finish(); + let res = client.get(srv1.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + // send a request to same origin, http://srv1/test1 then http://srv1/test2. So it should NOT remove any header + let res = client.get(srv1.url("/test1")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_remove_sensitive_headers() { + fn gen_headers() -> header::HeaderMap { + let mut headers = header::HeaderMap::new(); + headers.insert(header::USER_AGENT, HeaderValue::from_str("value").unwrap()); + headers.insert( + header::AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert( + header::PROXY_AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert(header::COOKIE, HeaderValue::from_str("value").unwrap()); + headers + } + + // Same origin + let prev_uri = Uri::from_str("https://host/path1").unwrap(); + let next_uri = Uri::from_str("https://host/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 4); + + // different schema + let prev_uri = Uri::from_str("http://host/").unwrap(); + let next_uri = Uri::from_str("https://host/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different host + let prev_uri = Uri::from_str("https://host1/").unwrap(); + let next_uri = Uri::from_str("https://host2/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different port + let prev_uri = Uri::from_str("https://host:12/").unwrap(); + let next_uri = Uri::from_str("https://host:23/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different everything! + let prev_uri = Uri::from_str("http://host1:12/path1").unwrap(); + let next_uri = Uri::from_str("https://host2:23/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + } } From d8a0f46f264dd52a8d17a8c97036dcf9fc717cbb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 3 Sep 2021 18:00:43 +0100 Subject: [PATCH 19/99] refactor web module (#2379) --- .cargo/config.toml | 2 +- .github/workflows/ci.yml | 40 ++++- CHANGES.md | 5 +- src/dev.rs | 4 +- src/http/header/content_disposition.rs | 10 +- src/lib.rs | 1 - src/service.rs | 2 +- src/web.rs | 220 +++++++------------------ 8 files changed, 103 insertions(+), 181 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index db47ca46..f417a705 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,4 +6,4 @@ ci-min-test = "hack check --workspace --no-default-features --tests --examples" ci-default = "check --workspace --bins --tests --examples" ci-full = "check --workspace --all-features --bins --tests --examples" ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" -ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" +ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 221d2fb4..64750157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,13 +80,6 @@ jobs: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: doc tests - # due to unknown issue with running doc tests on macOS - if: matrix.target.os == 'ubuntu-latest' - uses: actions-rs/cargo@v1 - timeout-minutes: 40 - with: { command: ci-doctest } - - name: Generate coverage file if: > matrix.target.os == 'ubuntu-latest' @@ -106,5 +99,36 @@ jobs: - name: Clear the cargo caches run: | - cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + + rustdoc: + name: rustdoc + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Rust (nightly) + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.3.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: doc tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: { command: ci-doctest } diff --git a/CHANGES.md b/CHANGES.md index 217ec4f7..6826be07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,14 +5,17 @@ * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] ### Changed -* Minimum supported Rust version (MSRV) is now 1.51. * Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] +* Move `BaseHttpResponse` to `dev::Response`. [#2379] +* Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Fix quality parse error in Accept-Encoding header. [#2344] +* Re-export correct type at `web::HttpResponse`. [#2379] [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 +[#2379]: https://github.com/actix/actix-web/pull/2379 ## 4.0.0-beta.8 - 2021-06-26 diff --git a/src/dev.rs b/src/dev.rs index 0817d902..be3af86a 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -18,7 +18,7 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; -pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; +pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; pub use actix_service::{ @@ -26,7 +26,7 @@ pub use actix_service::{ }; use crate::http::header::ContentEncoding; -use actix_http::{Response, ResponseBuilder}; +use actix_http::ResponseBuilder; use actix_router::Patterns; diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 6e75fde9..fdd8a7da 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -1,10 +1,10 @@ //! # References //! -//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml +//! "The Content-Disposition Header Field" +//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" +//! "Returning Values from Forms: multipart/form-data" +//! Browser conformance tests at: +//! IANA assignment: use once_cell::sync::Lazy; use regex::Regex; diff --git a/src/lib.rs b/src/lib.rs index e7cf4636..d008fdb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,6 @@ pub mod test; pub(crate) mod types; pub mod web; -pub use actix_http::Response as BaseHttpResponse; pub use actix_http::{body, HttpMessage}; #[doc(inline)] pub use actix_rt as rt; diff --git a/src/service.rs b/src/service.rs index 48167e5b..b9fa0e12 100644 --- a/src/service.rs +++ b/src/service.rs @@ -476,7 +476,7 @@ impl WebService { /// Set service name. /// - /// Name is used for url generation. + /// Name is used for URL generation. pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self diff --git a/src/web.rs b/src/web.rs index 108ff314..40d7636c 100644 --- a/src/web.rs +++ b/src/web.rs @@ -3,44 +3,36 @@ 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; -use crate::extract::FromRequest; -use crate::handler::Handler; -use crate::resource::Resource; -use crate::responder::Responder; -use crate::route::Route; -use crate::scope::Scope; -use crate::service::WebService; +use crate::{ + error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, + responder::Responder, route::Route, scope::Scope, service::WebService, +}; pub use crate::config::ServiceConfig; pub use crate::data::Data; pub use crate::request::HttpRequest; pub use crate::request_data::ReqData; +pub use crate::response::HttpResponse; pub use crate::types::*; -/// Create resource for a specific path. +/// Creates a new resource for a specific path. /// -/// Resources may have variable path segments. For example, a -/// resource with the path `/a/{name}/c` would match all incoming -/// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. +/// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c` +/// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. /// -/// A variable segment is specified in the form `{identifier}`, -/// where the identifier can be used later in a request handler to -/// access the matched value for that segment. This is done by -/// looking up the identifier in the `Params` object returned by -/// `HttpRequest.match_info()` method. +/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used +/// later in a request handler to access the matched value for that segment. This is done by looking +/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method. /// /// By default, each segment matches the regular expression `[^{}/]+`. /// /// You can also specify a custom regex in the form `{identifier:regex}`: /// -/// For instance, to route `GET`-requests on any route matching -/// `/users/{userid}/{friend}` and store `userid` and `friend` in -/// the exposed `Params` object: +/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store +/// `userid` and `friend` in the exposed `Path` object: /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -55,10 +47,16 @@ pub fn resource(path: T) -> Resource { Resource::new(path) } -/// Configure scope for common root path. +/// Creates scope for common path prefix. /// -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. +/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic +/// path segments. +/// +/// # Examples +/// In this example, three routes are set up (and will handle any method): +/// * `/{project_id}/path1` +/// * `/{project_id}/path2` +/// * `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -70,148 +68,50 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -/// -/// In the above example, three routes get added: -/// * /{project_id}/path1 -/// * /{project_id}/path2 -/// * /{project_id}/path3 -/// pub fn scope(path: &str) -> Scope { Scope::new(path) } -/// Create *route* without configuration. +/// Creates a new un-configured route. pub fn route() -> Route { Route::new() } -/// Create *route* with `GET` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// -pub fn get() -> Route { - method(Method::GET) +macro_rules! method_route { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = "Creates a new route with `" $method_const "` method guard."] + /// + /// # Examples + #[doc = "In this example, one `" $method_const " /{project_id}` route is set up:"] + /// ``` + /// use actix_web::{web, App, HttpResponse}; + /// + /// let app = App::new().service( + /// web::resource("/{project_id}") + #[doc = " .route(web::" $method_fn "().to(|| HttpResponse::Ok()))"] + /// + /// ); + /// ``` + pub fn $method_fn() -> Route { + method(Method::$method_const) + } + } + }; } -/// Create *route* with `POST` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::post().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `POST` route gets added: -/// * /{project_id} -/// -pub fn post() -> Route { - method(Method::POST) -} +method_route!(get, GET); +method_route!(post, POST); +method_route!(put, PUT); +method_route!(patch, PATCH); +method_route!(delete, DELETE); +method_route!(head, HEAD); +method_route!(trace, TRACE); -/// Create *route* with `PUT` method guard. +/// Creates a new route with specified method guard. /// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::put().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `PUT` route gets added: -/// * /{project_id} -/// -pub fn put() -> Route { - method(Method::PUT) -} - -/// Create *route* with `PATCH` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::patch().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `PATCH` route gets added: -/// * /{project_id} -/// -pub fn patch() -> Route { - method(Method::PATCH) -} - -/// Create *route* with `DELETE` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::delete().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `DELETE` route gets added: -/// * /{project_id} -/// -pub fn delete() -> Route { - method(Method::DELETE) -} - -/// Create *route* with `HEAD` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::head().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `HEAD` route gets added: -/// * /{project_id} -/// -pub fn head() -> Route { - method(Method::HEAD) -} - -/// Create *route* with `TRACE` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::trace().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `HEAD` route gets added: -/// * /{project_id} -/// -pub fn trace() -> Route { - method(Method::TRACE) -} - -/// Create *route* and add method guard. +/// # Examples +/// In this example, one `GET /{project_id}` route is set up: /// /// ``` /// use actix_web::{web, http, App, HttpResponse}; @@ -221,15 +121,11 @@ pub fn trace() -> Route { /// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) /// ); /// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// pub fn method(method: Method) -> Route { Route::new().method(method) } -/// Create a new route and add handler. +/// Creates a new any-method route with handler. /// /// ``` /// use actix_web::{web, App, HttpResponse, Responder}; @@ -253,7 +149,7 @@ where Route::new().to(handler) } -/// Create raw service for a specific path. +/// Creates a raw service for a specific path. /// /// ``` /// use actix_web::{dev, web, guard, App, Error, HttpResponse}; @@ -272,8 +168,8 @@ pub fn service(path: T) -> WebService { WebService::new(path) } -/// Execute blocking function on a thread pool, returns future that resolves -/// to result of the function execution. +/// Executes blocking function on a thread pool, returns future that resolves to result of the +/// function execution. pub fn block(f: F) -> impl Future> where F: FnOnce() -> R + Send + 'static, From 1383c7d701c35df45abc425e70dae69d9bab1317 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 8 Sep 2021 17:42:14 +0100 Subject: [PATCH 20/99] speed up ci --- .github/workflows/ci.yml | 2 ++ Cargo.toml | 4 ++++ src/web.rs | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64750157..1ec034bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: runs-on: ${{ matrix.target.os }} env: + CI: 1 + CARGO_INCREMENTAL: 0 VCPKGRS_DYNAMIC: 1 steps: diff --git a/Cargo.toml b/Cargo.toml index 699717b4..05ed2eb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,10 @@ rcgen = "0.8" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.19.0" } +[profile.dev] +# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. +debug = 0 + [profile.release] lto = true opt-level = 3 diff --git a/src/web.rs b/src/web.rs index 40d7636c..e9f5c851 100644 --- a/src/web.rs +++ b/src/web.rs @@ -80,10 +80,10 @@ pub fn route() -> Route { macro_rules! method_route { ($method_fn:ident, $method_const:ident) => { paste::paste! { - #[doc = "Creates a new route with `" $method_const "` method guard."] + #[doc = " Creates a new route with `" $method_const "` method guard."] /// /// # Examples - #[doc = "In this example, one `" $method_const " /{project_id}` route is set up:"] + #[doc = " In this example, one `" $method_const " /{project_id}` route is set up:"] /// ``` /// use actix_web::{web, App, HttpResponse}; /// From 8dd30611faf552108638e7719025cbe5ac3d76e8 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 8 Sep 2021 19:42:40 -0400 Subject: [PATCH 21/99] accept owned strings in TestRequest::param (#2172) * accept owned strings in TestRequest::param * bump actix-router to 0.4.0 * update changelog Co-authored-by: Rob Ede --- CHANGES.md | 2 ++ src/test.rs | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6826be07..33898794 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,14 @@ ### Changed * Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] * Move `BaseHttpResponse` to `dev::Response`. [#2379] +* Enable `TestRequest::param` to accept more than just static strings. [#2172] * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Fix quality parse error in Accept-Encoding header. [#2344] * Re-export correct type at `web::HttpResponse`. [#2379] +[#2172]: https://github.com/actix/actix-web/pull/2172 [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 [#2379]: https://github.com/actix/actix-web/pull/2379 diff --git a/src/test.rs b/src/test.rs index 34dd6f2d..99e70859 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,6 @@ //! Various helpers for Actix applications to use during testing. -use std::{net::SocketAddr, rc::Rc}; +use std::{borrow::Cow, net::SocketAddr, rc::Rc}; pub use actix_http::test::TestBuffer; use actix_http::{ @@ -470,19 +470,31 @@ impl TestRequest { self } - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + /// Set request path pattern parameter. + /// + /// # Examples + /// ``` + /// use actix_web::test::TestRequest; + /// + /// let req = TestRequest::default().param("foo", "bar"); + /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); + /// ``` + pub fn param( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { self.path.add_static(name, value); self } - /// Set peer addr + /// Set peer addr. pub fn peer_addr(mut self, addr: SocketAddr) -> Self { self.peer_addr = Some(addr); self } - /// Set request payload + /// Set request payload. pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); self From ba88d3b4bf1cc3cccfd17d53f907422257e16944 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 9 Sep 2021 01:35:41 +0100 Subject: [PATCH 22/99] prepare actix-web beta.9 releases (#2381) * prepare actix-router release 0.5.0-beta.2 * prepare actix-web-codegen release 0.5.0-beta.4 * prepare actix-http release 3.0.0-beta.10 * prepare awc release 3.0.0-beta.8 * prepare actix-web release 4.0.0-beta.9 * prepare actix-http-test release 3.0.0-beta.6 * prepare actix-test release 0.1.0-beta.4 * prepare actix-files release 0.6.0-beta.7 * prepare actix-multipart release 0.4.0-beta.6 * prepare actix-web-actors release 4.0.0-beta.7 * fix http test version * re-add patch * update router repo url * fix http test readme version --- CHANGES.md | 3 +++ Cargo.toml | 10 +++++----- README.md | 4 ++-- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 8 ++++---- actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 3 +++ actix-http-test/Cargo.toml | 20 ++++++++++---------- actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 5 ++++- actix-http/Cargo.toml | 4 ++-- actix-http/README.md | 4 ++-- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 8 +++----- actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 3 +++ actix-router/Cargo.toml | 4 ++-- actix-test/CHANGES.md | 3 +++ actix-test/Cargo.toml | 10 +++++----- actix-web-actors/CHANGES.md | 3 +++ actix-web-actors/Cargo.toml | 8 ++++---- actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 3 +++ actix-web-codegen/Cargo.toml | 6 +++--- actix-web-codegen/README.md | 4 ++-- awc/CHANGES.md | 4 ++++ awc/Cargo.toml | 10 +++++----- awc/README.md | 4 ++-- 28 files changed, 91 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 33898794..398ac477 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.9 - 2021-09-09 ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] diff --git a/Cargo.toml b/Cargo.toml index 05ed2eb2..60525f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.8" +version = "4.0.0-beta.9" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -69,15 +69,15 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.1" -actix-router = "0.5.0-beta.1" +actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" 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.9" +actix-web-codegen = "0.5.0-beta.4" +actix-http = "3.0.0-beta.10" ahash = "0.7" bytes = "1" @@ -106,7 +106,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.7", features = ["openssl"] } +awc = { version = "3.0.0-beta.8", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index 33784d66..13ec3a01 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 533f7229..6d1512c2 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.7 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index ef288215..eccf49a7 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.6" +version = "0.6.0-beta.7" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false } -actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-http = "3.0.0-beta.10" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.8" +actix-web = "4.0.0-beta.9" actix-test = "0.1.0-beta.3" diff --git a/actix-files/README.md b/actix-files/README.md index 5815ef56..31bbd036 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 39b6a3a6..69e96f98 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.5 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index c04b5da4..e7fe7adc 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.4" +version = "3.0.0-beta.5" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" -readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http-test/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" -exclude = [".gitignore", ".cargo/config"] edition = "2018" [package.metadata.docs.rs] @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.5" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.7", default-features = false } +awc = { version = "3.0.0-beta.8", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.10" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 099fb385..f75b9c13 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 65206cf9..775b9e6d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.10 - 2021-09-09 ### Changed * `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] * Minimum supported Rust version (MSRV) is now 1.51. @@ -16,7 +19,7 @@ [#2377]: https://github.com/actix/actix-web/pull/2377 -## 3.0.0-beta.8 - 2021-08-09 +## 3.0.0-beta.9 - 2021-08-09 ### Fixed * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 54505a21..0e0da8f4 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -86,7 +86,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" -actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-http/README.md b/actix-http/README.md index c509eaff..b58b47f5 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.9)](https://docs.rs/actix-http/3.0.0-beta.9) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http/3.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.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.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) [![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-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 1e768ddf..c32583f0 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.6 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 5103407c..6db81cca 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.5" +version = "0.4.0-beta.6" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" -readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-multipart" license = "MIT OR Apache-2.0" edition = "2018" @@ -16,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-web = { version = "4.0.0-beta.9", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -31,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.10" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index aed16721..f3366f50 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 99038251..00190343 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.2 - 2021-09-09 * Introduce `ResourceDef::join`. [#380] * Disallow prefix routes with tail segments. [#379] * Enforce path separators on dynamic prefixes. [#378] diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 2a2ce1cc..e32f0edd 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", @@ -8,7 +8,7 @@ authors = [ ] description = "Resource path matching and router" keywords = ["actix", "router", "routing"] -repository = "https://github.com/actix/actix-net.git" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index dc76ba3f..58e05c4b 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.4 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index b732cf74..41d32257 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.3" +version = "0.1.0-beta.4" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -20,13 +20,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.8" -actix-http-test = { version = "3.0.0-beta.4", features = [] } +actix-http = "3.0.0-beta.10" +actix-http-test = "3.0.0-beta.5" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.7", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.8", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 084e7b27..2e453063 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.7 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index fcb5195b..ef6bd919 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.6" +version = "4.0.0-beta.7" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.8" -actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.9", default-features = false } bytes = "1" bytestring = "1" @@ -29,6 +29,6 @@ tokio = { version = "1", features = ["sync"] } actix-rt = "2.2" actix-test = "0.1.0-beta.3" -awc = { version = "3.0.0-beta.7", default-features = false } +awc = { version = "3.0.0-beta.8", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 2858d3f2..a647e4bc 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index f0a56b30..c154d8af 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.4 - 2021-09-09 * In routing macros, paths are now validated at compile time. [#2350] * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 66f7acf6..2ad714f4 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.3" +version = "0.5.0-beta.4" description = "Routing and runtime macros for Actix Web" readme = "README.md" homepage = "https://actix.rs" @@ -17,13 +17,13 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" -actix-router = "0.5.0-beta.1" +actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.8" +actix-web = "4.0.0-beta.9" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index e69cfbbe..268e8b01 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 9c6f258a..252b62ef 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,11 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.8 - 2021-09-09 ### Changed * Send headers within the redirect requests. [#2310] [#2310]: https://github.com/actix/actix-web/pull/2310 + ## 3.0.0-beta.7 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 016d3b48..262c3dce 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.7" +version = "3.0.0-beta.8" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.10" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" @@ -77,9 +77,9 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.8", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.8", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } diff --git a/awc/README.md b/awc/README.md index fe91383c..868bc5ca 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.7)](https://docs.rs/awc/3.0.0-beta.7) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.8)](https://docs.rs/awc/3.0.0-beta.8) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.7/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.7) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.8/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.8) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 46699e34299ed14401df8ea8022efe47a83041e0 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 11 Sep 2021 00:01:01 +0100 Subject: [PATCH 23/99] remove time dep from actix-http (#2383) --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 1 - actix-http-test/src/lib.rs | 11 ++- actix-http/Cargo.toml | 7 +- actix-http/src/config.rs | 29 +++---- actix-http/src/header/shared/http_date.rs | 82 +++++++++++++++++++ actix-http/src/header/shared/httpdate.rs | 97 ----------------------- actix-http/src/header/shared/mod.rs | 4 +- actix-http/src/lib.rs | 1 - actix-http/src/time_parser.rs | 72 ----------------- actix-http/tests/test_server.rs | 1 + src/middleware/logger.rs | 6 +- 12 files changed, 109 insertions(+), 204 deletions(-) create mode 100644 actix-http/src/header/shared/http_date.rs delete mode 100644 actix-http/src/header/shared/httpdate.rs delete mode 100644 actix-http/src/time_parser.rs diff --git a/Cargo.toml b/Cargo.toml index 60525f3a..73a52182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ serde_json = "1.0" serde_urlencoded = "0.7" smallvec = "1.6.1" socket2 = "0.4.0" -time = { version = "0.2.23", default-features = false, features = ["std"] } +time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index e7fe7adc..ee4971a1 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -47,7 +47,6 @@ serde = "1.0" serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" -time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 0f126c99..ec7b46ff 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -7,8 +7,7 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -use std::sync::mpsc; -use std::{net, thread, time}; +use std::{net, sync::mpsc, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; @@ -95,15 +94,15 @@ pub async fn test_server_with_addr>( .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) .ssl(builder.build()) } #[cfg(not(feature = "openssl"))] { Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) } }; diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 0e0da8f4..889c9133 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -60,6 +60,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["alloc h2 = "0.3.1" http = "0.2.2" httparse = "1.5.1" +httpdate = "1.0.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" @@ -70,11 +71,8 @@ percent-encoding = "2.1" pin-project = "1.0.0" pin-project-lite = "0.2" rand = "0.8" -regex = "1.3" -serde = "1.0" sha-1 = "0.9" smallvec = "1.6.1" -time = { version = "0.2.23", default-features = false, features = ["std"] } tokio = { version = "1.2", features = ["sync"] } # compression @@ -92,11 +90,12 @@ async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" rcgen = "0.8" +regex = "1.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } -webpki = { version = "0.21.0" } +webpki = { version = "0.21" } [[example]] name = "ws" diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 97750ff7..069099b8 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -1,18 +1,19 @@ -use std::cell::Cell; -use std::fmt::Write; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, net}; +use std::{ + cell::Cell, + fmt::{self, Write}, + net, + rc::Rc, + time::{Duration, SystemTime}, +}; use actix_rt::{ task::JoinHandle, time::{interval, sleep_until, Instant, Sleep}, }; use bytes::BytesMut; -use time::OffsetDateTime; /// "Sun, 06 Nov 1994 08:49:37 GMT".len() -const DATE_VALUE_LENGTH: usize = 29; +pub(crate) const DATE_VALUE_LENGTH: usize = 29; #[derive(Debug, PartialEq, Clone, Copy)] /// Server keep-alive setting @@ -206,12 +207,7 @@ impl Date { fn update(&mut self) { self.pos = 0; - write!( - self, - "{}", - OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT") - ) - .unwrap(); + write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); } } @@ -269,11 +265,11 @@ impl DateService { } // TODO: move to a util module for testing all spawn handle drop style tasks. -#[cfg(test)] /// Test Module for checking the drop state of certain async tasks that are spawned /// with `actix_rt::spawn` /// /// The target task must explicitly generate `NotifyOnDrop` when spawn the task +#[cfg(test)] mod notify_on_drop { use std::cell::RefCell; @@ -283,9 +279,8 @@ mod notify_on_drop { /// Check if the spawned task is dropped. /// - /// # Panic: - /// - /// When there was no `NotifyOnDrop` instance on current thread + /// # Panics + /// Panics when there was no `NotifyOnDrop` instance on current thread. pub(crate) fn is_dropped() -> bool { NOTIFY_DROPPED.with(|bool| { bool.borrow() diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs new file mode 100644 index 00000000..3441f90a --- /dev/null +++ b/actix-http/src/header/shared/http_date.rs @@ -0,0 +1,82 @@ +use std::{fmt, io::Write, str::FromStr, time::SystemTime}; + +use bytes::BytesMut; +use http::header::{HeaderValue, InvalidHeaderValue}; + +use crate::{ + config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, + helpers::MutWriter, +}; + +/// A timestamp with HTTP formatting and parsing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HttpDate(SystemTime); + +impl FromStr for HttpDate { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match httpdate::parse_http_date(s) { + Ok(sys_time) => Ok(HttpDate(sys_time)), + Err(_) => Err(ParseError::Header), + } + } +} + +impl fmt::Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let date_str = httpdate::fmt_http_date(self.0); + f.write_str(&date_str) + } +} + +impl IntoHeaderValue for HttpDate { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH); + let mut wrt = MutWriter(&mut buf); + + // unwrap: date output is known to be well formed and of known length + write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap(); + + HeaderValue::from_maybe_shared(buf.split().freeze()) + } +} + +impl From for HttpDate { + fn from(sys_time: SystemTime) -> HttpDate { + HttpDate(sys_time) + } +} + +impl From for SystemTime { + fn from(HttpDate(sys_time): HttpDate) -> SystemTime { + sys_time + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + #[test] + fn date_header() { + macro_rules! assert_parsed_date { + ($case:expr, $exp:expr) => { + assert_eq!($case.parse::().unwrap(), $exp); + }; + } + + // 784198117 = SystemTime::from(datetime!(1994-11-07 08:48:37).assume_utc()).duration_since(SystemTime::UNIX_EPOCH)); + let nov_07 = HttpDate(SystemTime::UNIX_EPOCH + Duration::from_secs(784198117)); + + assert_parsed_date!("Mon, 07 Nov 1994 08:48:37 GMT", nov_07); + assert_parsed_date!("Monday, 07-Nov-94 08:48:37 GMT", nov_07); + assert_parsed_date!("Mon Nov 7 08:48:37 1994", nov_07); + + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/actix-http/src/header/shared/httpdate.rs b/actix-http/src/header/shared/httpdate.rs deleted file mode 100644 index 18278a6d..00000000 --- a/actix-http/src/header/shared/httpdate.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{ - fmt, - io::Write, - str::FromStr, - time::{SystemTime, UNIX_EPOCH}, -}; - -use bytes::buf::BufMut; -use bytes::BytesMut; -use http::header::{HeaderValue, InvalidHeaderValue}; -use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset}; - -use crate::error::ParseError; -use crate::header::IntoHeaderValue; -use crate::time_parser; - -/// A timestamp with HTTP formatting and parsing. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(OffsetDateTime); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match time_parser::parse_http_date(s) { - Some(t) => Ok(HttpDate(t.assume_utc())), - None => Err(ParseError::Header), - } - } -} - -impl fmt::Display for HttpDate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - HttpDate(PrimitiveDateTime::from(sys).assume_utc()) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - write!( - wrt, - "{}", - self.0 - .to_offset(UtcOffset::UTC) - .format("%a, %d %b %Y %H:%M:%S GMT") - ) - .unwrap(); - HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze()) - } -} - -impl From for SystemTime { - fn from(date: HttpDate) -> SystemTime { - let dt = date.0; - let epoch = OffsetDateTime::unix_epoch(); - - UNIX_EPOCH + (dt - epoch) - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - use time::{date, time, PrimitiveDateTime}; - - #[test] - fn test_date() { - let nov_07 = HttpDate( - PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(), - ); - - assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - nov_07 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - nov_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - nov_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/actix-http/src/header/shared/mod.rs b/actix-http/src/header/shared/mod.rs index b8f9173f..274e1314 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -3,12 +3,12 @@ mod charset; mod content_encoding; mod extended; -mod httpdate; +mod http_date; mod quality_item; pub use self::charset::Charset; pub use self::content_encoding::ContentEncoding; pub use self::extended::{parse_extended_value, ExtendedValue}; -pub use self::httpdate::HttpDate; +pub use self::http_date::HttpDate; pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use language_tags::LanguageTag; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 17ee3ff2..3ad8d095 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -44,7 +44,6 @@ mod request; mod response; mod response_builder; mod service; -mod time_parser; pub mod error; pub mod h1; diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs deleted file mode 100644 index fd82fd42..00000000 --- a/actix-http/src/time_parser.rs +++ /dev/null @@ -1,72 +0,0 @@ -use time::{Date, OffsetDateTime, PrimitiveDateTime}; - -/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. -pub(crate) fn parse_http_date(time: &str) -> Option { - try_parse_rfc_1123(time) - .or_else(|| try_parse_rfc_850(time)) - .or_else(|| try_parse_asctime(time)) -} - -/// Attempt to parse a `time` string as a RFC 1123 formatted date time string. -/// -/// Eg: `Fri, 12 Feb 2021 00:14:29 GMT` -fn try_parse_rfc_1123(time: &str) -> Option { - time::parse(time, "%a, %d %b %Y %H:%M:%S").ok() -} - -/// Attempt to parse a `time` string as a RFC 850 formatted date time string. -/// -/// Eg: `Wednesday, 11-Jan-21 13:37:41 UTC` -fn try_parse_rfc_850(time: &str) -> Option { - let dt = PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S").ok()?; - - // If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3, - // we consider the year as part of this century if it's within the next 50 years, - // otherwise we consider as part of the previous century. - - let now = OffsetDateTime::now_utc(); - let century_start_year = (now.year() / 100) * 100; - let mut expanded_year = century_start_year + dt.year(); - - if expanded_year > now.year() + 50 { - expanded_year -= 100; - } - - let date = Date::try_from_ymd(expanded_year, dt.month(), dt.day()).ok()?; - Some(PrimitiveDateTime::new(date, dt.time())) -} - -/// Attempt to parse a `time` string using ANSI C's `asctime` format. -/// -/// Eg: `Wed Feb 13 15:46:11 2013` -fn try_parse_asctime(time: &str) -> Option { - time::parse(time, "%a %b %_d %H:%M:%S %Y").ok() -} - -#[cfg(test)] -mod tests { - use time::{date, time}; - - use super::*; - - #[test] - fn test_rfc_850_year_shift() { - let date = try_parse_rfc_850("Friday, 19-Nov-82 16:14:55 EST").unwrap(); - assert_eq!(date, date!(1982 - 11 - 19).with_time(time!(16:14:55))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-62 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2062 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-21 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2021 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-23 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2023 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-99 13:37:41 EST").unwrap(); - assert_eq!(date, date!(1999 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-00 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2000 - 01 - 11).with_time(time!(13:37:41))); - } -} diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 1e6d0b63..c04aeae0 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -183,6 +183,7 @@ async fn test_chunked_payload() { Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), None => panic!("Failed to find size in HTTP Response: {}", data), }; + size }; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 9574b02f..961eca49 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -18,7 +18,7 @@ use bytes::Bytes; use futures_core::ready; use log::{debug, warn}; use regex::{Regex, RegexSet}; -use time::OffsetDateTime; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ dev::{BodySize, MessageBody}, @@ -538,7 +538,7 @@ impl FormatText { }; } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), - FormatText::RequestTime => *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")), + FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -767,7 +767,7 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); + assert!(s.contains(&now.format(&Rfc3339).unwrap())); } #[actix_rt::test] From 8ae278cb68eda1e6c4fbd3463b018e0f0fe1c313 Mon Sep 17 00:00:00 2001 From: Arniu Tseng Date: Sat, 11 Sep 2021 08:11:16 +0800 Subject: [PATCH 24/99] Remove `FromRequest::Config` (#2233) Co-authored-by: Jonas Platte Co-authored-by: Igor Aleksanov Co-authored-by: Rob Ede --- CHANGES.md | 3 ++ Cargo.toml | 1 + MIGRATION.md | 2 ++ actix-files/src/path_buf.rs | 1 - actix-multipart/src/extractor.rs | 1 - src/data.rs | 1 - src/extract.rs | 55 ++++++++++++++++++++------------ src/info.rs | 2 -- src/request.rs | 1 - src/request_data.rs | 1 - src/types/either.rs | 1 - src/types/form.rs | 30 +++++++++-------- src/types/header.rs | 1 - src/types/json.rs | 1 - src/types/path.rs | 3 +- src/types/payload.rs | 17 ++++------ src/types/query.rs | 3 +- 17 files changed, 66 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 398ac477..d8831602 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Asscociated type `FromRequest::Config` was removed. [#2233] +[#2233]: https://github.com/actix/actix-web/pull/2233 ## 4.0.0-beta.9 - 2021-09-09 ### Added diff --git a/Cargo.toml b/Cargo.toml index 73a52182..dc7e9af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] +rustdoc-args = ["--cfg", "docsrs"] [lib] name = "actix_web" diff --git a/MIGRATION.md b/MIGRATION.md index 9a70adb9..d53bd7bf 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -11,6 +11,8 @@ Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. +* The `type Config` of `FromRequest` was removed. + * Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). By default all compression algorithms are enabled. To select algorithm you want to include with `middleware::Compress` use following flags: diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 8a87acd5..76f58930 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -59,7 +59,6 @@ impl AsRef for PathBufWrap { impl FromRequest for PathBufWrap { type Error = UriSegmentError; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ready(req.match_info().path().parse()) diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index c87f8cc2..1ad1f203 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -33,7 +33,6 @@ use crate::server::Multipart; impl FromRequest for Multipart { type Error = Error; type Future = Ready>; - type Config = (); #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { diff --git a/src/data.rs b/src/data.rs index 174faba3..9d4fe084 100644 --- a/src/data.rs +++ b/src/data.rs @@ -120,7 +120,6 @@ where } impl FromRequest for Data { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/extract.rs b/src/extract.rs index 592f7ab8..39062dd1 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -13,13 +13,42 @@ use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; -/// Trait implemented by types that can be extracted from request. +/// A type that implements [`FromRequest`] is called an **extractor** and can extract data +/// from the request. Examples of types that implement this trait are [`Json`], [`Form`], [`Path`]. /// -/// Types that implement this trait can be used with `Route` handlers. +/// An extractor can be customized by injecting the corresponding configuration with one of: +/// +/// - [`App::app_data()`](`crate::App::app_data`) +/// - [`Scope::app_data()`](`crate::Scope::app_data`) +/// - [`Resource::app_data()`](`crate::Resource::app_data`) +/// +/// Here are some built-in extractors and their corresponding configuration. +/// Please refer to the respective documentation for details. +/// +/// | Extractor | Configuration | +/// |-------------|-------------------| +/// | [`Json`] | [`JsonConfig`] | +/// | [`Form`] | [`FormConfig`] | +/// | [`Path`] | [`PathConfig`] | +/// | [`Query`] | [`QueryConfig`] | +/// | [`Payload`] | [`PayloadConfig`] | +/// | [`String`] | [`PayloadConfig`] | +/// | [`Bytes`] | [`PayloadConfig`] | +/// +/// [`Json`]: crate::web::Json +/// [`JsonConfig`]: crate::web::JsonConfig +/// [`Form`]: crate::web::Form +/// [`FormConfig`]: crate::web::FormConfig +/// [`Path`]: crate::web::Path +/// [`PathConfig`]: crate::web::PathConfig +/// [`Query`]: crate::web::Query +/// [`QueryConfig`]: crate::web::QueryConfig +/// [`Payload`]: crate::web::Payload +/// [`PayloadConfig`]: crate::web::PayloadConfig +/// [`String`]: FromRequest#impl-FromRequest-for-String +/// [`Bytes`]: crate::web::Bytes#impl-FromRequest +#[cfg_attr(docsrs, doc(alias = "Extractor"))] pub trait FromRequest: Sized { - /// Configuration for this extractor. - type Config: Default + 'static; - /// The associated error which can be returned. type Error: Into; @@ -35,14 +64,6 @@ pub trait FromRequest: Sized { fn extract(req: &HttpRequest) -> Self::Future { Self::from_request(req, &mut Payload::None) } - - /// Create and configure config instance. - fn configure(f: F) -> Self::Config - where - F: FnOnce(Self::Config) -> Self::Config, - { - f(Self::Config::default()) - } } /// Optionally extract a field from the request @@ -65,7 +86,6 @@ pub trait FromRequest: Sized { /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -100,7 +120,6 @@ where { type Error = Error; type Future = FromRequestOptFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -156,7 +175,6 @@ where /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -189,7 +207,6 @@ where { type Error = Error; type Future = FromRequestResFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -233,7 +250,6 @@ where impl FromRequest for Uri { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.uri().clone()) @@ -255,7 +271,6 @@ impl FromRequest for Uri { impl FromRequest for Method { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.method().clone()) @@ -266,7 +281,6 @@ impl FromRequest for Method { impl FromRequest for () { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { ok(()) @@ -306,7 +320,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type<$($T),+>; - type Config = ($($T::Config),+); fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { $fut_type { diff --git a/src/info.rs b/src/info.rs index de8ad67e..d928a1e6 100644 --- a/src/info.rs +++ b/src/info.rs @@ -209,7 +209,6 @@ impl ConnectionInfo { impl FromRequest for ConnectionInfo { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.connection_info().clone()) @@ -252,7 +251,6 @@ impl ResponseError for MissingPeerAddr {} impl FromRequest for PeerAddr { type Error = MissingPeerAddr; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match req.peer_addr() { diff --git a/src/request.rs b/src/request.rs index c25a5397..0027f9b4 100644 --- a/src/request.rs +++ b/src/request.rs @@ -358,7 +358,6 @@ impl Drop for HttpRequest { /// } /// ``` impl FromRequest for HttpRequest { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/request_data.rs b/src/request_data.rs index 58194301..575dc1eb 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -64,7 +64,6 @@ impl Deref for ReqData { } impl FromRequest for ReqData { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/types/either.rs b/src/types/either.rs index 35e63cec..5700b63c 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -187,7 +187,6 @@ where { type Error = EitherExtractError; type Future = EitherExtractFut; - type Config = (); fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { EitherExtractFut { diff --git a/src/types/form.rs b/src/types/form.rs index 2ace0e06..71100eb9 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -126,20 +126,12 @@ impl FromRequest for Form where T: DeserializeOwned + 'static, { - type Config = FormConfig; type Error = Error; type Future = FormExtractFut; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let (limit, err_handler) = req - .app_data::() - .or_else(|| { - req.app_data::>() - .map(|d| d.as_ref()) - }) - .map(|c| (c.limit, c.err_handler.clone())) - .unwrap_or((16384, None)); + let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone(); FormExtractFut { fut: UrlEncoded::new(req, payload).limit(limit), @@ -241,14 +233,26 @@ impl FormConfig { self.err_handler = Some(Rc::new(f)); self } + + /// Extract payload config from app data. + /// + /// Checks both `T` and `Data`, in that order, and falls back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } } +/// Allow shared refs used as default. +const DEFAULT_CONFIG: FormConfig = FormConfig { + limit: 16_384, // 2^14 bytes (~16kB) + err_handler: None, +}; + impl Default for FormConfig { fn default() -> Self { - FormConfig { - limit: 16_384, // 2^14 bytes (~16kB) - err_handler: None, - } + DEFAULT_CONFIG } } diff --git a/src/types/header.rs b/src/types/header.rs index 9b64f445..6ea77faf 100644 --- a/src/types/header.rs +++ b/src/types/header.rs @@ -62,7 +62,6 @@ where { type Error = ParseError; type Future = Ready>; - type Config = (); #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { diff --git a/src/types/json.rs b/src/types/json.rs index 8c2f51a6..19443ea9 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -130,7 +130,6 @@ impl Responder for Json { impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; - type Config = JsonConfig; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { diff --git a/src/types/path.rs b/src/types/path.rs index 4052646e..aed897fa 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -97,12 +97,11 @@ where { type Error = Error; type Future = Ready>; - type Config = PathConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() + .app_data::() .and_then(|c| c.ehandler.clone()); ready( diff --git a/src/types/payload.rs b/src/types/payload.rs index 188da620..46ad96be 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -63,7 +63,6 @@ impl Stream for Payload { /// See [here](#usage) for example of usage as an extractor. impl FromRequest for Payload { - type Config = PayloadConfig; type Error = Error; type Future = Ready>; @@ -90,7 +89,6 @@ impl FromRequest for Payload { /// } /// ``` impl FromRequest for Bytes { - type Config = PayloadConfig; type Error = Error; type Future = Either>>; @@ -126,8 +124,7 @@ impl<'a> Future for BytesExtractFut { /// /// Text extractor automatically decode body according to the request's charset. /// -/// [**PayloadConfig**](PayloadConfig) allows to configure -/// extraction process. +/// Use [`PayloadConfig`] to configure extraction process. /// /// # Examples /// ``` @@ -139,7 +136,6 @@ impl<'a> Future for BytesExtractFut { /// format!("Body {}!", text) /// } impl FromRequest for String { - type Config = PayloadConfig; type Error = Error; type Future = Either>>; @@ -198,14 +194,15 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result fmt::Display for Query { impl FromRequest for Query { type Error = Error; type Future = Ready>; - type Config = QueryConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() + .app_data::() .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) From 450ff5fa1dbbf9aa9adb68f711ed5e3b53445bab Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 11 Sep 2021 16:48:47 +0100 Subject: [PATCH 25/99] improve extract docs (#2384) --- CHANGES.md | 5 ++++- src/extract.rs | 28 +++++++++++++++++++--------- src/types/form.rs | 2 +- src/types/json.rs | 2 +- src/types/path.rs | 2 +- src/types/payload.rs | 3 ++- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d8831602..e2bd6ec8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,12 @@ ## Unreleased - 2021-xx-xx ### Changed -* Asscociated type `FromRequest::Config` was removed. [#2233] +* Associated type `FromRequest::Config` was removed. [#2233] +* Inner field made private on `web::Payload`. [#????] [#2233]: https://github.com/actix/actix-web/pull/2233 +[#????]: https://github.com/actix/actix-web/pull/???? + ## 4.0.0-beta.9 - 2021-09-09 ### Added diff --git a/src/extract.rs b/src/extract.rs index 39062dd1..29fd0d05 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -13,28 +13,37 @@ use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; -/// A type that implements [`FromRequest`] is called an **extractor** and can extract data -/// from the request. Examples of types that implement this trait are [`Json`], [`Form`], [`Path`]. +/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from +/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`]. /// +/// # Configuration /// An extractor can be customized by injecting the corresponding configuration with one of: /// -/// - [`App::app_data()`](`crate::App::app_data`) -/// - [`Scope::app_data()`](`crate::Scope::app_data`) -/// - [`Resource::app_data()`](`crate::Resource::app_data`) +/// - [`App::app_data()`][crate::App::app_data] +/// - [`Scope::app_data()`][crate::Scope::app_data] +/// - [`Resource::app_data()`][crate::Resource::app_data] /// /// Here are some built-in extractors and their corresponding configuration. /// Please refer to the respective documentation for details. /// /// | Extractor | Configuration | /// |-------------|-------------------| +/// | [`Header`] | _None_ | +/// | [`Path`] | [`PathConfig`] | /// | [`Json`] | [`JsonConfig`] | /// | [`Form`] | [`FormConfig`] | -/// | [`Path`] | [`PathConfig`] | /// | [`Query`] | [`QueryConfig`] | -/// | [`Payload`] | [`PayloadConfig`] | -/// | [`String`] | [`PayloadConfig`] | /// | [`Bytes`] | [`PayloadConfig`] | +/// | [`String`] | [`PayloadConfig`] | +/// | [`Payload`] | [`PayloadConfig`] | /// +/// # Implementing An Extractor +/// To reduce duplicate code in handlers where extracting certain parts of a request has a common +/// structure, you can implement `FromRequest` for your own types. +/// +/// Note that the request payload can only be consumed by one extractor. +/// +/// [`Header`]: crate::web::Header /// [`Json`]: crate::web::Json /// [`JsonConfig`]: crate::web::JsonConfig /// [`Form`]: crate::web::Form @@ -47,7 +56,8 @@ use crate::{dev::Payload, Error, HttpRequest}; /// [`PayloadConfig`]: crate::web::PayloadConfig /// [`String`]: FromRequest#impl-FromRequest-for-String /// [`Bytes`]: crate::web::Bytes#impl-FromRequest -#[cfg_attr(docsrs, doc(alias = "Extractor"))] +/// [`Either`]: crate::web::Either +#[doc(alias = "extract", alias = "extractor")] pub trait FromRequest: Sized { /// The associated error which can be returned. type Error: Into; diff --git a/src/types/form.rs b/src/types/form.rs index 71100eb9..098a864d 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -32,7 +32,7 @@ use crate::{ /// To extract typed data from a request body, the inner type `T` must implement the /// [`DeserializeOwned`] trait. /// -/// Use [`FormConfig`] to configure extraction process. +/// Use [`FormConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web}; diff --git a/src/types/json.rs b/src/types/json.rs index 19443ea9..df01fdb3 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -34,7 +34,7 @@ use crate::{ /// To extract typed data from a request body, the inner type `T` must implement the /// [`serde::Deserialize`] trait. /// -/// Use [`JsonConfig`] to configure extraction process. +/// Use [`JsonConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web, App}; diff --git a/src/types/path.rs b/src/types/path.rs index aed897fa..b58aec18 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -14,7 +14,7 @@ use crate::{ /// Extract typed data from request path segments. /// -/// Use [`PathConfig`] to configure extraction process. +/// Use [`PathConfig`] to configure extraction option. /// /// # Examples /// ``` diff --git a/src/types/payload.rs b/src/types/payload.rs index 46ad96be..00047e8b 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -43,10 +43,11 @@ use crate::{ /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` -pub struct Payload(pub crate::dev::Payload); +pub struct Payload(crate::dev::Payload); impl Payload { /// Unwrap to inner Payload type. + #[inline] pub fn into_inner(self) -> crate::dev::Payload { self.0 } From efefa0d0ce79e24acc79fb0333369e7ab6d79c41 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 11 Sep 2021 09:27:50 -0700 Subject: [PATCH 26/99] web: add option to not require content type header for Json (#2362) Co-authored-by: Rob Ede --- CHANGES.md | 8 ++++-- src/types/json.rs | 68 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2bd6ec8..439dd054 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,16 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Option to allow `Json` extractor to work without a `Content-Length` header present. [#2362] + ### Changed * Associated type `FromRequest::Config` was removed. [#2233] -* Inner field made private on `web::Payload`. [#????] +* Inner field made private on `web::Payload`. [#2384] [#2233]: https://github.com/actix/actix-web/pull/2233 -[#????]: https://github.com/actix/actix-web/pull/???? +[#2362]: https://github.com/actix/actix-web/pull/2362 +[#2384]: https://github.com/actix/actix-web/pull/2384 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/src/types/json.rs b/src/types/json.rs index df01fdb3..6d07fe45 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -127,7 +127,7 @@ impl Responder for Json { } /// See [here](#extractor) for example of usage as an extractor. -impl FromRequest for Json { +impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; @@ -136,12 +136,13 @@ impl FromRequest for Json { let config = JsonConfig::from_req(req); let limit = config.limit; - let ctype = config.content_type.as_deref(); + let ctype_required = config.content_type_required; + let ctype_fn = config.content_type.as_deref(); let err_handler = config.err_handler.clone(); JsonExtractFut { req: Some(req.clone()), - fut: JsonBody::new(req, payload, ctype).limit(limit), + fut: JsonBody::new(req, payload, ctype_fn, ctype_required).limit(limit), err_handler, } } @@ -156,7 +157,7 @@ pub struct JsonExtractFut { err_handler: JsonErrorHandler, } -impl Future for JsonExtractFut { +impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -224,6 +225,7 @@ pub struct JsonConfig { limit: usize, err_handler: JsonErrorHandler, content_type: Option bool + Send + Sync>>, + content_type_required: bool, } impl JsonConfig { @@ -251,6 +253,12 @@ impl JsonConfig { self } + /// Sets whether or not the request must have a `Content-Type` header to be parsed. + pub fn content_type_required(mut self, content_type_required: bool) -> Self { + self.content_type_required = content_type_required; + self + } + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { @@ -267,6 +275,7 @@ const DEFAULT_CONFIG: JsonConfig = JsonConfig { limit: DEFAULT_LIMIT, err_handler: None, content_type: None, + content_type_required: true, }; impl Default for JsonConfig { @@ -277,15 +286,18 @@ impl Default for JsonConfig { /// Future that resolves to some `T` when parsed from a JSON payload. /// -/// Form can be deserialized from any type `T` that implements [`serde::Deserialize`]. +/// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize]. /// /// Returns error if: -/// - content type is not `application/json` -/// - content length is greater than [limit](JsonBody::limit()) +/// - `Content-Type` is not `application/json` when `ctype_required` (passed to [`new`][Self::new]) +/// is `true`. +/// - `Content-Length` is greater than [limit](JsonBody::limit()). +/// - The payload, when consumed, is not valid JSON. pub enum JsonBody { Error(Option), Body { limit: usize, + /// Length as reported by `Content-Length` header, if present. length: Option, #[cfg(feature = "__compress")] payload: Decompress, @@ -304,18 +316,21 @@ impl JsonBody { pub fn new( req: &HttpRequest, payload: &mut Payload, - ctype: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_fn: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_required: bool, ) -> Self { // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { + let can_parse_json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - || ctype.map_or(false, |predicate| predicate(mime)) + || ctype_fn.map_or(false, |predicate| predicate(mime)) } else { - false + // if `ctype_required` is false, assume payload is + // json even when content-type header is missing + !ctype_required }; - if !json { + if !can_parse_json { return JsonBody::Error(Some(JsonPayloadError::ContentType)); } @@ -325,7 +340,7 @@ impl JsonBody { .and_then(|l| l.to_str().ok()) .and_then(|s| s.parse::().ok()); - // Notice the content_length is not checked against limit of json config here. + // Notice the content-length is not checked against limit of json config here. // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. @@ -379,7 +394,7 @@ impl JsonBody { } } -impl Future for JsonBody { +impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -562,7 +577,7 @@ mod tests { #[actix_rt::test] async fn test_json_body() { let (req, mut pl) = TestRequest::default().to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -571,7 +586,7 @@ mod tests { header::HeaderValue::from_static("application/text"), )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -585,7 +600,7 @@ mod tests { )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None) + let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; assert!(json_eq( @@ -604,7 +619,7 @@ mod tests { .set_payload(Bytes::from_static(&[0u8; 1000])) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None) + let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; @@ -625,7 +640,7 @@ mod tests { .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert_eq!( json.ok().unwrap(), MyObject { @@ -695,6 +710,21 @@ mod tests { assert!(s.is_err()) } + #[actix_rt::test] + async fn test_json_with_no_content_type() { + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .app_data(JsonConfig::default().content_type_required(false)) + .to_http_parts(); + + let s = Json::::from_request(&req, &mut pl).await; + assert!(s.is_ok()) + } + #[actix_rt::test] async fn test_with_config_in_data_wrapper() { let (req, mut pl) = TestRequest::default() From a3806cde190ec27703576a91b470a6c8706cd5b1 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 12 Sep 2021 22:41:08 +0100 Subject: [PATCH 27/99] fix changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 439dd054..05055a51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased - 2021-xx-xx ### Added -* Option to allow `Json` extractor to work without a `Content-Length` header present. [#2362] +* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] ### Changed * Associated type `FromRequest::Config` was removed. [#2233] From a6707fb7ee401d248268a8ac28ca96a854ad797a Mon Sep 17 00:00:00 2001 From: Omid Rad Date: Mon, 11 Oct 2021 19:28:09 +0200 Subject: [PATCH 28/99] Remove checked_expr (#2401) --- CHANGES.md | 4 ++++ src/service.rs | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05055a51..848a908a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,9 +8,13 @@ * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] +### Removed +* `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] + [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 +[#2401]: https://github.com/actix/actix-web/pull/2401 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/src/service.rs b/src/service.rs index b9fa0e12..515d782d 100644 --- a/src/service.rs +++ b/src/service.rs @@ -393,16 +393,6 @@ impl ServiceResponse { self.response.headers_mut() } - /// Execute closure and in case of error convert it to response. - pub fn checked_expr(mut self, f: F) -> Result - where - F: FnOnce(&mut Self) -> Result<(), E>, - E: Into, - { - f(&mut self).map_err(Into::into)?; - Ok(self) - } - /// Extract response body pub fn into_body(self) -> B { self.response.into_body() From 99985fc4ecdc694182cbbcbb490f0e3e0f6701c6 Mon Sep 17 00:00:00 2001 From: James Rhodes <30299230+jarhodes314@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:35:33 +0100 Subject: [PATCH 29/99] web: implement `into_inner` for `Data` (#2407) --- CHANGES.md | 1 + src/data.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 848a908a..2509197f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] +* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] diff --git a/src/data.rs b/src/data.rs index 9d4fe084..7e01d346 100644 --- a/src/data.rs +++ b/src/data.rs @@ -75,7 +75,9 @@ impl Data { pub fn new(state: T) -> Data { Data(Arc::new(state)) } +} +impl Data { /// Get reference to inner app data. pub fn get_ref(&self) -> &T { self.0.as_ref() @@ -304,4 +306,38 @@ mod tests { let data_arc = Data::from(dyn_arc); assert_eq!(data_arc_box.get_num(), data_arc.get_num()) } + + #[actix_rt::test] + async fn test_dyn_data_into_arc() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let arc_from_data = data_arc.clone().into_inner(); + assert_eq!(data_arc.get_num(), arc_from_data.get_num()) + } + + #[actix_rt::test] + async fn test_get_ref_from_dyn_data() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let ref_data = data_arc.get_ref(); + assert_eq!(data_arc.get_num(), ref_data.get_num()) + } } From 6b3ea4fc619ea0c3951d1beefdb13c131e43fd7e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 14 Oct 2021 18:06:31 +0100 Subject: [PATCH 30/99] copy original route macro input with compile errors (#2410) --- actix-web-codegen/CHANGES.md | 3 +++ actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/src/route.rs | 17 +++++++++++++++-- .../trybuild/route-duplicate-method-fail.stderr | 4 ++-- .../trybuild/route-missing-method-fail.stderr | 4 ++-- .../route-unexpected-method-fail.stderr | 4 ++-- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index c154d8af..dae9830a 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Improve error recovery potential when macro input is invalid. [#2410] + +[#2410]: https://github.com/actix/actix-web/pull/2410 ## 0.5.0-beta.4 - 2021-09-09 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 2ad714f4..b8b346b8 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -21,6 +21,7 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" +actix-macros = "0.2.2" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.9" diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index c2f851a0..4d4af7ec 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -349,8 +349,21 @@ pub(crate) fn with_method( input: TokenStream, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, method) { + match Route::new(args, input.clone(), method) { Ok(route) => route.into_token_stream().into(), - Err(err) => err.to_compile_error().into(), + // on parse err, make IDEs happy; see fn docs + Err(err) => input_and_compile_error(input, err), } } + +/// Converts the error to a token stream and appends it to the original input. +/// +/// Returning the original input in addition to the error is good for IDEs which can gracefully +/// recover and show more precise errors within the macro body. +/// +/// See for more info. +fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream { + let compile_err = TokenStream::from(err.to_compile_error()); + item.extend(compile_err); + return item; +} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index abdc895d..90cff1b1 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -4,8 +4,8 @@ error: HTTP method defined more than once: `GET` 3 | #[route("/", method="GET", method="GET")] | ^^^^^ -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-duplicate-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index 0e16b5e2..c36b090c 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -6,8 +6,8 @@ error: The #[route(..)] macro requires at least one `method` attribute | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-missing-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index a638a96a..dda36606 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -4,8 +4,8 @@ error: Unexpected HTTP method: `UNEXPECTED` 3 | #[route("/", method="UNEXPECTED")] | ^^^^^^^^^^^^ -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-unexpected-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` From efdf3ab1c3ddc80508d020802c79a175134a30aa Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 01:32:58 +0100 Subject: [PATCH 31/99] clippy --- actix-http/src/h1/dispatcher.rs | 14 +++++++------- actix-http/src/lib.rs | 7 +------ actix-http/src/ws/mask.rs | 6 +++--- actix-router/src/resource.rs | 20 ++++++++------------ actix-web-codegen/src/route.rs | 2 +- src/middleware/compress.rs | 2 +- src/types/path.rs | 14 ++++---------- src/types/query.rs | 8 +------- 8 files changed, 26 insertions(+), 47 deletions(-) diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index aef765b8..69530ed1 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -303,9 +303,9 @@ where body: &impl MessageBody, ) -> Result { let size = body.size(); - let mut this = self.project(); + let this = self.project(); this.codec - .encode(Message::Item((message, size)), &mut this.write_buf) + .encode(Message::Item((message, size)), this.write_buf) .map_err(|err| { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -425,13 +425,13 @@ where Poll::Ready(Some(Ok(item))) => { this.codec.encode( Message::Chunk(Some(item)), - &mut this.write_buf, + this.write_buf, )?; } Poll::Ready(None) => { this.codec - .encode(Message::Chunk(None), &mut this.write_buf)?; + .encode(Message::Chunk(None), this.write_buf)?; // payload stream finished. // set state to None and handle next message this.state.set(State::None); @@ -460,13 +460,13 @@ where Poll::Ready(Some(Ok(item))) => { this.codec.encode( Message::Chunk(Some(item)), - &mut this.write_buf, + this.write_buf, )?; } Poll::Ready(None) => { this.codec - .encode(Message::Chunk(None), &mut this.write_buf)?; + .encode(Message::Chunk(None), this.write_buf)?; // payload stream finished. // set state to None and handle next message this.state.set(State::None); @@ -592,7 +592,7 @@ where let mut updated = false; let mut this = self.as_mut().project(); loop { - match this.codec.decode(&mut this.read_buf) { + match this.codec.decode(this.read_buf) { Ok(Some(msg)) => { updated = true; this.flags.insert(Flags::STARTED); diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 3ad8d095..42ce4ffe 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -103,14 +103,9 @@ type ConnectCallback = dyn Fn(&IO, &mut Extensions); /// /// # Implementation Details /// Uses Option to reduce necessary allocations when merging with request extensions. +#[derive(Default)] pub(crate) struct OnConnectData(Option); -impl Default for OnConnectData { - fn default() -> Self { - Self(None) - } -} - impl OnConnectData { /// Construct by calling the on-connect callback with the underlying transport I/O. pub(crate) fn from_io( diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 276ca4a8..11a6ddc3 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -25,8 +25,8 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { // // un aligned prefix and suffix would be mask/unmask per byte. // proper aligned middle slice goes into fast path and operates on 4-byte blocks. - let (mut prefix, words, mut suffix) = unsafe { buf.align_to_mut::() }; - apply_mask_fallback(&mut prefix, mask); + let (prefix, words, suffix) = unsafe { buf.align_to_mut::() }; + apply_mask_fallback(prefix, mask); let head = prefix.len() & 3; let mask_u32 = if head > 0 { if cfg!(target_endian = "big") { @@ -40,7 +40,7 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { for word in words.iter_mut() { *word ^= mask_u32; } - apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes()); + apply_mask_fallback(suffix, mask_u32.to_ne_bytes()); } #[cfg(test)] diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index be54336e..dcd65535 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -394,9 +394,7 @@ impl ResourceDef { pub fn set_name(&mut self, name: impl Into) { let name = name.into(); - if name.is_empty() { - panic!("resource name should not be empty"); - } + assert!(!name.is_empty(), "resource name should not be empty"); self.name = Some(name) } @@ -978,9 +976,7 @@ impl ResourceDef { let (name, pattern) = match param.find(':') { Some(idx) => { - if tail { - panic!("custom regex is not supported for tail match"); - } + assert!(!tail, "custom regex is not supported for tail match"); let (name, pattern) = param.split_at(idx); (name, &pattern[1..]) @@ -1087,12 +1083,12 @@ impl ResourceDef { 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 - ); - } + assert!( + dyn_segment_count <= MAX_DYNAMIC_SEGMENTS, + "Only {} dynamic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, + dyn_segment_count + ); // Store the pattern in capture group #1 to have context info outside it let mut re = format!("({})", re); diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 4d4af7ec..b1825200 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -365,5 +365,5 @@ pub(crate) fn with_method( fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream { let compile_err = TokenStream::from(err.to_compile_error()); item.extend(compile_err); - return item; + item } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 0e61a8e7..4854f4be 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -276,7 +276,7 @@ impl AcceptEncoding { let mut encodings = raw .replace(' ', "") .split(',') - .filter_map(|l| AcceptEncoding::new(l)) + .filter_map(AcceptEncoding::new) .collect::>(); encodings.sort(); diff --git a/src/types/path.rs b/src/types/path.rs index b58aec18..cd24deb8 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -102,7 +102,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() - .and_then(|c| c.ehandler.clone()); + .and_then(|c| c.err_handler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) @@ -158,9 +158,9 @@ where /// ); /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct PathConfig { - ehandler: Option Error + Send + Sync>>, + err_handler: Option Error + Send + Sync>>, } impl PathConfig { @@ -169,17 +169,11 @@ impl PathConfig { where F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, { - self.ehandler = Some(Arc::new(f)); + self.err_handler = Some(Arc::new(f)); self } } -impl Default for PathConfig { - fn default() -> Self { - PathConfig { ehandler: None } - } -} - #[cfg(test)] mod tests { use actix_router::ResourceDef; diff --git a/src/types/query.rs b/src/types/query.rs index eed33719..ba2034bf 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -167,7 +167,7 @@ impl FromRequest for Query { /// .app_data(query_cfg) /// .service(index); /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct QueryConfig { err_handler: Option Error + Send + Sync>>, } @@ -183,12 +183,6 @@ impl QueryConfig { } } -impl Default for QueryConfig { - fn default() -> Self { - QueryConfig { err_handler: None } - } -} - #[cfg(test)] mod tests { use actix_http::http::StatusCode; From ad22cc4e7f57ef11dcfc9706f1e1e747a3475815 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 01:59:28 +0100 Subject: [PATCH 32/99] bump msrv to 1.52.1 --- .github/workflows/ci.yml | 2 +- CHANGES.md | 1 + Cargo.toml | 2 -- README.md | 4 ++-- actix-files/CHANGES.md | 1 + actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 1 + actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 1 + actix-http/README.md | 4 ++-- actix-multipart/CHANGES.md | 1 + actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 1 + actix-test/CHANGES.md | 1 + actix-web-actors/CHANGES.md | 1 + actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 1 + actix-web-codegen/README.md | 4 ++-- actix-web-codegen/tests/trybuild.rs | 2 +- awc/README.md | 2 +- clippy.toml | 2 +- src/lib.rs | 2 +- 22 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ec034bc..e4d713b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.51.0 # MSRV + - 1.52.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index 2509197f..1654b085 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] +* Minimum supported Rust version (MSRV) is now 1.52. ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] diff --git a/Cargo.toml b/Cargo.toml index dc7e9af3..ae47398e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,6 @@ members = [ "actix-test", "actix-router", ] -# enable when MSRV is 1.51+ -# resolver = "2" [features] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] diff --git a/README.md b/README.md index 13ec3a01..00e8fa6c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9)
@@ -32,7 +32,7 @@ * SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Includes an async [HTTP client](https://docs.rs/awc/) -* Runs on stable Rust 1.51+ +* Runs on stable Rust 1.52+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6d1512c2..8e0a3eec 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.6.0-beta.7 - 2021-09-09 diff --git a/actix-files/README.md b/actix-files/README.md index 31bbd036..ed15e333 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- Minimum supported Rust version: 1.51 or later +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 69e96f98..98b197bc 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 3.0.0-beta.5 - 2021-09-09 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index f75b9c13..6bf0d710 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 775b9e6d..71aa8668 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 3.0.0-beta.10 - 2021-09-09 diff --git a/actix-http/README.md b/actix-http/README.md index b58b47f5..68a6e0a5 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -4,7 +4,7 @@ [![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.10)](https://docs.rs/actix-http/3.0.0-beta.10) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 ## Example diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index c32583f0..33da6a20 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.4.0-beta.6 - 2021-09-09 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index f3366f50..254ef877 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 00190343..c2858f2b 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.5.0-beta.2 - 2021-09-09 diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 58e05c4b..9c0a9ee8 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.1.0-beta.4 - 2021-09-09 diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 2e453063..e3693f0f 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 4.0.0-beta.7 - 2021-09-09 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index a647e4bc..2c29dedf 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum supported Rust version: 1.51 or later +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index dae9830a..4b3e04ac 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx * Improve error recovery potential when macro input is invalid. [#2410] +* Minimum supported Rust version (MSRV) is now 1.52. [#2410]: https://github.com/actix/actix-web/pull/2410 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 268e8b01..ee552cfb 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum supported Rust version: 1.51 or later. +- Minimum Supported Rust Version (MSRV): 1.52 ## Compile Testing diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 54bc1cae..edbe1a8e 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.51)] // MSRV +#[rustversion::stable(1.52)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/awc/README.md b/awc/README.md index 868bc5ca..38c967e6 100644 --- a/awc/README.md +++ b/awc/README.md @@ -12,7 +12,7 @@ - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 ## Example diff --git a/clippy.toml b/clippy.toml index 829dd1c5..cef91fde 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.51" +msrv = "1.52" diff --git a/src/lib.rs b/src/lib.rs index d008fdb7..3ad77ff5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.51+ +//! * Runs on stable Rust 1.52+ //! //! # Crate Features //! * `cookies` - cookies support (enabled by default) From 591abc37c31d969998247dda91baf4c950d0cd91 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 17:30:32 +0100 Subject: [PATCH 33/99] add test runtime macro (#2409) --- Cargo.toml | 2 +- actix-files/src/lib.rs | 2 +- actix-files/tests/encoding.rs | 2 +- actix-files/tests/guard.rs | 2 +- actix-http-test/src/lib.rs | 2 +- actix-http/src/client/pool.rs | 1 + actix-http/src/h1/encoder.rs | 1 + actix-router/src/router.rs | 3 +- actix-test/src/lib.rs | 4 +- actix-web-codegen/CHANGES.md | 2 + actix-web-codegen/Cargo.toml | 11 ++--- actix-web-codegen/src/lib.rs | 40 ++++++++++++++----- actix-web-codegen/src/route.rs | 22 +++++----- actix-web-codegen/tests/test_macro.rs | 2 +- actix-web-codegen/tests/trybuild.rs | 2 + .../tests/trybuild/test-runtime.rs | 6 +++ examples/basic.rs | 2 +- src/test.rs | 16 ++++---- tests/test-macro-import-conflict.rs | 15 +++++++ 19 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 actix-web-codegen/tests/trybuild/test-runtime.rs create mode 100644 tests/test-macro-import-conflict.rs diff --git a/Cargo.toml b/Cargo.toml index ae47398e..630ef564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" -actix-macros = "0.2.1" +actix-macros = "0.2.3" actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 1eb091aa..175c6eae 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -83,7 +83,7 @@ mod tests { use super::*; - #[actix_rt::test] + #[actix_web::test] async fn test_file_extension_to_mime() { let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index d21d4f8f..652a7c12 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -8,7 +8,7 @@ use actix_web::{ App, }; -#[actix_rt::test] +#[actix_web::test] async fn test_utf8_file_contents() { // use default ISO-8859-1 encoding let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; diff --git a/actix-files/tests/guard.rs b/actix-files/tests/guard.rs index 8b1785e7..d053f3fd 100644 --- a/actix-files/tests/guard.rs +++ b/actix-files/tests/guard.rs @@ -7,7 +7,7 @@ use actix_web::{ }; use bytes::Bytes; -#[actix_rt::test] +#[actix_web::test] async fn test_guard_filter() { let srv = test::init_service( App::new() diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index ec7b46ff..c7b083b5 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -36,7 +36,7 @@ use socket2::{Domain, Protocol, Socket, Type}; /// Ok(HttpResponse::Ok().into()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let mut srv = TestServer::start( /// || HttpService::new( diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 88188038..7c36dcff 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -186,6 +186,7 @@ where let mut conn = None; // check if there is idle connection for given key. + #[allow(clippy::must_not_suspend)] let mut map = inner.available.borrow_mut(); if let Some(conns) = map.get_mut(&key) { diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 5e1d4778..ead14206 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -20,6 +20,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; #[derive(Debug)] pub(crate) struct MessageEncoder { + #[allow(dead_code)] pub length: BodySize, pub te: TransferEncoding, _phantom: PhantomData, diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index f5deb858..fad1a440 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -6,8 +6,9 @@ use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; pub struct ResourceId(pub u16); /// Information about current resource -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct ResourceInfo { + #[allow(dead_code)] resource: ResourceId, } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index c863af44..23a7eeba 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -64,7 +64,7 @@ pub use actix_web::test::{ /// Ok(HttpResponse::Ok()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let srv = actix_test::start(|| /// App::new().service(my_handler) @@ -104,7 +104,7 @@ where /// Ok(HttpResponse::Ok()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let srv = actix_test::start_with(actix_test::config().h1(), || /// App::new().service(my_handler) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 4b3e04ac..f1f050b2 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,9 +2,11 @@ ## Unreleased - 2021-xx-xx * Improve error recovery potential when macro input is invalid. [#2410] +* Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] * Minimum supported Rust version (MSRV) is now 1.52. [#2410]: https://github.com/actix/actix-web/pull/2410 +[#2409]: https://github.com/actix/actix-web/pull/2409 ## 0.5.0-beta.4 - 2021-09-09 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index b8b346b8..afedafdf 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -2,11 +2,12 @@ name = "actix-web-codegen" version = "0.5.0-beta.4" description = "Routing and runtime macros for Actix Web" -readme = "README.md" homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -documentation = "https://docs.rs/actix-web-codegen" -authors = ["Nikolay Kim "] +repository = "https://github.com/actix/actix-web.git" +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -21,7 +22,7 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" -actix-macros = "0.2.2" +actix-macros = "0.2.3" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.9" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 2237f422..85faf6bc 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -59,6 +59,7 @@ #![recursion_limit = "512"] use proc_macro::TokenStream; +use quote::quote; mod route; @@ -157,24 +158,41 @@ method_macro! { } /// Marks async main function as the actix system entry-point. -/// -/// # Actix Web Re-export -/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. -/// + /// # Examples /// ``` -/// #[actix_web_codegen::main] +/// #[actix_web::main] /// async fn main() { /// async { println!("Hello world"); }.await /// } /// ``` #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { - use quote::quote; - let input = syn::parse_macro_input!(item as syn::ItemFn); - (quote! { - #[actix_web::rt::main(system = "::actix_web::rt::System")] - #input + let mut output: TokenStream = (quote! { + #[::actix_web::rt::main(system = "::actix_web::rt::System")] }) - .into() + .into(); + + output.extend(item); + output +} + +/// Marks async test functions to use the actix system entry-point. +/// +/// # Examples +/// ``` +/// #[actix_web::test] +/// async fn test() { +/// assert_eq!(async { "Hello world" }.await, "Hello world"); +/// } +/// ``` +#[proc_macro_attribute] +pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { + let mut output: TokenStream = (quote! { + #[::actix_web::rt::test(system = "::actix_web::rt::System")] + }) + .into(); + + output.extend(item); + output } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index b1825200..eac1948a 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -220,7 +220,7 @@ fn guess_resource_type(typ: &syn::Type) -> ResourceType { impl Route { pub fn new( args: AttributeArgs, - input: TokenStream, + ast: syn::ItemFn, method: Option, ) -> syn::Result { if args.is_empty() { @@ -234,14 +234,11 @@ impl Route { ), )); } - let ast: syn::ItemFn = syn::parse(input)?; + let name = ast.sig.ident.clone(); - // Try and pull out the doc comments so that we can reapply them to the - // generated struct. - // - // Note that multi line doc comments are converted to multiple doc - // attributes. + // Try and pull out the doc comments so that we can reapply them to the generated struct. + // Note that multi line doc comments are converted to multiple doc attributes. let doc_attributes = ast .attrs .iter() @@ -349,9 +346,16 @@ pub(crate) fn with_method( input: TokenStream, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input.clone(), method) { + + let ast = match syn::parse::(input.clone()) { + Ok(ast) => ast, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; + + match Route::new(args, ast, method) { Ok(route) => route.into_token_stream().into(), - // on parse err, make IDEs happy; see fn docs + // on macro related error, make IDEs happy; see fn docs Err(err) => input_and_compile_error(input, err), } } diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 6b08c409..769cf2bc 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -256,7 +256,7 @@ async fn test_auto_async() { assert!(response.status().is_success()); } -#[actix_rt::test] +#[actix_web::test] async fn test_wrap() { let srv = actix_test::start(|| App::new().service(get_wrap)); diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index edbe1a8e..dd70cb7c 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -13,4 +13,6 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.pass("tests/trybuild/docstring-ok.rs"); + + t.pass("tests/trybuild/test-runtime.rs"); } diff --git a/actix-web-codegen/tests/trybuild/test-runtime.rs b/actix-web-codegen/tests/trybuild/test-runtime.rs new file mode 100644 index 00000000..0b901b25 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/test-runtime.rs @@ -0,0 +1,6 @@ +#[actix_web::test] +async fn my_test() { + assert!(async { 1 }.await, 1); +} + +fn main() {} diff --git a/examples/basic.rs b/examples/basic.rs index 796f002e..d2954612 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -35,7 +35,7 @@ async fn main() -> std::io::Result<()> { ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) - .bind("127.0.0.1:8080")? + .bind(("127.0.0.1", 8080))? .workers(1) .run() .await diff --git a/src/test.rs b/src/test.rs index 99e70859..43bf612c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -52,7 +52,7 @@ pub fn default_service( /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_init_service() { /// let app = test::init_service( /// App::new() @@ -98,7 +98,7 @@ where /// ``` /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_response() { /// let app = test::init_service( /// App::new() @@ -129,7 +129,7 @@ where /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( @@ -176,7 +176,7 @@ where /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( @@ -224,7 +224,7 @@ where /// name: String, /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_post_person() { /// let app = test::init_service( /// App::new().service( @@ -296,7 +296,7 @@ where /// name: String /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_add_person() { /// let app = test::init_service( /// App::new().service( @@ -356,8 +356,8 @@ where /// } /// } /// -/// #[test] -/// fn test_index() { +/// #[actix_web::test] +/// async fn test_index() { /// let req = test::TestRequest::default().insert_header("content-type", "text/plain") /// .to_http_request(); /// diff --git a/tests/test-macro-import-conflict.rs b/tests/test-macro-import-conflict.rs new file mode 100644 index 00000000..0d23bb41 --- /dev/null +++ b/tests/test-macro-import-conflict.rs @@ -0,0 +1,15 @@ +//! Checks that test macro does not cause problems in the presence of imports named "test" that +//! could be either a module with test items or the "test with runtime" macro itself. +//! +//! Before actix/actix-net#399 was implemented, this macro was running twice. The first run output +//! `#[test]` and it got run again and since it was in scope. +//! +//! Prevented by using the fully-qualified test marker (`#[::core::prelude::v1::test]`). + +use actix_web::test; + +#[actix_web::test] +async fn test_macro_naming_conflict() { + let _req = test::TestRequest::default(); + assert_eq!(async { 1 }.await, 1); +} From 4f6f0b0137ebe6ff106a0ed7b67cb15c68b29fc8 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:00:11 +0100 Subject: [PATCH 34/99] chore: Bump rustls to 0.20.0 (#2416) Co-authored-by: Kirill Mironov --- CHANGES.md | 2 + Cargo.toml | 10 +++-- actix-http-test/Cargo.toml | 2 +- actix-http/CHANGES.md | 4 ++ actix-http/Cargo.toml | 11 ++--- actix-http/examples/ws.rs | 23 +++++++---- actix-http/src/client/connector.rs | 13 +++--- actix-http/src/h2/service.rs | 2 +- actix-http/src/service.rs | 7 ++-- actix-http/tests/test_rustls.rs | 58 +++++++++++++++++++++------ actix-test/Cargo.toml | 2 +- awc/CHANGES.md | 3 ++ awc/Cargo.toml | 10 +++-- awc/tests/test_rustls_client.rs | 64 ++++++++++++++++++++++-------- tests/test_server.rs | 24 ++++++----- 15 files changed, 162 insertions(+), 73 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1654b085..cea963ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] * Minimum supported Rust version (MSRV) is now 1.52. +* Updated rustls to v0.20. [#2414] ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] @@ -17,6 +18,7 @@ [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2414]: https://github.com/actix/actix-web/pull/2414 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/Cargo.toml b/Cargo.toml index 630ef564..e53f4411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.6", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.4" actix-http = "3.0.0-beta.10" @@ -111,11 +111,15 @@ brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" flate2 = "1.0.13" -zstd = "0.7" +futures-util = { version = "0.3.7", default-features = false, features = ["std"] } rand = "0.8" rcgen = "0.8" +rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } -tls-rustls = { package = "rustls", version = "0.19.0" } +tls-rustls = { package = "rustls", version = "0.20.0" } +webpki = "0.22" +webpki-roots = "0.22" +zstd = "0.7" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index ee4971a1..2bdd6969 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" -actix-tls = "3.0.0-beta.5" +actix-tls = "3.0.0-beta.6" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 71aa8668..3911fc00 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,8 +1,12 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. +[#2414]: https://github.com/actix/actix-web/pull/2414 + ## 3.0.0-beta.10 - 2021-09-09 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 889c9133..d0724ba5 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -46,7 +46,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.6", features = ["accept", "connect"] } ahash = "0.7" base64 = "0.13" @@ -85,17 +85,18 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.6", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" rcgen = "0.8" regex = "1.3" +rustls-pemfile = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tls-openssl = { version = "0.10", package = "openssl" } -tls-rustls = { version = "0.19", package = "rustls" } -webpki = { version = "0.21" } +tls-openssl = { package = "openssl", version = "0.10.9" } +tls-rustls = { package = "rustls", version = "0.20.0" } +webpki = { version = "0.22" } [[example]] name = "ws" diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index d3cedf87..b6be4d2f 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -85,22 +85,31 @@ impl Stream for Heartbeat { fn tls_config() -> rustls::ServerConfig { use std::io::BufReader; - use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig, - }; + use rustls::{Certificate, PrivateKey}; + use rustls_pemfile::{certs, pkcs8_private_keys}; let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + let mut config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(b"http/1.1".to_vec()); + config.alpn_protocols.push(b"h2".to_vec()); config } diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index bd46919e..bde5e485 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -313,18 +313,15 @@ where SslConnector::Rustls(tls) => { const H2: &[u8] = b"h2"; - use actix_tls::connect::ssl::rustls::{ - RustlsConnector, Session, TlsStream, - }; + use actix_tls::connect::ssl::rustls::{RustlsConnector, TlsStream}; impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; - let h2 = sock - .get_ref() - .1 - .get_alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + let h2 = + sock.get_ref().1.alpn_protocol().map_or(false, |protos| { + protos.windows(2).any(|w| w == H2) + }); if h2 { (Box::new(sock), Protocol::Http2) } else { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 09e24045..32dae8ac 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -177,7 +177,7 @@ mod rustls { > { let mut protos = vec![b"h2".to_vec()]; protos.extend_from_slice(&config.alpn_protocols); - config.set_protocols(&protos); + config.alpn_protocols = protos; Acceptor::new(config) .map_err(TlsError::Tls) diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index afe47bf2..62c96887 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -263,7 +263,7 @@ mod openssl { mod rustls { use std::io; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream}; + use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_tls::accept::TlsError; use super::*; @@ -308,14 +308,13 @@ mod rustls { > { let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; protos.extend_from_slice(&config.alpn_protocols); - config.set_protocols(&protos); + config.alpn_protocols = protos; Acceptor::new(config) .map_err(TlsError::Tls) .map_init_err(|_| panic!()) .and_then(|io: TlsStream| async { - let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() - { + let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") { Protocol::Http2 } else { diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index cb7c77ad..69c7db74 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -3,7 +3,7 @@ extern crate tls_rustls as rustls; use std::{ - convert::Infallible, + convert::{Infallible, TryFrom}, io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, @@ -20,16 +20,17 @@ use actix_http::{ }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; +use actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS; use actix_utils::future::{err, ok}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, Session, + Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore, + ServerConfig as RustlsServerConfig, ServerName, }; -use webpki::DNSNameRef; +use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(mut stream: S) -> Result where @@ -47,13 +48,24 @@ fn tls_config() -> RustlsServerConfig { let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = RustlsServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + let mut config = RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); + config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec()); config } @@ -62,19 +74,39 @@ pub fn get_negotiated_alpn_protocol( addr: SocketAddr, client_alpn_protocol: &[u8], ) -> Option> { - let mut config = rustls::ClientConfig::new(); + let mut root_certs = RootCertStore::empty(); + for cert in TLS_SERVER_ROOTS.0 { + let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( + cert.subject, + cert.spki, + cert.name_constraints, + ); + let certs = vec![cert].into_iter(); + root_certs.add_server_trust_anchors(certs); + } + + let mut config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_certs) + .with_no_client_auth(); + config.alpn_protocols.push(client_alpn_protocol.to_vec()); - let mut sess = rustls::ClientSession::new( - &Arc::new(config), - DNSNameRef::try_from_ascii_str("localhost").unwrap(), - ); + + let mut sess = rustls::ClientConnection::new( + Arc::new(config), + ServerName::try_from("localhost").unwrap(), + ) + .unwrap(); + let mut sock = StdTcpStream::connect(addr).unwrap(); let mut stream = rustls::Stream::new(&mut sess, &mut sock); + // The handshake will fails because the client will not be able to verify the server // certificate, but it doesn't matter here as we are just interested in the negotiated ALPN // protocol let _ = stream.flush(); - sess.get_alpn_protocol().map(|proto| proto.to_vec()) + + sess.alpn_protocol().map(|proto| proto.to_vec()) } #[actix_rt::test] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 41d32257..62a27e5a 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -35,4 +35,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } -tls-rustls = { package = "rustls", version = "0.19.0", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 252b62ef..49d88e5e 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Updated rustls to v0.20. [#2414] + +[#2414]: https://github.com/actix/actix-web/pull/2414 ## 3.0.0-beta.8 - 2021-09-09 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 262c3dce..967d4960 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -73,8 +73,8 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } +tls-openssl = { package = "openssl", version = "0.10.9", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } @@ -82,7 +82,7 @@ actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" -actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.6", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } brotli2 = "0.3.2" @@ -90,7 +90,9 @@ env_logger = "0.8" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" -webpki = "0.21" +rustls-pemfile = "0.2" +webpki = "0.22" +webpki-roots = "0.22" [[example]] name = "client" diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index bc811c04..95f2d061 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -8,6 +8,7 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, + time::SystemTime, }; use actix_http::HttpService; @@ -15,37 +16,52 @@ use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; -use rustls::internal::pemfile::{certs, pkcs8_private_keys}; -use rustls::{ClientConfig, NoClientAuth, ServerConfig}; +use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerConfig, + ServerName, +}; +use rustls_pemfile::{certs, pkcs8_private_keys}; +use webpki_roots::TLS_SERVER_ROOTS; fn tls_config() -> ServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - config + ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() } mod danger { + use super::*; + pub struct NoCertificateVerification; - impl rustls::ServerCertVerifier for NoCertificateVerification { + impl ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: webpki::DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(rustls::ServerCertVerified::assertion()) + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) } } } @@ -73,10 +89,26 @@ async fn test_connection_reuse_h2() { }) .await; - // disable TLS verification - let mut config = ClientConfig::new(); + let mut root_certs = RootCertStore::empty(); + for cert in TLS_SERVER_ROOTS.0 { + let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( + cert.subject, + cert.spki, + cert.name_constraints, + ); + let certs = vec![cert].into_iter(); + root_certs.add_server_trust_anchors(certs); + } + + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_certs) + .with_no_client_auth(); + let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - config.set_protocols(&protos); + config.alpn_protocols = protos; + + // disable TLS verification config .dangerous() .set_certificate_verifier(Arc::new(danger::NoCertificateVerification)); diff --git a/tests/test_server.rs b/tests/test_server.rs index beb8ff0f..ff6f5ae5 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -883,27 +883,31 @@ async fn test_brotli_encoding_large_openssl() { mod plus_rustls { use std::io::BufReader; - use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, - }; + use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig}; + use rustls_pemfile::{certs, pkcs8_private_keys}; use super::*; - fn rustls_config() -> RustlsServerConfig { + fn tls_config() -> RustlsServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = RustlsServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - config + RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() } #[actix_rt::test] @@ -914,7 +918,7 @@ mod plus_rustls { .map(char::from) .collect::(); - let srv = actix_test::start_with(actix_test::config().rustls(rustls_config()), || { + let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { HttpResponse::Ok() .encoding(actix_web::http::ContentEncoding::Identity) From 37f2bf562559a314b9d2f6846cc359d5ea7cf4dc Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:06:51 +0100 Subject: [PATCH 35/99] clippy --- actix-http/src/client/pool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 7c36dcff..88188038 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -186,7 +186,6 @@ where let mut conn = None; // check if there is idle connection for given key. - #[allow(clippy::must_not_suspend)] let mut map = inner.available.borrow_mut(); if let Some(conns) = map.get_mut(&key) { From c09ec6af4cb74366108d359357196b0e23b2f94d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:27:30 +0100 Subject: [PATCH 36/99] split off coverage ci job --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d713b4..b5e92bb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,7 @@ jobs: - name: Generate Cargo.lock uses: actions-rs/cargo@v1 - with: - command: generate-lockfile + with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 @@ -82,32 +81,44 @@ jobs: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: Generate coverage file - if: > - matrix.target.os == 'ubuntu-latest' - && matrix.version == 'stable' - && github.ref == 'refs/heads/master' - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --out Xml --verbose - - name: Upload to Codecov - if: > - matrix.target.os == 'ubuntu-latest' - && matrix.version == 'stable' - && github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: - file: cobertura.xml - - name: Clear the cargo caches run: | cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + if: github.ref == 'refs/heads/master' + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --out Xml --verbose + - name: Upload to Codecov + if: github.ref == 'refs/heads/master' + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } + + rustdoc: name: rustdoc runs-on: ubuntu-latest - steps: - uses: actions/checkout@v2 From 9abe166d52c8cfae53627a9a683291c8569aaa69 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 22:32:05 +0100 Subject: [PATCH 37/99] actix-web beta 10 releases (#2417) --- .cargo/config.toml | 5 ++ .github/workflows/ci.yml | 35 +++++++++++++- CHANGES.md | 7 ++- Cargo.toml | 18 ++++---- README.md | 4 +- actix-files/Cargo.toml | 8 ++-- actix-http-test/Cargo.toml | 8 ++-- actix-http/CHANGES.md | 3 ++ actix-http/Cargo.toml | 18 ++++---- actix-http/README.md | 4 +- actix-http/src/client/connector.rs | 74 +++++++++++++++++------------- actix-http/tests/test_rustls.rs | 15 +----- actix-multipart/Cargo.toml | 4 +- actix-test/CHANGES.md | 6 +++ actix-test/Cargo.toml | 19 ++++++-- actix-web-actors/Cargo.toml | 8 ++-- actix-web-codegen/CHANGES.md | 3 ++ actix-web-codegen/Cargo.toml | 6 +-- actix-web-codegen/README.md | 4 +- awc/CHANGES.md | 3 ++ awc/Cargo.toml | 16 +++---- awc/README.md | 4 +- awc/tests/test_rustls_client.rs | 15 +----- 23 files changed, 169 insertions(+), 118 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index f417a705..40a513ef 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,3 +7,8 @@ ci-default = "check --workspace --bins --tests --examples" ci-full = "check --workspace --all-features --bins --tests --examples" ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" + +ci-feature-powerset-check-no-tls="hack --workspace --feature-powerset --skip=__compress,rustls,openssl check" +ci-feature-powerset-check-rustls="hack --workspace --feature-powerset --features=rustls --skip=__compress,openssl check" +ci-feature-powerset-check-openssl="hack --workspace --feature-powerset --features=openssl --skip=__compress,rustls check" +ci-feature-powerset-check-all="hack --workspace --feature-powerset --skip=__compress check" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5e92bb7..aff0b934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: target: - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } + - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } version: - 1.52.0 # MSRV - stable @@ -32,6 +32,8 @@ jobs: - uses: actions/checkout@v2 # install OpenSSL on Windows + # TODO: GitHub actions docs state that OpenSSL is + # already installed on these Windows machines somewhere - name: Set vcpkg root if: matrix.target.triple == 'x86_64-pc-windows-msvc' run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append @@ -86,6 +88,36 @@ jobs: cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + # if: github.ref == 'refs/heads/master' + uses: actions-rs/cargo@v1 + with: { command: ci-feature-powerset-check-all } + coverage: name: coverage runs-on: ubuntu-latest @@ -115,7 +147,6 @@ jobs: uses: codecov/codecov-action@v1 with: { file: cobertura.xml } - rustdoc: name: rustdoc runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index cea963ca..a2b6b14b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,19 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.10 - 2021-10-20 ### Added * Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] +* `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] -* Minimum supported Rust version (MSRV) is now 1.52. * Updated rustls to v0.20. [#2414] +* Minimum supported Rust version (MSRV) is now 1.52. ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] @@ -18,6 +22,7 @@ [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/Cargo.toml b/Cargo.toml index e53f4411..15228220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.9" +version = "4.0.0-beta.10" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -11,7 +11,7 @@ categories = [ "web-programming::websocket" ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" @@ -61,22 +61,22 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.3" -actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.6", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } -actix-web-codegen = "0.5.0-beta.4" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" +actix-router = "0.5.0-beta.2" +actix-web-codegen = "0.5.0-beta.5" ahash = "0.7" bytes = "1" @@ -105,7 +105,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.8", features = ["openssl"] } +awc = { version = "3.0.0-beta.9", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } @@ -117,8 +117,6 @@ rcgen = "0.8" rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -webpki = "0.22" -webpki-roots = "0.22" zstd = "0.7" [profile.dev] diff --git a/README.md b/README.md index 00e8fa6c..25b59536 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web/4.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.10)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index eccf49a7..66497877 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false } -actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-http = "3.0.0-beta.11" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.9" -actix-test = "0.1.0-beta.3" +actix-web = "4.0.0-beta.10" +actix-test = "0.1.0-beta.5" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 2bdd6969..d3fc8a47 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" -actix-tls = "3.0.0-beta.6" +actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.8", default-features = false } +awc = { version = "3.0.0-beta.9", default-features = false } base64 = "0.13" bytes = "1" @@ -50,5 +50,5 @@ serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.11" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 3911fc00..3273847c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.11 - 2021-10-20 ### Changed * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index d0724ba5..3abf537f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,14 +1,17 @@ [package] name = "actix-http" -version = "3.0.0-beta.10" +version = "3.0.0-beta.11" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +repository = "https://github.com/actix/actix-web.git" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -46,7 +49,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.6", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.7", features = ["accept", "connect"] } ahash = "0.7" base64 = "0.13" @@ -85,7 +88,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.6", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" @@ -96,7 +99,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -webpki = { version = "0.22" } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 68a6e0a5..5b1e552f 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.10)](https://docs.rs/actix-http/3.0.0-beta.10) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http/3.0.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.11) [![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/client/connector.rs b/actix-http/src/client/connector.rs index bde5e485..4314511a 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -28,18 +28,13 @@ use super::pool::ConnectionPool; use super::Connect; use super::Protocol; -#[cfg(feature = "openssl")] -use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; -#[cfg(feature = "rustls")] -use actix_tls::connect::ssl::rustls::ClientConfig; - enum SslConnector { #[allow(dead_code)] None, #[cfg(feature = "openssl")] - Openssl(OpensslConnector), + Openssl(actix_tls::connect::ssl::openssl::SslConnector), #[cfg(feature = "rustls")] - Rustls(std::sync::Arc), + Rustls(std::sync::Arc), } /// Manages HTTP client network connectivity. @@ -78,10 +73,35 @@ impl Connector<()> { } } - // Build Ssl connector with openssl, based on supplied alpn protocols - #[cfg(feature = "openssl")] + /// Provides an empty TLS connector when no TLS feature is enabled. + #[cfg(not(any(feature = "openssl", feature = "rustls")))] + fn build_ssl(_: Vec>) -> SslConnector { + SslConnector::None + } + + /// Build TLS connector with rustls, based on supplied ALPN protocols + /// + /// Note that if both `openssl` and `rustls` features are enabled, rustls will be used. + #[cfg(feature = "rustls")] fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::ssl::openssl::SslMethod; + use actix_tls::connect::tls::rustls::{webpki_roots_cert_store, ClientConfig}; + + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + + config.alpn_protocols = protocols; + + SslConnector::Rustls(std::sync::Arc::new(config)) + } + + /// Build TLS connector with openssl, based on supplied ALPN protocols + #[cfg(all(feature = "openssl", not(feature = "rustls")))] + fn build_ssl(protocols: Vec>) -> SslConnector { + use actix_tls::connect::tls::openssl::{ + SslConnector as OpensslConnector, SslMethod, + }; use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); @@ -91,28 +111,12 @@ impl Connector<()> { } let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); - let _ = ssl - .set_alpn_protos(&alpn) - .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); + if let Err(err) = ssl.set_alpn_protos(&alpn) { + error!("Can not set ALPN protocol: {:?}", err); + } + SslConnector::Openssl(ssl.build()) } - - // Build Ssl connector with rustls, based on supplied alpn protocols - #[cfg(all(not(feature = "openssl"), feature = "rustls"))] - fn build_ssl(protocols: Vec>) -> SslConnector { - let mut config = ClientConfig::new(); - config.set_protocols(&protocols); - config.root_store.add_server_trust_anchors( - &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS, - ); - SslConnector::Rustls(std::sync::Arc::new(config)) - } - - // ssl turned off, provides empty ssl connector - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - fn build_ssl(_: Vec>) -> SslConnector { - SslConnector::None - } } impl Connector { @@ -167,14 +171,20 @@ where #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. - pub fn ssl(mut self, connector: OpensslConnector) -> Self { + pub fn ssl( + mut self, + connector: actix_tls::connect::ssl::openssl::SslConnector, + ) -> Self { self.ssl = SslConnector::Openssl(connector); self } #[cfg(feature = "rustls")] /// Use custom `SslConnector` instance. - pub fn rustls(mut self, connector: std::sync::Arc) -> Self { + pub fn rustls( + mut self, + connector: std::sync::Arc, + ) -> Self { self.ssl = SslConnector::Rustls(connector); self } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 69c7db74..924ef49a 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -20,7 +20,7 @@ use actix_http::{ }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; -use actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS; +use actix_tls::connect::tls::rustls::webpki_roots_cert_store; use actix_utils::future::{err, ok}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; @@ -74,20 +74,9 @@ pub fn get_negotiated_alpn_protocol( addr: SocketAddr, client_alpn_protocol: &[u8], ) -> Option> { - let mut root_certs = RootCertStore::empty(); - for cert in TLS_SERVER_ROOTS.0 { - let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( - cert.subject, - cert.spki, - cert.name_constraints, - ); - let certs = vec![cert].into_iter(); - root_certs.add_server_trust_anchors(certs); - } - let mut config = rustls::ClientConfig::builder() .with_safe_defaults() - .with_root_certificates(root_certs) + .with_root_certificates(webpki_roots_cert_store()) .with_no_client_auth(); config.alpn_protocols.push(client_alpn_protocol.to_vec()); diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 6db81cca..f2b1d08a 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -14,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-web = { version = "4.0.0-beta.10", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -29,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 9c0a9ee8..07089258 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,8 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.5 - 2021-10-20 +* Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. +[#2414]: https://github.com/actix/actix-web/pull/2414 + ## 0.1.0-beta.4 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 62a27e5a..ede72f21 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,13 +1,22 @@ [package] name = "actix-test" -version = "0.1.0-beta.4" +version = "0.1.0-beta.5" authors = [ "Nikolay Kim ", "Rob Ede ", ] -edition = "2018" description = "Integration testing tools for Actix Web applications" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" +edition = "2018" [features] default = [] @@ -20,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" actix-http-test = "3.0.0-beta.5" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.8", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index ef6bd919..2d987a13 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.10" -actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.10", default-features = false } bytes = "1" bytestring = "1" @@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.3" +actix-test = "0.1.0-beta.5" -awc = { version = "3.0.0-beta.8", default-features = false } +awc = { version = "3.0.0-beta.9", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index f1f050b2..3811ef03 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.5 - 2021-10-20 * Improve error recovery potential when macro input is invalid. [#2410] * Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index afedafdf..c04ca435 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.4" +version = "0.5.0-beta.5" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.3" +actix-test = "0.1.0-beta.5" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.9" +actix-web = "4.0.0-beta.10" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index ee552cfb..2ffd5b31 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.5)](https://docs.rs/actix-web-codegen/0.5.0-beta.5) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 49d88e5e..5682a237 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.9 - 2021-10-20 * Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 967d4960..5a823533 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.8" +version = "3.0.0-beta.9" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -14,7 +14,7 @@ categories = [ "web-programming::websocket", ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" @@ -77,13 +77,13 @@ tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" -actix-tls = { version = "3.0.0-beta.6", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" @@ -91,8 +91,6 @@ flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" rustls-pemfile = "0.2" -webpki = "0.22" -webpki-roots = "0.22" [[example]] name = "client" diff --git a/awc/README.md b/awc/README.md index 38c967e6..67bcb965 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.8)](https://docs.rs/awc/3.0.0-beta.8) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.9)](https://docs.rs/awc/3.0.0-beta.9) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.8/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.8) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.9/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.9) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 95f2d061..c075a609 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -14,6 +14,7 @@ use std::{ use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; +use actix_tls::connect::tls::rustls::webpki_roots_cert_store; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ @@ -22,7 +23,6 @@ use rustls::{ ServerName, }; use rustls_pemfile::{certs, pkcs8_private_keys}; -use webpki_roots::TLS_SERVER_ROOTS; fn tls_config() -> ServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); @@ -89,20 +89,9 @@ async fn test_connection_reuse_h2() { }) .await; - let mut root_certs = RootCertStore::empty(); - for cert in TLS_SERVER_ROOTS.0 { - let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( - cert.subject, - cert.spki, - cert.name_constraints, - ); - let certs = vec![cert].into_iter(); - root_certs.add_server_trust_anchors(certs); - } - let mut config = ClientConfig::builder() .with_safe_defaults() - .with_root_certificates(root_certs) + .with_root_certificates(webpki_roots_cert_store()) .with_no_client_auth(); let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; From 4af414064baa2debc963ee00a01e2019a859cf3a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 23:31:46 +0100 Subject: [PATCH 38/99] prepare actix-multipart release 0.4.0-beta.7 --- CHANGES.md | 3 ++- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 2 +- actix-multipart/README.md | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a2b6b14b..9d7b3180 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,12 +16,13 @@ * Minimum supported Rust version (MSRV) is now 1.52. ### Removed -* `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] +* Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2403]: https://github.com/actix/actix-web/pull/2403 [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 33da6a20..09cc707b 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.7 - 2021-10-20 * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index f2b1d08a..92637cef 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.6" +version = "0.4.0-beta.7" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 254ef877..67481429 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.7)](https://docs.rs/actix-multipart/0.4.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.7/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From c79b9a0df3ba7690271f0efd5f5f35a97813480b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 23:32:46 +0100 Subject: [PATCH 39/99] prepare actix-files release 0.6.0-beta.8 --- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 2 +- actix-files/README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 8e0a3eec..e1a2c90c 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.8 - 2021-10-20 * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 66497877..3d734060 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.7" +version = "0.6.0-beta.8" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] diff --git a/actix-files/README.md b/actix-files/README.md index ed15e333..eac7339a 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.8)](https://docs.rs/actix-files/0.6.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.8/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.8) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From d40b6748bc84a98483657a9422493bc16db760aa Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Fri, 22 Oct 2021 07:22:58 +0800 Subject: [PATCH 40/99] remove dead dep (#2420) --- actix-http/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3abf537f..a8fc4255 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -67,7 +67,6 @@ httpdate = "1.0.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" -once_cell = "1.5" log = "0.4" mime = "0.3" percent-encoding = "2.1" From d13854505feae2fc1c2435563e209b991bdb4399 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 26 Oct 2021 07:37:40 +0800 Subject: [PATCH 41/99] move actix_http::client module to awc (#2425) --- actix-http/CHANGES.md | 5 + actix-http/Cargo.toml | 7 +- actix-http/src/lib.rs | 1 - actix-test/Cargo.toml | 4 +- awc/Cargo.toml | 19 +- awc/src/builder.rs | 6 +- {actix-http => awc}/src/client/config.rs | 3 +- {actix-http => awc}/src/client/connection.rs | 50 ++--- {actix-http => awc}/src/client/connector.rs | 181 +++++++++---------- {actix-http => awc}/src/client/error.rs | 7 +- {actix-http => awc}/src/client/h1proto.rs | 35 ++-- {actix-http => awc}/src/client/h2proto.rs | 15 +- {actix-http => awc}/src/client/mod.rs | 1 - {actix-http => awc}/src/client/pool.rs | 46 ++--- awc/src/connect.rs | 10 +- awc/src/error.rs | 14 +- awc/src/lib.rs | 32 ++-- awc/src/middleware/redirect.rs | 2 +- 18 files changed, 197 insertions(+), 241 deletions(-) rename {actix-http => awc}/src/client/config.rs (96%) rename {actix-http => awc}/src/client/connection.rs (89%) rename {actix-http => awc}/src/client/connector.rs (88%) rename {actix-http => awc}/src/client/error.rs (98%) rename {actix-http => awc}/src/client/h1proto.rs (92%) rename {actix-http => awc}/src/client/h2proto.rs (96%) rename {actix-http => awc}/src/client/mod.rs (95%) rename {actix-http => awc}/src/client/pool.rs (95%) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 3273847c..81595c92 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +### Removed +* `client` module. [#2425] +* `trust-dns` feature. [#2425] + +[#2425]: https://github.com/actix/actix-web/pull/2425 ## 3.0.0-beta.11 - 2021-10-20 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a8fc4255..3d45cc8c 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -37,9 +37,6 @@ compress-brotli = ["brotli2", "__compress"] compress-gzip = ["flate2", "__compress"] compress-zstd = ["zstd", "__compress"] -# trust-dns as client dns resolver -trust-dns = ["trust-dns-resolver"] - # Internal (PRIVATE!) features used to aid testing and cheking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] @@ -49,7 +46,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.7", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.7", features = ["accept"] } ahash = "0.7" base64 = "0.13" @@ -82,8 +79,6 @@ brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.7", optional = true } -trust-dns-resolver = { version = "0.20.0", optional = true } - [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 42ce4ffe..bfb6b8c5 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -29,7 +29,6 @@ extern crate log; pub mod body; mod builder; -pub mod client; mod config; #[cfg(feature = "__compress")] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index ede72f21..002e7662 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -22,10 +22,10 @@ edition = "2018" default = [] # rustls -rustls = ["tls-rustls", "actix-http/rustls"] +rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] # openssl -openssl = ["tls-openssl", "actix-http/openssl"] +openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.0" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 5a823533..6eeb9ce5 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -30,10 +30,10 @@ features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-z default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] # openssl -openssl = ["tls-openssl", "actix-http/openssl"] +openssl = ["tls-openssl", "actix-tls/openssl"] # rustls -rustls = ["tls-rustls", "actix-http/rustls"] +rustls = ["tls-rustls", "actix-tls/rustls"] # Brotli algorithm content-encoding support compress-brotli = ["actix-http/compress-brotli", "__compress"] @@ -46,7 +46,7 @@ compress-zstd = ["actix-http/compress-zstd", "__compress"] cookies = ["cookie"] # trust-dns as dns resolver -trust-dns = ["actix-http/trust-dns"] +trust-dns = ["trust-dns-resolver"] # Internal (PRIVATE!) features used to aid testing and cheking feature status. # Don't rely on these whatsoever. They may disappear at anytime. @@ -57,13 +57,18 @@ actix-codec = "0.4.0" actix-service = "2.0.0" actix-http = "3.0.0-beta.11" actix-rt = { version = "2.1", default-features = false } +actix-tls = { version = "3.0.0-beta.7", features = ["connect"] } +actix-utils = "3.0.0" +ahash = "0.7" base64 = "0.13" bytes = "1" cfg-if = "1" -cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } +futures-util = { version = "0.3.7", default-features = false } +h2 = "0.3" +http = "0.2" itoa = "0.4" log =" 0.4" mime = "0.3" @@ -73,9 +78,15 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" +tokio = { version = "1", features = ["sync"] } + +cookie = { version = "0.15", features = ["percent-encode"], optional = true } + tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } +trust-dns-resolver = { version = "0.20.0", optional = true } + [dev-dependencies] actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } diff --git a/awc/src/builder.rs b/awc/src/builder.rs index c594b483..11ececa7 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -4,13 +4,11 @@ use std::net::IpAddr; use std::rc::Rc; use std::time::Duration; -use actix_http::{ - client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}, - http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}, -}; +use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}; use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; +use crate::client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}; use crate::connect::DefaultConnector; use crate::error::SendRequestError; use crate::middleware::{NestTransform, Redirect, Transform}; diff --git a/actix-http/src/client/config.rs b/awc/src/client/config.rs similarity index 96% rename from actix-http/src/client/config.rs rename to awc/src/client/config.rs index 1c0405cb..530c1e03 100644 --- a/actix-http/src/client/config.rs +++ b/awc/src/client/config.rs @@ -1,5 +1,4 @@ -use std::net::IpAddr; -use std::time::Duration; +use std::{net::IpAddr, time::Duration}; const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB diff --git a/actix-http/src/client/connection.rs b/awc/src/client/connection.rs similarity index 89% rename from actix-http/src/client/connection.rs rename to awc/src/client/connection.rs index a30f651c..97b96fc0 100644 --- a/actix-http/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -12,10 +12,9 @@ use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use crate::h1::ClientCodec; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; -use crate::{body::MessageBody, Error}; +use actix_http::{ + body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead, +}; use super::error::SendRequestError; use super::pool::Acquired; @@ -219,11 +218,7 @@ impl ConnectionType { } } - pub(super) fn from_h1( - io: Io, - created: time::Instant, - acquired: Acquired, - ) -> Self { + pub(super) fn from_h1(io: Io, created: time::Instant, acquired: Acquired) -> Self { Self::H1(H1Connection { io: Some(io), created, @@ -271,9 +266,7 @@ where Connection::Tls(ConnectionType::H2(conn)) => { h2proto::send_request(conn, head.into(), body).await } - _ => unreachable!( - "Plain Tcp connection can be used only in Http1 protocol" - ), + _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), } }) } @@ -301,9 +294,7 @@ where Err(SendRequestError::TunnelNotSupported) } Connection::Tcp(ConnectionType::H2(_)) => { - unreachable!( - "Plain Tcp connection can be used only in Http1 protocol" - ) + unreachable!("Plain Tcp connection can be used only in Http1 protocol") } } }) @@ -321,12 +312,8 @@ where buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_read(cx, buf) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_read(cx, buf) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), _ => unreachable!("H2Connection can not impl AsyncRead trait"), } } @@ -345,12 +332,8 @@ where buf: &[u8], ) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_write(cx, buf) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_write(cx, buf) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), _ => unreachable!(H2_UNREACHABLE_WRITE), } } @@ -363,17 +346,10 @@ where } } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_shutdown(cx) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_shutdown(cx) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), _ => unreachable!(H2_UNREACHABLE_WRITE), } } diff --git a/actix-http/src/client/connector.rs b/awc/src/client/connector.rs similarity index 88% rename from actix-http/src/client/connector.rs rename to awc/src/client/connector.rs index 4314511a..8a162c4f 100644 --- a/actix-http/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -8,6 +8,7 @@ use std::{ time::Duration, }; +use actix_http::Protocol; use actix_rt::{ net::{ActixStream, TcpStream}, time::{sleep, Sleep}, @@ -19,14 +20,13 @@ use actix_tls::connect::{ }; use futures_core::{future::LocalBoxFuture, ready}; use http::Uri; -use pin_project::pin_project; +use pin_project_lite::pin_project; use super::config::ConnectorConfig; use super::connection::{Connection, ConnectionIo}; use super::error::ConnectError; use super::pool::ConnectionPool; use super::Connect; -use super::Protocol; enum SslConnector { #[allow(dead_code)] @@ -99,9 +99,7 @@ impl Connector<()> { /// Build TLS connector with openssl, based on supplied ALPN protocols #[cfg(all(feature = "openssl", not(feature = "rustls")))] fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::tls::openssl::{ - SslConnector as OpensslConnector, SslMethod, - }; + use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod}; use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); @@ -112,7 +110,7 @@ impl Connector<()> { let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); if let Err(err) = ssl.set_alpn_protos(&alpn) { - error!("Can not set ALPN protocol: {:?}", err); + log::error!("Can not set ALPN protocol: {:?}", err); } SslConnector::Openssl(ssl.build()) @@ -148,11 +146,8 @@ where // This remap is to hide ActixStream's trait methods. They are not meant to be called // from user code. Io: ActixStream + fmt::Debug + 'static, - S: Service< - TcpConnect, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + 'static, { /// Tcp connection timeout, i.e. max time to connect to remote host including dns name @@ -171,10 +166,7 @@ where #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. - pub fn ssl( - mut self, - connector: actix_tls::connect::ssl::openssl::SslConnector, - ) -> Self { + pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self { self.ssl = SslConnector::Openssl(connector); self } @@ -328,10 +320,11 @@ where impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; - let h2 = - sock.get_ref().1.alpn_protocol().map_or(false, |protos| { - protos.windows(2).any(|w| w == H2) - }); + let h2 = sock + .get_ref() + .1 + .alpn_protocol() + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -357,8 +350,8 @@ where let tcp_pool = ConnectionPool::new(tcp_service, tcp_config); let tls_config = self.config; - let tls_pool = tls_service - .map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); + let tls_pool = + tls_service.map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); ConnectorServicePriv { tcp_pool, tls_pool } } @@ -389,10 +382,12 @@ where } } -#[pin_project] -pub struct TcpConnectorFuture { - #[pin] - fut: Fut, +pin_project! { + #[project = TcpConnectorFutureProj] + pub struct TcpConnectorFuture { + #[pin] + fut: Fut, + } } impl Future for TcpConnectorFuture @@ -451,23 +446,25 @@ where } } -#[pin_project(project = TlsConnectorProj)] -#[allow(clippy::large_enum_variant)] -enum TlsConnectorFuture { - TcpConnect { - #[pin] - fut: Fut1, - tls_service: Option, - timeout: Duration, - }, - TlsConnect { - #[pin] - fut: Fut2, - #[pin] - timeout: Sleep, - }, -} +pin_project! { + #[project = TlsConnectorProj] + #[allow(clippy::large_enum_variant)] + enum TlsConnectorFuture { + TcpConnect { + #[pin] + fut: Fut1, + tls_service: Option, + timeout: Duration, + }, + TlsConnect { + #[pin] + fut: Fut2, + #[pin] + timeout: Sleep, + }, + } +} /// helper trait for generic over different TlsStream types between tls crates. trait IntoConnectionIo { fn into_connection_io(self) -> (Box, Protocol); @@ -475,12 +472,7 @@ trait IntoConnectionIo { impl Future for TlsConnectorFuture where - S: Service< - TcpConnection, - Response = Res, - Error = std::io::Error, - Future = Fut2, - >, + S: Service, Response = Res, Error = std::io::Error, Future = Fut2>, S::Response: IntoConnectionIo, Fut1: Future, ConnectError>>, Fut2: Future>, @@ -522,11 +514,7 @@ pub struct TcpConnectorInnerService { } impl TcpConnectorInnerService { - fn new( - service: S, - timeout: Duration, - local_address: Option, - ) -> Self { + fn new(service: S, timeout: Duration, local_address: Option) -> Self { Self { service, timeout, @@ -537,11 +525,8 @@ impl TcpConnectorInnerService { impl Service for TcpConnectorInnerService where - S: Service< - TcpConnect, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + 'static, { type Response = S::Response; @@ -564,12 +549,14 @@ where } } -#[pin_project] -pub struct TcpConnectorInnerFuture { - #[pin] - fut: Fut, - #[pin] - timeout: Sleep, +pin_project! { + #[project = TcpConnectorInnerFutureProj] + pub struct TcpConnectorInnerFuture { + #[pin] + fut: Fut, + #[pin] + timeout: Sleep, + } } impl Future for TcpConnectorInnerFuture @@ -618,12 +605,8 @@ where impl Service for ConnectorServicePriv where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, Io1: ConnectionIo, Io2: ConnectionIo, { @@ -643,38 +626,46 @@ where match req.uri.scheme_str() { Some("https") | Some("wss") => match self.tls_pool { None => ConnectorServiceFuture::SslIsNotSupported, - Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)), + Some(ref pool) => ConnectorServiceFuture::Tls { + fut: pool.call(req), + }, + }, + _ => ConnectorServiceFuture::Tcp { + fut: self.tcp_pool.call(req), }, - _ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)), } } } -#[pin_project(project = ConnectorServiceProj)] -pub enum ConnectorServiceFuture -where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, - Io1: ConnectionIo, - Io2: ConnectionIo, -{ - Tcp(#[pin] as Service>::Future), - Tls(#[pin] as Service>::Future), - SslIsNotSupported, +pin_project! { + #[project = ConnectorServiceFutureProj] + pub enum ConnectorServiceFuture + where + S1: Service, + S1: Clone, + S1: 'static, + S2: Service, + S2: Clone, + S2: 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, + { + Tcp { + #[pin] + fut: as Service>::Future + }, + Tls { + #[pin] + fut: as Service>::Future + }, + SslIsNotSupported + } } impl Future for ConnectorServiceFuture where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, Io1: ConnectionIo, Io2: ConnectionIo, { @@ -682,9 +673,9 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project() { - ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp), - ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls), - ConnectorServiceProj::SslIsNotSupported => { + ConnectorServiceFutureProj::Tcp { fut } => fut.poll(cx).map_ok(Connection::Tcp), + ConnectorServiceFutureProj::Tls { fut } => fut.poll(cx).map_ok(Connection::Tls), + ConnectorServiceFutureProj::SslIsNotSupported => { Poll::Ready(Err(ConnectError::SslIsNotSupported)) } } diff --git a/actix-http/src/client/error.rs b/awc/src/client/error.rs similarity index 98% rename from actix-http/src/client/error.rs rename to awc/src/client/error.rs index 34833503..0f3b1fde 100644 --- a/actix-http/src/client/error.rs +++ b/awc/src/client/error.rs @@ -2,12 +2,13 @@ use std::{error::Error as StdError, fmt, io}; use derive_more::{Display, From}; +use actix_http::{ + error::{Error, ParseError}, + http::Error as HttpError, +}; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::SslError; -use crate::error::{Error, ParseError}; -use crate::http::Error as HttpError; - /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] #[non_exhaustive] diff --git a/actix-http/src/client/h1proto.rs b/awc/src/client/h1proto.rs similarity index 92% rename from actix-http/src/client/h1proto.rs rename to awc/src/client/h1proto.rs index 65a30748..3c2bb7cc 100644 --- a/actix-http/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -5,24 +5,25 @@ use std::{ }; use actix_codec::Framed; +use actix_http::{ + body::{BodySize, MessageBody}, + error::PayloadError, + h1, + http::{ + header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, + StatusCode, + }, + Error, Payload, RequestHeadType, ResponseHead, +}; use actix_utils::future::poll_fn; use bytes::buf::BufMut; use bytes::{Bytes, BytesMut}; use futures_core::{ready, Stream}; use futures_util::SinkExt as _; - -use crate::h1; -use crate::http::{ - header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, - StatusCode, -}; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; -use crate::{error::PayloadError, Error}; +use pin_project_lite::pin_project; use super::connection::{ConnectionIo, H1Connection}; use super::error::{ConnectError, SendRequestError}; -use crate::body::{BodySize, MessageBody}; pub(crate) async fn send_request( io: H1Connection, @@ -194,10 +195,11 @@ where Ok(()) } -#[pin_project::pin_project] -pub(crate) struct PlStream { - #[pin] - framed: Framed, h1::ClientPayloadCodec>, +pin_project! { + pub(crate) struct PlStream { + #[pin] + framed: Framed, h1::ClientPayloadCodec>, + } } impl PlStream { @@ -211,10 +213,7 @@ impl PlStream { impl Stream for PlStream { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.project(); match ready!(this.framed.as_mut().next_item(cx)?) { diff --git a/actix-http/src/client/h2proto.rs b/awc/src/client/h2proto.rs similarity index 96% rename from actix-http/src/client/h2proto.rs rename to awc/src/client/h2proto.rs index b9d5f96b..feb2dbd0 100644 --- a/actix-http/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -8,13 +8,12 @@ use h2::{ }; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, Method, Version}; +use log::trace; -use crate::{ +use actix_http::{ body::{BodySize, MessageBody}, header::HeaderMap, - message::{RequestHeadType, ResponseHead}, - payload::Payload, - Error, + Error, Payload, RequestHeadType, ResponseHead, }; use super::{ @@ -131,10 +130,7 @@ where Ok((head, payload)) } -async fn send_body( - body: B, - mut send: SendStream, -) -> Result<(), SendRequestError> +async fn send_body(body: B, mut send: SendStream) -> Result<(), SendRequestError> where B: MessageBody, B::Error: Into, @@ -184,8 +180,7 @@ where pub(crate) fn handshake( io: Io, config: &ConnectorConfig, -) -> impl Future, Connection), h2::Error>> -{ +) -> impl Future, Connection), h2::Error>> { let mut builder = Builder::new(); builder .initial_window_size(config.stream_window_size) diff --git a/actix-http/src/client/mod.rs b/awc/src/client/mod.rs similarity index 95% rename from actix-http/src/client/mod.rs rename to awc/src/client/mod.rs index 41d5fef2..3abbf50a 100644 --- a/actix-http/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -17,7 +17,6 @@ pub use actix_tls::connect::{ pub use self::connection::{Connection, ConnectionIo}; pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use crate::Protocol; #[derive(Clone)] pub struct Connect { diff --git a/actix-http/src/client/pool.rs b/awc/src/client/pool.rs similarity index 95% rename from actix-http/src/client/pool.rs rename to awc/src/client/pool.rs index 88188038..229c6324 100644 --- a/actix-http/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -14,22 +14,20 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; +use actix_http::Protocol; use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use ahash::AHashMap; use futures_core::future::LocalBoxFuture; use http::uri::Authority; -use pin_project::pin_project; +use pin_project_lite::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use super::config::ConnectorConfig; -use super::connection::{ - ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner, -}; +use super::connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner}; use super::error::ConnectError; use super::h2proto::handshake; use super::Connect; -use super::Protocol; #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub struct Key { @@ -152,9 +150,7 @@ where impl Service for ConnectionPool where - S: Service - + Clone - + 'static, + S: Service + Clone + 'static, Io: ConnectionIo, { type Response = ConnectionType; @@ -195,8 +191,8 @@ where let config = &inner.config; let idle_dur = now - c.used; let age = now - c.created; - let conn_ineligible = idle_dur > config.conn_keep_alive - || age > config.conn_lifetime; + let conn_ineligible = + idle_dur > config.conn_keep_alive || age > config.conn_lifetime; if conn_ineligible { // drop connections that are too old @@ -231,9 +227,7 @@ where // match the connection and spawn new one if did not get anything. match conn { - Some(conn) => { - Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)) - } + Some(conn) => Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)), None => { let (io, proto) = connector.call(req).await?; @@ -284,9 +278,7 @@ where let mut read_buf = ReadBuf::new(&mut buf); let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) { - Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => { - ConnectionState::Tainted - } + Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => ConnectionState::Tainted, Poll::Pending => ConnectionState::Live, _ => ConnectionState::Skip, @@ -302,11 +294,13 @@ struct PooledConnection { created: Instant, } -#[pin_project] -struct CloseConnection { - io: Io, - #[pin] - timeout: Sleep, +pin_project! { + #[project = CloseConnectionProj] + struct CloseConnection { + io: Io, + #[pin] + timeout: Sleep, + } } impl CloseConnection @@ -413,17 +407,11 @@ mod test { unimplemented!() } - fn poll_flush( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll> { + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { unimplemented!() } - fn poll_shutdown( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 6a9fc463..f27a8c36 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -8,16 +8,14 @@ use std::{ use actix_codec::Framed; use actix_http::{ - body::Body, - client::{ - Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, - }, - h1::ClientCodec, - Payload, RequestHead, RequestHeadType, ResponseHead, + body::Body, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, }; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; +use crate::client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, +}; use crate::response::ClientResponse; pub type BoxConnectorService = Rc< diff --git a/awc/src/error.rs b/awc/src/error.rs index c83c5ebb..d415efe9 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,15 +1,15 @@ //! HTTP client errors -pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use actix_http::error::PayloadError; -pub use actix_http::http::Error as HttpError; -pub use actix_http::ws::HandshakeError as WsHandshakeError; -pub use actix_http::ws::ProtocolError as WsProtocolError; +pub use actix_http::{ + error::PayloadError, + http::{header::HeaderValue, Error as HttpError, StatusCode}, + ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError}, +}; +use derive_more::{Display, From}; use serde_json::error::Error as JsonError; -use actix_http::http::{header::HeaderValue, StatusCode}; -use derive_more::{Display, From}; +pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; /// Websocket client error #[derive(Debug, Display, From)] diff --git a/awc/src/lib.rs b/awc/src/lib.rs index c0290ddc..05f97aa3 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -104,22 +104,8 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] -use std::{convert::TryFrom, rc::Rc, time::Duration}; - -#[cfg(feature = "cookies")] -pub use cookie; - -pub use actix_http::{client::Connector, http}; - -use actix_http::{ - client::{TcpConnect, TcpConnectError, TcpConnection}, - http::{Error as HttpError, HeaderMap, Method, Uri}, - RequestHead, -}; -use actix_rt::net::TcpStream; -use actix_service::Service; - mod builder; +mod client; mod connect; pub mod error; mod frozen; @@ -130,13 +116,29 @@ mod sender; pub mod test; pub mod ws; +pub use actix_http::http; +#[cfg(feature = "cookies")] +pub use cookie; + pub use self::builder::ClientBuilder; +pub use self::client::Connector; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::sender::SendClientRequest; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{ + http::{Error as HttpError, HeaderMap, Method, Uri}, + RequestHead, +}; +use actix_rt::net::TcpStream; +use actix_service::Service; + +use self::client::{TcpConnect, TcpConnectError, TcpConnection}; + /// An asynchronous HTTP and WebSocket client. /// /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index a8c14d54..8a79a659 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -9,7 +9,6 @@ use std::{ use actix_http::{ body::Body, - client::{InvalidUrl, SendRequestError}, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -19,6 +18,7 @@ use futures_core::ready; use super::Transform; +use crate::client::{InvalidUrl, SendRequestError}; use crate::connect::{ConnectRequest, ConnectResponse}; use crate::ClientResponse; From 855e260fdb0f983537c463fd8d64c3eeb38ab546 Mon Sep 17 00:00:00 2001 From: Luca Palmieri Date: Tue, 26 Oct 2021 09:24:38 +0100 Subject: [PATCH 42/99] Add `html_utf8` content type. (#2423) --- CHANGES.md | 2 ++ src/http/header/content_type.rs | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9d7b3180..3be41d46 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* `ContentType::html` now returns `Content-Type: text/html; charset=utf-8` instead of `Content-Type: text/html`. ## 4.0.0-beta.10 - 2021-10-20 diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index e1c419c2..23046000 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -60,52 +60,53 @@ crate::http::header::common_header! { } impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` + /// A constructor to easily create a `Content-Type: application/json` /// header. #[inline] pub fn json() -> ContentType { ContentType(mime::APPLICATION_JSON) } - /// A constructor to easily create a `Content-Type: text/plain; + /// A constructor to easily create a `Content-Type: text/plain; /// charset=utf-8` header. #[inline] pub fn plaintext() -> ContentType { ContentType(mime::TEXT_PLAIN_UTF_8) } - /// A constructor to easily create a `Content-Type: text/html` header. + /// A constructor to easily create a `Content-Type: text/html; charset=utf-8` + /// header. #[inline] pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) + ContentType(mime::TEXT_HTML_UTF_8) } - /// A constructor to easily create a `Content-Type: text/xml` header. + /// A constructor to easily create a `Content-Type: text/xml` header. #[inline] pub fn xml() -> ContentType { ContentType(mime::TEXT_XML) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/www-form-url-encoded` header. #[inline] pub fn form_url_encoded() -> ContentType { ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) } - /// A constructor to easily create a `Content-Type: image/jpeg` header. + /// A constructor to easily create a `Content-Type: image/jpeg` header. #[inline] pub fn jpeg() -> ContentType { ContentType(mime::IMAGE_JPEG) } - /// A constructor to easily create a `Content-Type: image/png` header. + /// A constructor to easily create a `Content-Type: image/png` header. #[inline] pub fn png() -> ContentType { ContentType(mime::IMAGE_PNG) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/octet-stream` header. #[inline] pub fn octet_stream() -> ContentType { From be9530eb729035cf4225753a2d6f64a12e0e6140 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 26 Oct 2021 20:16:48 +0800 Subject: [PATCH 43/99] avoid building actix-tls with no-default-features (#2426) --- actix-http/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3d45cc8c..7d39e000 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -27,10 +27,10 @@ path = "src/lib.rs" default = [] # openssl -openssl = ["actix-tls/openssl"] +openssl = ["actix-tls/accept", "actix-tls/openssl"] # rustls support -rustls = ["actix-tls/rustls"] +rustls = ["actix-tls/accept", "actix-tls/rustls"] # enable compression support compress-brotli = ["brotli2", "__compress"] @@ -46,7 +46,6 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.7", features = ["accept"] } ahash = "0.7" base64 = "0.13" @@ -74,6 +73,9 @@ sha-1 = "0.9" smallvec = "1.6.1" tokio = { version = "1.2", features = ["sync"] } +# tls +actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } + # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } From ec6d284a8e393d8c8f5bf60eece49abbab62658f Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Sun, 31 Oct 2021 16:19:21 +0300 Subject: [PATCH 44/99] improve "data no configured" message (#2429) --- src/data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data.rs b/src/data.rs index 7e01d346..d27ad196 100644 --- a/src/data.rs +++ b/src/data.rs @@ -137,7 +137,7 @@ impl FromRequest for Data { type_name::(), ); err(ErrorInternalServerError( - "App data is not configured, to configure use App::data()", + "App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()", )) } } From 6ec2d7b90966bc55b72ee4c90129074742d087bc Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 4 Nov 2021 23:15:23 +0800 Subject: [PATCH 45/99] add keep alive to h2 through ping pong (#2433) --- actix-http/src/h2/dispatcher.rs | 147 ++++++++++++++++++-------- actix-http/tests/test_h2_ping_pong.rs | 77 ++++++++++++++ 2 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 actix-http/tests/test_h2_ping_pong.rs diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index ea149b1e..7326dfff 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -10,11 +10,15 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite}; +use actix_rt::time::Sleep; use actix_service::Service; use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; use futures_core::ready; -use h2::server::{Connection, SendResponse}; +use h2::{ + server::{Connection, SendResponse}, + Ping, PingPong, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use log::{error, trace}; use pin_project_lite::pin_project; @@ -36,29 +40,46 @@ pin_project! { on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, - _phantom: PhantomData, + ping_pong: Option, + _phantom: PhantomData } } -impl Dispatcher { +impl Dispatcher +where + T: AsyncRead + AsyncWrite + Unpin, +{ pub(crate) fn new( flow: Rc>, - connection: Connection, + mut connection: Connection, on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, ) -> Self { + let ping_pong = config.keep_alive_timer().map(|timer| H2PingPong { + timer: Box::pin(timer), + on_flight: false, + ping_pong: connection.ping_pong().unwrap(), + }); + Self { flow, config, peer_addr, connection, on_connect_data, + ping_pong, _phantom: PhantomData, } } } +struct H2PingPong { + timer: Pin>, + on_flight: bool, + ping_pong: PingPong, +} + impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, @@ -77,54 +98,92 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - while let Some((req, tx)) = - ready!(Pin::new(&mut this.connection).poll_accept(cx)?) - { - let (parts, body) = req.into_parts(); - let pl = crate::h2::Payload::new(body); - let pl = Payload::::H2(pl); - let mut req = Request::with_payload(pl); + loop { + match Pin::new(&mut this.connection).poll_accept(cx)? { + Poll::Ready(Some((req, tx))) => { + let (parts, body) = req.into_parts(); + let pl = crate::h2::Payload::new(body); + let pl = Payload::::H2(pl); + let mut req = Request::with_payload(pl); - let head = req.head_mut(); - head.uri = parts.uri; - head.method = parts.method; - head.version = parts.version; - head.headers = parts.headers.into(); - head.peer_addr = this.peer_addr; + let head = req.head_mut(); + head.uri = parts.uri; + head.method = parts.method; + head.version = parts.version; + head.headers = parts.headers.into(); + head.peer_addr = this.peer_addr; - // merge on_connect_ext data into request extensions - this.on_connect_data.merge_into(&mut req); + // merge on_connect_ext data into request extensions + this.on_connect_data.merge_into(&mut req); - let fut = this.flow.service.call(req); - let config = this.config.clone(); + let fut = this.flow.service.call(req); + let config = this.config.clone(); - // multiplex request handling with spawn task - actix_rt::spawn(async move { - // resolve service call and send response. - let res = match fut.await { - Ok(res) => handle_response(res.into(), tx, config).await, - Err(err) => { - let res: Response = err.into(); - handle_response(res, tx, config).await - } - }; + // multiplex request handling with spawn task + actix_rt::spawn(async move { + // resolve service call and send response. + let res = match fut.await { + Ok(res) => handle_response(res.into(), tx, config).await, + Err(err) => { + let res: Response = err.into(); + handle_response(res, tx, config).await + } + }; - // log error. - if let Err(err) = res { - match err { - DispatchError::SendResponse(err) => { - trace!("Error sending HTTP/2 response: {:?}", err) + // log error. + if let Err(err) = res { + match err { + DispatchError::SendResponse(err) => { + trace!("Error sending HTTP/2 response: {:?}", err) + } + DispatchError::SendData(err) => warn!("{:?}", err), + DispatchError::ResponseBody(err) => { + error!("Response payload stream error: {:?}", err) + } + } } - DispatchError::SendData(err) => warn!("{:?}", err), - DispatchError::ResponseBody(err) => { - error!("Response payload stream error: {:?}", err) - } - } + }); } - }); - } + Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => match this.ping_pong.as_mut() { + Some(ping_pong) => loop { + if ping_pong.on_flight { + // When have on flight ping pong. poll pong and and keep alive timer. + // on success pong received update keep alive timer to determine the next timing of + // ping pong. + match ping_pong.ping_pong.poll_pong(cx)? { + Poll::Ready(_) => { + ping_pong.on_flight = false; - Poll::Ready(Ok(())) + let dead_line = + this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + } + Poll::Pending => { + return ping_pong + .timer + .as_mut() + .poll(cx) + .map(|_| Ok(())) + } + } + } else { + // When there is no on flight ping pong. keep alive timer is used to wait for next + // timing of ping pong. Therefore at this point it serves as an interval instead. + ready!(ping_pong.timer.as_mut().poll(cx)); + + ping_pong.ping_pong.send_ping(Ping::opaque())?; + + let dead_line = this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + + ping_pong.on_flight = true; + } + }, + None => return Poll::Pending, + }, + } + } } } diff --git a/actix-http/tests/test_h2_ping_pong.rs b/actix-http/tests/test_h2_ping_pong.rs new file mode 100644 index 00000000..5e03785a --- /dev/null +++ b/actix-http/tests/test_h2_ping_pong.rs @@ -0,0 +1,77 @@ +use std::io; + +use actix_http::{error::Error, HttpService, Response}; +use actix_server::Server; + +#[actix_rt::test] +async fn h2_ping_pong() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let handle = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(3) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(handle.clone()).unwrap(); + + handle.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for h2 client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + let (mut tx, conn) = h2::client::handshake(stream).await.unwrap(); + + tokio::spawn(async move { conn.await.unwrap() }); + + let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap(); + let res = res.await.unwrap(); + + assert_eq!(res.status().as_u16(), 200); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it can not answer ping pong. + std::thread::sleep(std::time::Duration::from_secs(1000)); + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from keep alive and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} From 5e554dca35844c241e813663da09e20575d586d3 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 4 Nov 2021 23:57:55 +0800 Subject: [PATCH 46/99] fix awc clippy warning (#2431) --- awc/src/client/pool.rs | 3 ++- awc/tests/test_client.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 229c6324..9d130412 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -19,6 +19,7 @@ use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use ahash::AHashMap; use futures_core::future::LocalBoxFuture; +use futures_util::FutureExt; use http::uri::Authority; use pin_project_lite::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; @@ -201,7 +202,7 @@ where // check if the connection is still usable if let ConnectionInnerType::H1(ref mut io) = c.conn { let check = ConnectionCheckFuture { io }; - match check.await { + match check.now_or_never().expect("ConnectionCheckFuture must never yield with Poll::Pending.") { ConnectionState::Tainted => { inner.close(c.conn); continue; diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 615789fb..a0af0cab 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -795,17 +795,15 @@ async fn client_unread_response() { let lst = std::net::TcpListener::bind(addr).unwrap(); std::thread::spawn(move || { - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream.write_all( - b"HTTP/1.1 200 OK\r\n\ + let (mut stream, _) = lst.accept().unwrap(); + let mut b = [0; 1000]; + let _ = stream.read(&mut b).unwrap(); + let _ = stream.write_all( + b"HTTP/1.1 200 OK\r\n\ connection: close\r\n\ \r\n\ welcome!", - ); - } + ); }); // client request From c020cedb631d08528d42de24741975b0a5b6e0b6 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Sun, 7 Nov 2021 12:02:23 -0500 Subject: [PATCH 47/99] Log internal server errors (#2387) --- src/middleware/logger.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 961eca49..f3f6aa5a 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -275,9 +275,7 @@ where }; if let Some(error) = res.response().error() { - if res.response().head().status != StatusCode::INTERNAL_SERVER_ERROR { - debug!("Error in response: {:?}", error); - } + debug!("Error in response: {:?}", error); } if let Some(ref mut format) = this.format { From 2754608f3c98821a28c4f2a67d2ef40612587c04 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 8 Nov 2021 02:46:43 +0000 Subject: [PATCH 48/99] fix codegen tests --- src/middleware/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index f3f6aa5a..b4d100b3 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -22,7 +22,7 @@ use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ dev::{BodySize, MessageBody}, - http::{HeaderName, StatusCode}, + http::HeaderName, service::{ServiceRequest, ServiceResponse}, Error, HttpResponse, Result, }; From a2f59c02f72c0c50cb69dd9e17439f70b6a8b6db Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 15 Nov 2021 04:03:33 +0000 Subject: [PATCH 49/99] bump actix-server to beta 9 (#2442) --- CHANGES.md | 12 ++- Cargo.toml | 10 +-- README.md | 4 +- actix-files/Cargo.toml | 8 +- actix-http-test/CHANGES.md | 7 ++ actix-http-test/Cargo.toml | 13 +-- actix-http-test/README.md | 4 +- actix-http-test/src/lib.rs | 105 ++++++++++++++-------- actix-http/CHANGES.md | 7 ++ actix-http/Cargo.toml | 10 +-- actix-http/README.md | 4 +- actix-http/tests/test_h2_ping_pong.rs | 6 +- actix-http/tests/test_openssl.rs | 33 +++---- actix-http/tests/test_rustls.rs | 5 +- actix-http/tests/test_server.rs | 82 +++++++++++++---- actix-multipart/Cargo.toml | 4 +- actix-router/Cargo.toml | 2 +- actix-test/CHANGES.md | 4 + actix-test/Cargo.toml | 11 +-- actix-test/src/lib.rs | 79 +++++++++++----- actix-web-actors/Cargo.toml | 8 +- actix-web-codegen/Cargo.toml | 4 +- awc/CHANGES.md | 3 + awc/Cargo.toml | 16 ++-- awc/README.md | 4 +- awc/src/client/connection.rs | 3 +- awc/tests/test_rustls_client.rs | 3 +- examples/on_connect.rs | 1 + src/dev.rs | 2 +- src/server.rs | 4 +- tests/test_httpserver.rs | 125 ++++++++++++-------------- tests/test_server.rs | 68 +++++++++++++- 32 files changed, 415 insertions(+), 236 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3be41d46..4ecbd0c2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,18 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.11 - 2021-11-15 +### Added +* Re-export `dev::ServerHandle` from `actix-server`. [#2442] + ### Changed -* `ContentType::html` now returns `Content-Type: text/html; charset=utf-8` instead of `Content-Type: text/html`. +* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + +[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2442]: https://github.com/actix/actix-web/pull/2442 ## 4.0.0-beta.10 - 2021-10-20 diff --git a/Cargo.toml b/Cargo.toml index 15228220..8ca34c92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.10" +version = "4.0.0-beta.11" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -69,12 +69,12 @@ __compress = [] actix-codec = "0.4.0" actix-macros = "0.2.3" actix-rt = "2.2" -actix-server = "2.0.0-beta.3" +actix-server = "2.0.0-beta.9" actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" actix-router = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.5" @@ -104,8 +104,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.9", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.10", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index 25b59536..9444f130 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web/4.0.0-beta.10) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.11)](https://docs.rs/actix-web/4.0.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.10) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.11)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 3d734060..bbb9f551 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false } -actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.11", default-features = false } +actix-http = "3.0.0-beta.12" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.10" -actix-test = "0.1.0-beta.5" +actix-web = "4.0.0-beta.11" +actix-test = "0.1.0-beta.6" diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 98b197bc..ea00acb0 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,8 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.6 - 2021-11-15 +* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] * Minimum supported Rust version (MSRV) is now 1.52. +[#2442]: https://github.com/actix/actix-web/pull/2442 + ## 3.0.0-beta.5 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index d3fc8a47..b111f868 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.5" +version = "3.0.0-beta.6" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -34,13 +34,13 @@ actix-codec = "0.4.0" actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" -actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.9", default-features = false } +actix-server = "2.0.0-beta.9" +awc = { version = "3.0.0-beta.10", default-features = false } base64 = "0.13" bytes = "1" futures-core = { version = "0.3.7", default-features = false } -http = "0.2.2" +http = "0.2.5" log = "0.4" socket2 = "0.4" serde = "1.0" @@ -48,7 +48,8 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } +tokio = { version = "1.2", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.12" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 6bf0d710..3eee6645 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.6)](https://docs.rs/actix-http-test/3.0.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index c7b083b5..cda98cea 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -7,7 +7,7 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -use std::{net, sync::mpsc, thread, time::Duration}; +use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; @@ -19,6 +19,7 @@ use bytes::Bytes; use futures_core::stream::Stream; use http::Method; use socket2::{Domain, Protocol, Socket, Type}; +use tokio::sync::mpsc; /// Start test server /// @@ -55,12 +56,13 @@ pub async fn test_server>(factory: F) -> TestServer test_server_with_addr(tcp, factory).await } -/// Start [`test server`](test_server()) on a concrete Address +/// Start [`test server`](test_server()) on an existing address binding. pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { - let (tx, rx) = mpsc::channel(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); // run server in separate thread thread::spawn(move || { @@ -68,59 +70,73 @@ pub async fn test_server_with_addr>( let local_addr = tcp.local_addr().unwrap(); let srv = Server::build() - .listen("test", tcp, factory)? .workers(1) - .disable_signals(); + .disable_signals() + .listen("test", tcp, factory) + .expect("test server could not be created"); - sys.block_on(async { - srv.run(); - tx.send((System::current(), local_addr)).unwrap(); - }); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - sys.run() + // drive server loop + sys.block_on(srv).unwrap(); + + // start system event loop + sys.run().unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + let _ = thread_stop_tx.send(()); }); - let (system, addr) = rx.recv().unwrap(); + let (system, server, addr) = started_rx.recv().unwrap(); let client = { + #[cfg(feature = "openssl")] let connector = { - #[cfg(feature = "openssl")] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let _ = builder - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - Connector::new() - .conn_lifetime(Duration::from_secs(0)) - .timeout(Duration::from_millis(30000)) - .ssl(builder.build()) - } - #[cfg(not(feature = "openssl"))] - { - Connector::new() - .conn_lifetime(Duration::from_secs(0)) - .timeout(Duration::from_millis(30000)) - } + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) + .ssl(builder.build()) + }; + + #[cfg(not(feature = "openssl"))] + let connector = { + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) }; Client::builder().connector(connector).finish() }; TestServer { - addr, + server, client, system, + addr, + thread_stop_rx, } } /// Test server controller pub struct TestServer { + server: actix_server::ServerHandle, + client: awc::Client, + system: actix_rt::System, addr: net::SocketAddr, - client: Client, - system: System, + thread_stop_rx: mpsc::Receiver<()>, } impl TestServer { @@ -257,15 +273,32 @@ impl TestServer { self.client.headers() } - /// Stop HTTP server - fn stop(&mut self) { + /// Gracefully stop HTTP server. + /// + /// Waits for spawned `Server` and `System` to shutdown gracefully. + pub async fn stop(&mut self) { + // signal server to stop + self.server.stop(true).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too self.system.stop(); + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; } } impl Drop for TestServer { fn drop(&mut self) { - self.stop() + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); } } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 81595c92..9ec75b4b 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,11 +1,18 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.12 - 2021-11-15 +### Changed +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + ### Removed * `client` module. [#2425] * `trust-dns` feature. [#2425] [#2425]: https://github.com/actix/actix-web/pull/2425 +[#2442]: https://github.com/actix/actix-web/pull/2442 ## 3.0.0-beta.11 - 2021-10-20 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 7d39e000..78431244 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.11" +version = "3.0.0-beta.12" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -57,7 +57,7 @@ encoding_rs = "0.8" 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" +http = "0.2.5" httparse = "1.5.1" httpdate = "1.0.1" itoa = "0.4" @@ -71,7 +71,6 @@ pin-project-lite = "0.2" rand = "0.8" sha-1 = "0.9" smallvec = "1.6.1" -tokio = { version = "1.2", features = ["sync"] } # tls actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } @@ -82,8 +81,8 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.7", optional = true } [dev-dependencies] -actix-server = "2.0.0-beta.3" -actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-server = "2.0.0-beta.9" +actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } @@ -95,6 +94,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } +tokio = { version = "1.2", features = ["net", "rt"] } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 5b1e552f..536d1707 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.11)](https://docs.rs/actix-http/3.0.0-beta.11) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.12)](https://docs.rs/actix-http/3.0.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.11/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.11) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.12) [![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/tests/test_h2_ping_pong.rs b/actix-http/tests/test_h2_ping_pong.rs index 5e03785a..30ce9aa5 100644 --- a/actix-http/tests/test_h2_ping_pong.rs +++ b/actix-http/tests/test_h2_ping_pong.rs @@ -13,7 +13,7 @@ async fn h2_ping_pong() -> io::Result<()> { let join = std::thread::spawn(move || { actix_rt::System::new().block_on(async move { - let handle = Server::build() + let srv = Server::build() .disable_signals() .workers(1) .listen("h2_ping_pong", lst, || { @@ -24,9 +24,9 @@ async fn h2_ping_pong() -> io::Result<()> { })? .run(); - tx.send(handle.clone()).unwrap(); + tx.send(srv.handle()).unwrap(); - handle.await + srv.await }) }); diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index a58d0cc7..0eaaabcc 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -8,7 +8,7 @@ use actix_http::{ body::{AnyBody, Body, SizedStream}, error::PayloadError, http::{ - header::{self, HeaderName, HeaderValue}, + header::{self, HeaderValue}, Method, StatusCode, Version, }, Error, HttpMessage, HttpService, Request, Response, @@ -143,38 +143,25 @@ async fn test_h2_content_length() { }) .await; - let header = HeaderName::from_static("content-length"); - let value = HeaderValue::from_static("0"); + static VALUE: HeaderValue = HeaderValue::from_static("0"); { - for &i in &[0] { - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let _response = req.await.expect_err("should timeout on recv 1xx frame"); - // assert_eq!(response.headers().get(&header), None); + let req = srv.request(Method::HEAD, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); - let req = srv - .request(Method::GET, srv.surl(&format!("/{}", i))) - .send(); - let _response = req.await.expect_err("should timeout on recv 1xx frame"); - // assert_eq!(response.headers().get(&header), None); - } + let req = srv.request(Method::GET, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); - for &i in &[1] { - let req = srv - .request(Method::GET, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); - } + let req = srv.request(Method::GET, srv.surl("/1")).send(); + let response = req.await.unwrap(); + assert!(response.headers().get("content-length").is_none()); for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), Some(&value)); + assert_eq!(response.headers().get("content-length"), Some(&VALUE)); } } } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 924ef49a..a9f6e99f 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -26,10 +26,7 @@ use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; -use rustls::{ - Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore, - ServerConfig as RustlsServerConfig, ServerName, -}; +use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(mut stream: S) -> Result diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index c04aeae0..ea78ce11 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -24,7 +24,7 @@ use regex::Regex; #[actix_rt::test] async fn test_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -39,11 +39,13 @@ async fn test_h1() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -59,6 +61,8 @@ async fn test_h1_2() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } #[derive(Debug, Display, Error)] @@ -73,7 +77,7 @@ impl From for Response { #[actix_rt::test] async fn test_expect_continue() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { if req.head().uri.query() == Some("yes=") { @@ -98,11 +102,13 @@ async fn test_expect_continue() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] async fn test_expect_continue_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { sleep(Duration::from_millis(20)).then(move |_| { @@ -129,6 +135,8 @@ async fn test_expect_continue_h1() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] @@ -136,7 +144,7 @@ async fn test_chunked_payload() { let chunk_sizes = vec![32768, 32, 32768]; let total_size: usize = chunk_sizes.iter().sum(); - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(fn_service(|mut request: Request| { request @@ -188,11 +196,13 @@ async fn test_chunked_payload() { }; assert_eq!(returned_size, total_size); + + srv.stop().await; } #[actix_rt::test] async fn test_slow_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .client_timeout(100) .finish(|_| ok::<_, Infallible>(Response::ok())) @@ -205,11 +215,13 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_malformed_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -221,11 +233,13 @@ async fn test_http1_malformed_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 400 Bad Request")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -242,11 +256,13 @@ async fn test_http1_keepalive() { let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_timeout() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(1) .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -264,11 +280,13 @@ async fn test_http1_keepalive_timeout() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -285,11 +303,13 @@ async fn test_http1_keepalive_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive_default_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -305,11 +325,13 @@ async fn test_http10_keepalive_default_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -332,11 +354,13 @@ async fn test_http10_keepalive() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_disabled() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -353,6 +377,8 @@ async fn test_http1_keepalive_disabled() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] @@ -362,7 +388,7 @@ async fn test_content_length() { StatusCode, }; - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); @@ -400,6 +426,8 @@ async fn test_content_length() { assert_eq!(response.headers().get(&header), Some(&value)); } } + + srv.stop().await; } #[actix_rt::test] @@ -439,6 +467,8 @@ async fn test_h1_headers() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from(data2)); + + srv.stop().await; } const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -478,6 +508,8 @@ async fn test_h1_body() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -503,6 +535,8 @@ async fn test_h1_head_empty() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] @@ -528,11 +562,13 @@ async fn test_h1_head_binary() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_head_binary2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() @@ -549,6 +585,8 @@ async fn test_h1_head_binary2() { .unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); } + + srv.stop().await; } #[actix_rt::test] @@ -571,6 +609,8 @@ async fn test_h1_body_length() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -606,6 +646,8 @@ async fn test_h1_body_chunked_explicit() { // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -635,6 +677,8 @@ async fn test_h1_body_chunked_implicit() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -662,6 +706,8 @@ async fn test_h1_response_http_error_handling() { bytes, Bytes::from_static(b"error processing HTTP: failed to parse header value") ); + + srv.stop().await; } #[derive(Debug, Display, Error)] @@ -689,11 +735,13 @@ async fn test_h1_service_error() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_on_connect() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .on_connect_ext(|_, data| { data.insert(20isize); @@ -708,4 +756,6 @@ async fn test_h1_on_connect() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 92637cef..b2f3e391 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -14,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-web = { version = "4.0.0-beta.11", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -29,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index e32f0edd..b95bca50 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -30,7 +30,7 @@ serde = "1" [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } firestorm = { version = "0.4", features = ["enable_system_time"] } -http = "0.2.3" +http = "0.2.5" serde = { version = "1", features = ["derive"] } [[bench]] diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 07089258..5c22139a 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.6 - 2021-11-15 +* No significant changes from `0.1.0-beta.5`. + + ## 0.1.0-beta.5 - 2021-10-20 * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 002e7662..58c0d31a 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.5" +version = "0.1.0-beta.6" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.11" -actix-http-test = "3.0.0-beta.5" +actix-http = "3.0.0-beta.12" +actix-http-test = "3.0.0-beta.6" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.10", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } @@ -45,3 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } +tokio = { version = "1.2", features = ["sync"] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 23a7eeba..cf5738aa 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -31,7 +31,7 @@ extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] extern crate tls_rustls as rustls; -use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time}; +use std::{error::Error as StdError, fmt, net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; @@ -41,8 +41,9 @@ use actix_http::{ }; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_web::{ - dev::{AppConfig, MessageBody, Server, Service}, - rt, web, Error, + dev::{AppConfig, MessageBody, Server, ServerHandle, Service}, + rt::{self, System}, + web, Error, }; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use futures_core::Stream; @@ -52,6 +53,7 @@ pub use actix_web::test::{ call_service, default_service, init_service, load_stream, ok_service, read_body, read_body_json, read_response, read_response_json, TestRequest, }; +use tokio::sync::mpsc; /// Start default [`TestServer`]. /// @@ -128,7 +130,11 @@ where B: MessageBody + 'static, B::Error: Into>, { - let (tx, rx) = mpsc::channel(); + // for sending handles and server info back from the spawned thread + let (started_tx, started_rx) = std::sync::mpsc::channel(); + + // for signaling the shutdown of spawned server and system + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); let tls = match cfg.stream { StreamType::Tcp => false, @@ -138,7 +144,7 @@ where StreamType::Rustls(_) => true, }; - // run server in separate thread + // run server in separate orphaned thread thread::spawn(move || { let sys = rt::System::new(); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); @@ -146,7 +152,7 @@ where let factory = factory.clone(); let srv_cfg = cfg.clone(); let timeout = cfg.client_timeout; - let builder = Server::build().workers(1).disable_signals(); + let builder = Server::build().workers(1).disable_signals().system_exit(); let srv = match srv_cfg.stream { StreamType::Tcp => match srv_cfg.tp { @@ -275,17 +281,25 @@ where }), }, } - .unwrap(); + .expect("test server could not be created"); - sys.block_on(async { - let srv = srv.run(); - tx.send((rt::System::current(), srv, local_addr)).unwrap(); - }); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - sys.run() + // drive server loop + sys.block_on(srv).unwrap(); + + // start system event loop + sys.run().unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + let _ = thread_stop_tx.send(()); }); - let (system, server, addr) = rx.recv().unwrap(); + let (system, server, addr) = started_rx.recv().unwrap(); let client = { let connector = { @@ -299,15 +313,15 @@ where .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) .ssl(builder.build()) } #[cfg(not(feature = "openssl"))] { Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) } }; @@ -315,11 +329,12 @@ where }; TestServer { - addr, + server, + thread_stop_rx, client, system, + addr, tls, - server, } } @@ -405,11 +420,12 @@ impl TestServerConfig { /// /// See [`start`] for usage example. pub struct TestServer { - addr: net::SocketAddr, + server: ServerHandle, + thread_stop_rx: mpsc::Receiver<()>, client: awc::Client, system: rt::System, + addr: net::SocketAddr, tls: bool, - server: Server, } impl TestServer { @@ -505,15 +521,30 @@ impl TestServer { } /// Gracefully stop HTTP server. - pub async fn stop(self) { + /// + /// Waits for spawned `Server` and `System` to shutdown gracefully. + pub async fn stop(mut self) { + // signal server to stop self.server.stop(true).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too self.system.stop(); - rt::time::sleep(time::Duration::from_millis(100)).await; + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; } } impl Drop for TestServer { fn drop(&mut self) { - self.system.stop() + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); } } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 2d987a13..706a90c0 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.11" -actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-http = "3.0.0-beta.12" +actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" bytestring = "1" @@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.5" +actix-test = "0.1.0-beta.6" -awc = { version = "3.0.0-beta.9", default-features = false } +awc = { version = "3.0.0-beta.10", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index c04ca435..a407d00f 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.5" +actix-test = "0.1.0-beta.6" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.10" +actix-web = "4.0.0-beta.11" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 5682a237..6b9531c7 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.10 - 2021-11-15 + + ## 3.0.0-beta.9 - 2021-10-20 * Updated rustls to v0.20. [#2414] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 6eeb9ce5..ce710d58 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.0.0-beta.7", features = ["connect"] } actix-utils = "3.0.0" @@ -68,7 +68,7 @@ derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } h2 = "0.3" -http = "0.2" +http = "0.2.5" itoa = "0.4" log =" 0.4" mime = "0.3" @@ -88,13 +88,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.12", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-utils = "3.0.0" -actix-server = "2.0.0-beta.3" +actix-server = "2.0.0-beta.9" actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" diff --git a/awc/README.md b/awc/README.md index 67bcb965..96c5ed40 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.9)](https://docs.rs/awc/3.0.0-beta.9) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.10)](https://docs.rs/awc/3.0.0-beta.10) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.9/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.10/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.10) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 97b96fc0..6bbc9ad0 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -173,6 +173,7 @@ impl H2ConnectionInner { /// Cancel spawned connection task on drop. impl Drop for H2ConnectionInner { fn drop(&mut self) { + // TODO: this can end up sending extraneous requests; see if there is a better way to handle if self .sender .send_request(http::Request::new(()), true) @@ -183,8 +184,8 @@ impl Drop for H2ConnectionInner { } } +/// Unified connection type cover HTTP/1 Plain/TLS and HTTP/2 protocols. #[allow(dead_code)] -/// Unified connection type cover Http1 Plain/Tls and Http2 protocols pub enum Connection> where A: ConnectionIo, diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index c075a609..355fcb6f 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -19,8 +19,7 @@ use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ client::{ServerCertVerified, ServerCertVerifier}, - Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerConfig, - ServerName, + Certificate, ClientConfig, PrivateKey, ServerConfig, ServerName, }; use rustls_pemfile::{certs, pkcs8_private_keys}; diff --git a/examples/on_connect.rs b/examples/on_connect.rs index 24ac86c6..9709835e 100644 --- a/examples/on_connect.rs +++ b/examples/on_connect.rs @@ -8,6 +8,7 @@ use std::{any::Any, io, net::SocketAddr}; use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer}; +#[allow(dead_code)] #[derive(Debug, Clone)] struct ConnectionInfo { bind: SocketAddr, diff --git a/src/dev.rs b/src/dev.rs index be3af86a..4fac207a 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -20,7 +20,7 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; -pub use actix_server::Server; +pub use actix_server::{Server, ServerHandle}; pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; diff --git a/src/server.rs b/src/server.rs index f15183f8..0f3d7c59 100644 --- a/src/server.rs +++ b/src/server.rs @@ -159,7 +159,7 @@ where /// /// By default max connections is set to a 25k. pub fn max_connections(mut self, num: usize) -> Self { - self.builder = self.builder.maxconn(num); + self.builder = self.builder.max_concurrent_connections(num); self } @@ -233,7 +233,7 @@ where self } - /// Stop actix system. + /// Stop Actix `System` after server shutdown. pub fn system_exit(mut self) -> Self { self.builder = self.builder.system_exit(); self diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 881c6ce9..887b51d4 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -14,57 +14,45 @@ async fn test_start() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new(); + actix_rt::System::new() + .block_on(async { + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), + ) + }) + .workers(1) + .backlog(1) + .max_connections(10) + .max_connection_rate(10) + .keep_alive(10) + .client_timeout(5000) + .client_shutdown(0) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)) + .unwrap() + .run(); - sys.block_on(async { - let srv = HttpServer::new(|| { - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), - ) + tx.send(srv.handle()).unwrap(); + + srv.await }) - .workers(1) - .backlog(1) - .max_connections(10) - .max_connection_rate(10) - .keep_alive(10) - .client_timeout(5000) - .client_shutdown(0) - .server_hostname("localhost") - .system_exit() - .disable_signals() - .bind(format!("{}", addr)) - .unwrap() - .run(); - - let _ = tx.send((srv, actix_rt::System::current())); - }); - - let _ = sys.run(); + .unwrap(); }); - let (srv, sys) = rx.recv().unwrap(); - #[cfg(feature = "client")] - { - use actix_http::client; + let srv = rx.recv().unwrap(); - let client = awc::Client::builder() - .connector( - client::Connector::new() - .timeout(Duration::from_millis(100)) - .finish(), - ) - .finish(); + let client = awc::Client::builder() + .connector(awc::Connector::new().timeout(Duration::from_millis(100))) + .finish(); - let host = format!("http://{}", addr); - let response = client.get(host.clone()).send().await.unwrap(); - assert!(response.status().is_success()); - } + let host = format!("http://{}", addr); + let response = client.get(host.clone()).send().await.unwrap(); + assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } #[cfg(feature = "openssl")] @@ -92,37 +80,38 @@ fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { #[cfg(feature = "openssl")] async fn test_start_ssl() { use actix_web::HttpRequest; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new(); - let builder = ssl_acceptor(); + actix_rt::System::new() + .block_on(async { + let builder = ssl_acceptor(); - let srv = HttpServer::new(|| { - App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { - assert!(req.app_config().secure()); - HttpResponse::Ok().body("test") - }))) - }) - .workers(1) - .shutdown_timeout(1) - .system_exit() - .disable_signals() - .bind_openssl(format!("{}", addr), builder) - .unwrap(); + let srv = HttpServer::new(|| { + App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { + assert!(req.app_config().secure()); + HttpResponse::Ok().body("test") + }))) + }) + .workers(1) + .shutdown_timeout(1) + .system_exit() + .disable_signals() + .bind_openssl(format!("{}", addr), builder) + .unwrap(); - sys.block_on(async { - let srv = srv.run(); - let _ = tx.send((srv, actix_rt::System::current())); - }); + let srv = srv.run(); + tx.send(srv.handle()).unwrap(); - let _ = sys.run(); + srv.await + }) + .unwrap() }); - let (srv, sys) = rx.recv().unwrap(); + let srv = rx.recv().unwrap(); - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); let _ = builder @@ -141,9 +130,5 @@ async fn test_start_ssl() { let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } diff --git a/tests/test_server.rs b/tests/test_server.rs index ff6f5ae5..d21dac8c 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -127,6 +127,8 @@ async fn test_body() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -154,6 +156,8 @@ async fn test_body_gzip() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -181,6 +185,8 @@ async fn test_body_gzip2() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -241,6 +247,8 @@ async fn test_body_encoding_override() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -275,6 +283,8 @@ async fn test_body_gzip_large() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -314,6 +324,8 @@ async fn test_body_gzip_large_random() { e.read_to_end(&mut dec).unwrap(); assert_eq!(dec.len(), data.len()); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -348,6 +360,8 @@ async fn test_body_chunked_implicit() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -380,6 +394,8 @@ async fn test_body_br_streaming() { let dec = e.finish().unwrap(); println!("T: {:?}", Bytes::copy_from_slice(&dec)); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -401,6 +417,8 @@ async fn test_head_binary() { // read response let bytes = response.body().await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] @@ -420,6 +438,8 @@ async fn test_no_chunking() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -447,6 +467,8 @@ async fn test_body_deflate() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -475,6 +497,8 @@ async fn test_body_brotli() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -503,6 +527,8 @@ async fn test_body_zstd() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -534,6 +560,8 @@ async fn test_body_zstd_streaming() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -559,6 +587,8 @@ async fn test_zstd_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -594,6 +624,8 @@ async fn test_zstd_encoding_large() { // read response let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -619,6 +651,8 @@ async fn test_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -644,6 +678,8 @@ async fn test_gzip_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -670,6 +706,8 @@ async fn test_gzip_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -702,6 +740,8 @@ async fn test_reading_gzip_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -727,6 +767,8 @@ async fn test_reading_deflate_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -753,6 +795,8 @@ async fn test_reading_deflate_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -785,6 +829,8 @@ async fn test_reading_deflate_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -810,6 +856,8 @@ async fn test_brotli_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -845,6 +893,8 @@ async fn test_brotli_encoding_large() { // read response let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[cfg(feature = "openssl")] @@ -861,9 +911,9 @@ async fn test_brotli_encoding_large_openssl() { }); // body - let mut e = BrotliEncoder::new(Vec::new(), 3); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); + let mut enc = BrotliEncoder::new(Vec::new(), 3); + enc.write_all(data.as_ref()).unwrap(); + let enc = enc.finish().unwrap(); // client request let mut response = srv @@ -877,6 +927,8 @@ async fn test_brotli_encoding_large_openssl() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[cfg(feature = "rustls")] @@ -944,6 +996,8 @@ mod plus_rustls { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } } @@ -998,6 +1052,8 @@ async fn test_server_cookies() { assert_eq!(cookies[0], second_cookie); assert_eq!(cookies[1], first_cookie); } + + srv.stop().await; } #[actix_rt::test] @@ -1018,6 +1074,8 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] @@ -1030,6 +1088,8 @@ async fn test_normalize() { let response = srv.get("/one/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await } // allow deprecated App::data @@ -1099,4 +1159,6 @@ async fn test_accept_encoding_no_match() { .unwrap(); assert_eq!(response.status().as_u16(), 406); + + srv.stop().await; } From e8a0e168636a439a2eb37323f4acb22be685116a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 15 Nov 2021 18:11:51 +0000 Subject: [PATCH 50/99] run tarpaulin on workspace --- .github/workflows/ci.yml | 2 +- CHANGES.md | 2 +- awc/CHANGES.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aff0b934..a8b21b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: if: github.ref == 'refs/heads/master' run: | cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --out Xml --verbose + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - name: Upload to Codecov if: github.ref == 'refs/heads/master' uses: codecov/codecov-action@v1 diff --git a/CHANGES.md b/CHANGES.md index 4ecbd0c2..0cb5ccb2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] -* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] +* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6b9531c7..98998fd5 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -4,6 +4,7 @@ ## 3.0.0-beta.10 - 2021-11-15 +* No significant changes from `3.0.0-beta.9`. ## 3.0.0-beta.9 - 2021-10-20 From 4df1cd78b7534cba98aaa930e3a6aaca2ca36ea3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 09:21:10 +0000 Subject: [PATCH 51/99] simplify `AnyBody` and `BodySize` (#2446) --- actix-http/CHANGES.md | 11 +++++++++++ actix-http/src/body/body.rs | 24 +++++++++++------------- actix-http/src/body/message_body.rs | 2 +- actix-http/src/body/mod.rs | 14 +++++++------- actix-http/src/body/size.rs | 12 ++++-------- actix-http/src/encoding/encoder.rs | 3 +-- actix-http/src/h1/dispatcher.rs | 8 ++++---- actix-http/src/h1/encoder.rs | 15 ++++++--------- actix-http/src/h2/dispatcher.rs | 4 +++- actix-http/src/response.rs | 2 +- actix-http/src/response_builder.rs | 4 ++-- awc/src/client/h1proto.rs | 4 ++-- awc/src/client/h2proto.rs | 18 +++++++++--------- awc/src/middleware/redirect.rs | 2 +- awc/src/sender.rs | 2 +- src/response/builder.rs | 4 ++-- src/response/http_codes.rs | 3 +-- 17 files changed, 67 insertions(+), 65 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9ec75b4b..2beda3dc 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* `AnyBody::empty` for quickly creating an empty body. [#2446] + +### Changed +* Rename `AnyBody::{Message => Stream}`. [#2446] + +### Removed +* `AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +* `BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] + +[#2446]: https://github.com/actix/actix-web/pull/2446 ## 3.0.0-beta.12 - 2021-11-15 diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index cd3e4c5c..32b46448 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -20,17 +20,19 @@ pub enum AnyBody { /// Empty response. `Content-Length` header is not set. None, - /// Zero sized response body. `Content-Length` header is set to `0`. - Empty, - /// Specific response body. Bytes(Bytes), /// Generic message body. - Message(BoxAnyBody), + Stream(BoxAnyBody), } impl AnyBody { + /// Constructs a new, empty body. + pub fn empty() -> Self { + Self::Bytes(Bytes::new()) + } + /// Create body from slice (copy) pub fn from_slice(s: &[u8]) -> Self { Self::Bytes(Bytes::copy_from_slice(s)) @@ -42,7 +44,7 @@ impl AnyBody { B: MessageBody + 'static, B::Error: Into>, { - Self::Message(BoxAnyBody::from_body(body)) + Self::Stream(BoxAnyBody::from_body(body)) } } @@ -52,9 +54,8 @@ impl MessageBody for AnyBody { fn size(&self) -> BodySize { match self { AnyBody::None => BodySize::None, - AnyBody::Empty => BodySize::Empty, AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Message(ref body) => body.size(), + AnyBody::Stream(ref body) => body.size(), } } @@ -64,7 +65,6 @@ impl MessageBody for AnyBody { ) -> Poll>> { match self.get_mut() { AnyBody::None => Poll::Ready(None), - AnyBody::Empty => Poll::Ready(None), AnyBody::Bytes(ref mut bin) => { let len = bin.len(); if len == 0 { @@ -74,7 +74,7 @@ impl MessageBody for AnyBody { } } - AnyBody::Message(body) => body + AnyBody::Stream(body) => body .as_pin_mut() .poll_next(cx) .map_err(|err| Error::new_body().with_cause(err)), @@ -86,12 +86,11 @@ impl PartialEq for AnyBody { fn eq(&self, other: &Body) -> bool { match *self { AnyBody::None => matches!(*other, AnyBody::None), - AnyBody::Empty => matches!(*other, AnyBody::Empty), AnyBody::Bytes(ref b) => match *other { AnyBody::Bytes(ref b2) => b == b2, _ => false, }, - AnyBody::Message(_) => false, + AnyBody::Stream(_) => false, } } } @@ -100,9 +99,8 @@ impl fmt::Debug for AnyBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Empty => write!(f, "AnyBody::Empty"), AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b), - AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"), + AnyBody::Stream(_) => write!(f, "AnyBody::Message(_)"), } } } diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index edb4c550..62a7e9b1 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -31,7 +31,7 @@ impl MessageBody for () { type Error = Infallible; fn size(&self) -> BodySize { - BodySize::Empty + BodySize::Sized(0) } fn poll_next( diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index a60a8895..0d5b0f07 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -33,7 +33,7 @@ pub use self::sized_stream::SizedStream; /// use bytes::Bytes; /// /// # async fn test_to_bytes() { -/// let body = Body::Empty; +/// let body = Body::None; /// let bytes = to_bytes(body).await.unwrap(); /// assert!(bytes.is_empty()); /// @@ -44,8 +44,9 @@ pub use self::sized_stream::SizedStream; /// ``` pub async fn to_bytes(body: B) -> Result { let cap = match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()), + BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), BodySize::Sized(size) => size as usize, + // good enough first guess for chunk size BodySize::Stream => 32_768, }; @@ -184,7 +185,7 @@ mod tests { #[actix_rt::test] async fn test_unit() { - assert_eq!(().size(), BodySize::Empty); + assert_eq!(().size(), BodySize::Sized(0)); assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) .await .is_none()); @@ -194,11 +195,11 @@ mod tests { async fn test_box_and_pin() { let val = Box::new(()); pin!(val); - assert_eq!(val.size(), BodySize::Empty); + assert_eq!(val.size(), BodySize::Sized(0)); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); let mut val = Box::pin(()); - assert_eq!(val.size(), BodySize::Empty); + assert_eq!(val.size(), BodySize::Sized(0)); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); } @@ -214,7 +215,6 @@ mod tests { #[actix_rt::test] async fn test_body_debug() { assert!(format!("{:?}", Body::None).contains("Body::None")); - assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); } @@ -252,7 +252,7 @@ mod tests { #[actix_rt::test] async fn test_to_bytes() { - let body = Body::Empty; + let body = Body::empty(); let bytes = to_bytes(body).await.unwrap(); assert!(bytes.is_empty()); diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs index 775d5b8f..e238eada 100644 --- a/actix-http/src/body/size.rs +++ b/actix-http/src/body/size.rs @@ -6,14 +6,9 @@ pub enum BodySize { /// Will skip writing Content-Length header. None, - /// Zero size body. - /// - /// Will write `Content-Length: 0` header. - Empty, - /// Known size body. /// - /// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`. + /// Will write `Content-Length: N` header. Sized(u64), /// Unknown size body. @@ -25,16 +20,17 @@ pub enum BodySize { impl BodySize { /// Returns true if size hint indicates no or empty body. /// + /// Streams will return false because it cannot be known without reading the stream. + /// /// ``` /// # use actix_http::body::BodySize; /// assert!(BodySize::None.is_eof()); - /// assert!(BodySize::Empty.is_eof()); /// assert!(BodySize::Sized(0).is_eof()); /// /// assert!(!BodySize::Sized(64).is_eof()); /// assert!(!BodySize::Stream.is_eof()); /// ``` pub fn is_eof(&self) -> bool { - matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) + matches!(self, BodySize::None | BodySize::Sized(0)) } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index abd8cedb..6cb034b7 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -61,7 +61,6 @@ impl Encoder { let body = match body { ResponseBody::Other(b) => match b { Body::None => return ResponseBody::Other(Body::None), - Body::Empty => return ResponseBody::Other(Body::Empty), Body::Bytes(buf) => { if can_encode { EncoderBody::Bytes(buf) @@ -69,7 +68,7 @@ impl Encoder { return ResponseBody::Other(Body::Bytes(buf)); } } - Body::Message(stream) => EncoderBody::BoxedStream(stream), + Body::Stream(stream) => EncoderBody::BoxedStream(stream), }, ResponseBody::Body(stream) => EncoderBody::Stream(stream), }; diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 69530ed1..844bc61e 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -325,7 +325,7 @@ where ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { - BodySize::None | BodySize::Empty => State::None, + BodySize::None | BodySize::Sized(0) => State::None, _ => State::SendPayload(body), }; self.project().state.set(state); @@ -339,7 +339,7 @@ where ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { - BodySize::None | BodySize::Empty => State::None, + BodySize::None | BodySize::Sized(0) => State::None, _ => State::SendErrorPayload(body), }; self.project().state.set(state); @@ -380,7 +380,7 @@ where // send_response would update InnerDispatcher state to SendPayload or // None(If response body is empty). // continue loop to poll it. - self.as_mut().send_error_response(res, AnyBody::Empty)?; + self.as_mut().send_error_response(res, AnyBody::empty())?; } // return with upgrade request and poll it exclusively. @@ -772,7 +772,7 @@ where trace!("Slow request timeout"); let _ = self.as_mut().send_error_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - AnyBody::Empty, + AnyBody::empty(), ); this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index ead14206..e07c3295 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -93,13 +93,10 @@ pub(crate) trait MessageType: Sized { dst.put_slice(b"\r\n"); } } - BodySize::Empty => { - if camel_case { - dst.put_slice(b"\r\nContent-Length: 0\r\n"); - } else { - dst.put_slice(b"\r\ncontent-length: 0\r\n"); - } + BodySize::Sized(0) if camel_case => { + dst.put_slice(b"\r\nContent-Length: 0\r\n") } + BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"), BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::None => dst.put_slice(b"\r\n"), } @@ -336,7 +333,7 @@ impl MessageEncoder { // transfer encoding if !head { self.te = match length { - BodySize::Empty => TransferEncoding::empty(), + BodySize::Sized(0) => TransferEncoding::empty(), BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Stream => { if message.chunked() && !stream { @@ -553,7 +550,7 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); @@ -624,7 +621,7 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 7326dfff..8b922b2c 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -285,9 +285,11 @@ fn prepare_response( let _ = match size { BodySize::None | BodySize::Stream => None, - BodySize::Empty => res + + BodySize::Sized(0) => res .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 2aa38c15..47f1c37e 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -28,7 +28,7 @@ impl Response { pub fn new(status: StatusCode) -> Self { Response { head: BoxedResponseHead::new(status), - body: AnyBody::Empty, + body: AnyBody::empty(), } } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index e46d9a28..a1cb1a42 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -270,7 +270,7 @@ impl ResponseBuilder { /// This `ResponseBuilder` will be left in a useless state. #[inline] pub fn finish(&mut self) -> Response { - self.body(AnyBody::Empty) + self.body(AnyBody::empty()) } /// Create an owned `ResponseBuilder`, leaving the original in a useless state. @@ -390,7 +390,7 @@ mod tests { fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(Body::Empty); + .body(Body::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 3c2bb7cc..7f3ba1b6 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -70,7 +70,7 @@ where // RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1 let is_expect = if head.as_ref().headers.contains_key(EXPECT) { match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => { + BodySize::None | BodySize::Sized(0) => { let keep_alive = framed.codec_ref().keepalive(); framed.io_mut().on_release(keep_alive); @@ -104,7 +104,7 @@ where if do_send { // send request body match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} + BodySize::None | BodySize::Sized(0) => {} _ => send_body(body, pin_framed.as_mut()).await?, }; diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index feb2dbd0..2618e190 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -36,10 +36,7 @@ where let head_req = head.as_ref().method == Method::HEAD; let length = body.size(); - let eof = matches!( - length, - BodySize::None | BodySize::Empty | BodySize::Sized(0) - ); + let eof = matches!(length, BodySize::None | BodySize::Sized(0)); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); @@ -52,13 +49,11 @@ where // Content length let _ = match length { BodySize::None => None, - BodySize::Stream => { - skip_len = false; - None - } - BodySize::Empty => req + + BodySize::Sized(0) => req .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); @@ -67,6 +62,11 @@ where HeaderValue::from_str(buf.format(len)).unwrap(), ) } + + BodySize::Stream => { + skip_len = false; + None + } }; // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 8a79a659..f01136d1 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -194,7 +194,7 @@ where match body { Some(ref bytes) => Body::Bytes(bytes.clone()), // TODO: should this be Body::Empty or Body::None. - _ => Body::Empty, + _ => Body::empty(), } } else { body = None; diff --git a/awc/src/sender.rs b/awc/src/sender.rs index c0639606..fcd0c71a 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -297,7 +297,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, Body::Empty) + self.send_body(addr, response_decompress, timeout, config, Body::empty()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> diff --git a/src/response/builder.rs b/src/response/builder.rs index 56d30d9d..f6099a01 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -387,7 +387,7 @@ impl HttpResponseBuilder { /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { - self.body(AnyBody::Empty) + self.body(AnyBody::empty()) } /// This method construct new `HttpResponseBuilder` @@ -475,7 +475,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(Body::Empty); + .body(Body::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/http_codes.rs b/src/response/http_codes.rs index d67ef3f9..44ddb78f 100644 --- a/src/response/http_codes.rs +++ b/src/response/http_codes.rs @@ -87,13 +87,12 @@ impl HttpResponse { #[cfg(test)] mod tests { - use crate::dev::Body; use crate::http::StatusCode; use crate::HttpResponse; #[test] fn test_build() { - let resp = HttpResponse::Ok().body(Body::Empty); + let resp = HttpResponse::Ok().finish(); assert_eq!(resp.status(), StatusCode::OK); } } From 13cf5a9e445b31a6b418bc3fb47555b1e45aa5c3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 16:55:45 +0000 Subject: [PATCH 52/99] remove chunked encoding header for websockets --- actix-http/src/ws/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 7df924cf..70e0e62a 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -210,7 +210,6 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { Response::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") - .insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header(( header::SEC_WEBSOCKET_ACCEPT, // key is known to be header value safe ascii From d8cbb879dde831f5b6083664660e90ab1ecc584d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 21:41:35 +0000 Subject: [PATCH 53/99] make `AnyBody` generic on `Body` type (#2448) --- CHANGES.md | 10 ++ actix-http/CHANGES.md | 17 ++- actix-http/Cargo.toml | 1 + actix-http/src/body/body.rs | 174 ++++++++++++++++++++------- actix-http/src/body/body_stream.rs | 36 ++++++ actix-http/src/body/mod.rs | 8 +- actix-http/src/body/response_body.rs | 84 ------------- actix-http/src/body/sized_stream.rs | 45 +++++++ actix-http/src/encoding/encoder.rs | 39 +++--- actix-http/src/h1/dispatcher.rs | 2 +- actix-http/src/response_builder.rs | 2 +- awc/src/sender.rs | 4 +- src/dev.rs | 2 +- src/middleware/compat.rs | 4 +- src/middleware/compress.rs | 31 +++-- src/responder.rs | 27 +---- src/response/builder.rs | 4 +- src/response/response.rs | 3 + 18 files changed, 283 insertions(+), 210 deletions(-) delete mode 100644 actix-http/src/body/response_body.rs diff --git a/CHANGES.md b/CHANGES.md index 0cb5ccb2..784500d9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,16 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Compress middleware's response type is now `AnyBody>`. [#2448] + +### Fixed +* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] + +### Removed +* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] + +[#2423]: https://github.com/actix/actix-web/pull/2423 ## 4.0.0-beta.11 - 2021-11-15 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 2beda3dc..71cdd6d4 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,14 +2,23 @@ ## Unreleased - 2021-xx-xx ### Added -* `AnyBody::empty` for quickly creating an empty body. [#2446] +* `body::AnyBody::empty` for quickly creating an empty body. [#2446] +* `impl Clone` for `body::AnyBody where S: Clone`. [#2448] +* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] ### Changed -* Rename `AnyBody::{Message => Stream}`. [#2446] +* Rename `body::AnyBody::{Message => Body}`. [#2446] +* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] +* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] +* Rename `body::{BoxAnyBody => BoxBody}`. [#2448] +* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448] +* `Encoder::response` now returns `AnyBody>`. [#2448] ### Removed -* `AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] -* `BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +* `EncoderError::Boxed`; it is no longer required. [#2446] +* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 78431244..27a14737 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -92,6 +92,7 @@ regex = "1.3" rustls-pemfile = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } tokio = { version = "1.2", features = ["net", "rt"] } diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 32b46448..d51173a5 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -8,6 +8,7 @@ use std::{ use bytes::{Bytes, BytesMut}; use futures_core::Stream; +use pin_project::pin_project; use crate::error::Error; @@ -16,15 +17,17 @@ use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; pub type Body = AnyBody; /// Represents various types of HTTP message body. -pub enum AnyBody { +#[pin_project(project = AnyBodyProj)] +#[derive(Clone)] +pub enum AnyBody { /// Empty response. `Content-Length` header is not set. None, - /// Specific response body. + /// Complete, in-memory response body. Bytes(Bytes), - /// Generic message body. - Stream(BoxAnyBody), + /// Generic / Other message body. + Body(#[pin] B), } impl AnyBody { @@ -33,29 +36,60 @@ impl AnyBody { Self::Bytes(Bytes::new()) } - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } - - /// Create body from generic message body. - pub fn from_message(body: B) -> Self + /// Create boxed body from generic message body. + pub fn new_boxed(body: B) -> Self where B: MessageBody + 'static, B::Error: Into>, { - Self::Stream(BoxAnyBody::from_body(body)) + Self::Body(BoxBody::from_body(body)) + } + + /// Constructs new `AnyBody` instance from a slice of bytes by copying it. + /// + /// If your bytes container is owned, it may be cheaper to use a `From` impl. + pub fn copy_from_slice(s: &[u8]) -> Self { + Self::Bytes(Bytes::copy_from_slice(s)) + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] + pub fn from_slice(s: &[u8]) -> Self { + Self::Bytes(Bytes::copy_from_slice(s)) } } -impl MessageBody for AnyBody { +impl AnyBody +where + B: MessageBody + 'static, + B::Error: Into>, +{ + /// Create body from generic message body. + pub fn new(body: B) -> Self { + Self::Body(body) + } + + pub fn into_boxed(self) -> AnyBody { + match self { + Self::None => AnyBody::None, + Self::Bytes(bytes) => AnyBody::Bytes(bytes), + Self::Body(body) => AnyBody::new_boxed(body), + } + } +} + +impl MessageBody for AnyBody +where + B: MessageBody, + B::Error: Into> + 'static, +{ type Error = Error; fn size(&self) -> BodySize { match self { AnyBody::None => BodySize::None, AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Stream(ref body) => body.size(), + AnyBody::Body(ref body) => body.size(), } } @@ -63,9 +97,9 @@ impl MessageBody for AnyBody { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - match self.get_mut() { - AnyBody::None => Poll::Ready(None), - AnyBody::Bytes(ref mut bin) => { + match self.project() { + AnyBodyProj::None => Poll::Ready(None), + AnyBodyProj::Bytes(bin) => { let len = bin.len(); if len == 0 { Poll::Ready(None) @@ -74,8 +108,7 @@ impl MessageBody for AnyBody { } } - AnyBody::Stream(body) => body - .as_pin_mut() + AnyBodyProj::Body(body) => body .poll_next(cx) .map_err(|err| Error::new_body().with_cause(err)), } @@ -90,30 +123,30 @@ impl PartialEq for AnyBody { AnyBody::Bytes(ref b2) => b == b2, _ => false, }, - AnyBody::Stream(_) => false, + AnyBody::Body(_) => false, } } } -impl fmt::Debug for AnyBody { +impl fmt::Debug for AnyBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b), - AnyBody::Stream(_) => write!(f, "AnyBody::Message(_)"), + AnyBody::Bytes(ref bytes) => write!(f, "AnyBody::Bytes({:?})", bytes), + AnyBody::Body(ref stream) => write!(f, "AnyBody::Message({:?})", stream), } } } impl From<&'static str> for AnyBody { - fn from(s: &'static str) -> Body { - AnyBody::Bytes(Bytes::from_static(s.as_ref())) + fn from(string: &'static str) -> Body { + AnyBody::Bytes(Bytes::from_static(string.as_ref())) } } impl From<&'static [u8]> for AnyBody { - fn from(s: &'static [u8]) -> Body { - AnyBody::Bytes(Bytes::from_static(s)) + fn from(bytes: &'static [u8]) -> Body { + AnyBody::Bytes(Bytes::from_static(bytes)) } } @@ -124,20 +157,20 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(s: String) -> Body { - s.into_bytes().into() + fn from(string: String) -> Body { + string.into_bytes().into() } } impl From<&'_ String> for AnyBody { - fn from(s: &String) -> Body { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) + fn from(string: &String) -> Body { + AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } impl From> for AnyBody { - fn from(s: Cow<'_, str>) -> Body { - match s { + fn from(string: Cow<'_, str>) -> Body { + match string { Cow::Owned(s) => AnyBody::from(s), Cow::Borrowed(s) => { AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) @@ -147,14 +180,14 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(s: Bytes) -> Body { - AnyBody::Bytes(s) + fn from(bytes: Bytes) -> Body { + AnyBody::Bytes(bytes) } } impl From for AnyBody { - fn from(s: BytesMut) -> Body { - AnyBody::Bytes(s.freeze()) + fn from(bytes: BytesMut) -> Body { + AnyBody::Bytes(bytes.freeze()) } } @@ -163,8 +196,8 @@ where S: Stream> + 'static, E: Into> + 'static, { - fn from(s: SizedStream) -> Body { - AnyBody::from_message(s) + fn from(stream: SizedStream) -> Body { + AnyBody::new_boxed(stream) } } @@ -173,15 +206,15 @@ where S: Stream> + 'static, E: Into> + 'static, { - fn from(s: BodyStream) -> Body { - AnyBody::from_message(s) + fn from(stream: BodyStream) -> Body { + AnyBody::new_boxed(stream) } } /// A boxed message body with boxed errors. -pub struct BoxAnyBody(Pin>>>); +pub struct BoxBody(Pin>>>); -impl BoxAnyBody { +impl BoxBody { /// Boxes a `MessageBody` and any errors it generates. pub fn from_body(body: B) -> Self where @@ -195,18 +228,18 @@ impl BoxAnyBody { /// Returns a mutable pinned reference to the inner message body type. pub fn as_pin_mut( &mut self, - ) -> Pin<&mut (dyn MessageBody>)> { + ) -> Pin<&mut (dyn MessageBody>)> { self.0.as_mut() } } -impl fmt::Debug for BoxAnyBody { +impl fmt::Debug for BoxBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("BoxAnyBody(dyn MessageBody)") } } -impl MessageBody for BoxAnyBody { +impl MessageBody for BoxBody { type Error = Error; fn size(&self) -> BodySize { @@ -223,3 +256,52 @@ impl MessageBody for BoxAnyBody { .map_err(|err| Error::new_body().with_cause(err)) } } + +#[cfg(test)] +mod tests { + use std::marker::PhantomPinned; + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + struct PinType(PhantomPinned); + + impl MessageBody for PinType { + type Error = crate::Error; + + fn size(&self) -> BodySize { + unimplemented!() + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + unimplemented!() + } + } + + assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(AnyBody: MessageBody); + + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + + #[actix_rt::test] + async fn nested_boxed_body() { + let body = AnyBody::copy_from_slice(&[1, 2, 3]); + let boxed_body = BoxBody::from_body(BoxBody::from_body(body)); + + assert_eq!( + to_bytes(boxed_body).await.unwrap(), + Bytes::from(vec![1, 2, 3]), + ); + } +} diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index f726f447..31de9b48 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -75,10 +75,22 @@ mod tests { use derive_more::{Display, Error}; use futures_core::ready; use futures_util::{stream, FutureExt as _}; + use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; use crate::body::to_bytes; + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + + assert_not_impl_all!(BodyStream>: MessageBody); + assert_not_impl_all!(BodyStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(BodyStream>>: MessageBody); + #[actix_rt::test] async fn skips_empty_chunks() { let body = BodyStream::new(stream::iter( @@ -124,6 +136,30 @@ mod tests { assert!(matches!(to_bytes(body).await, Err(StreamErr))); } + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { + Err(Box::::from("stringy error")) + })); + + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } + #[actix_rt::test] async fn stream_delayed_error() { let body = diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 0d5b0f07..07e5e67c 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -11,15 +11,13 @@ use futures_core::ready; mod body; mod body_stream; mod message_body; -mod response_body; mod size; mod sized_stream; -pub use self::body::{AnyBody, Body, BoxAnyBody}; +pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; pub use self::message_body::MessageBody; pub(crate) use self::message_body::MessageBodyMapErr; -pub use self::response_body::ResponseBody; pub use self::size::BodySize; pub use self::sized_stream::SizedStream; @@ -108,10 +106,10 @@ mod tests { assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - Body::from_slice(b"test".as_ref()).size(), + Body::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); - assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(Body::copy_from_slice(b"test".as_ref()).get_ref(), b"test"); let sb = Bytes::from(&b"test"[..]); pin!(sb); diff --git a/actix-http/src/body/response_body.rs b/actix-http/src/body/response_body.rs deleted file mode 100644 index 699ea938..00000000 --- a/actix-http/src/body/response_body.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{ - mem, - pin::Pin, - task::{Context, Poll}, -}; - -use bytes::Bytes; -use futures_core::Stream; -use pin_project::pin_project; - -use crate::error::Error; - -use super::{Body, BodySize, MessageBody}; - -#[pin_project(project = ResponseBodyProj)] -pub enum ResponseBody { - Body(#[pin] B), - Other(Body), -} - -impl ResponseBody { - pub fn into_body(self) -> ResponseBody { - match self { - ResponseBody::Body(b) => ResponseBody::Other(b), - ResponseBody::Other(b) => ResponseBody::Other(b), - } - } -} - -impl ResponseBody { - pub fn take_body(&mut self) -> ResponseBody { - mem::replace(self, ResponseBody::Other(Body::None)) - } -} - -impl ResponseBody { - pub fn as_ref(&self) -> Option<&B> { - if let ResponseBody::Body(ref b) = self { - Some(b) - } else { - None - } - } -} - -impl MessageBody for ResponseBody -where - B: MessageBody, - B::Error: Into, -{ - type Error = Error; - - fn size(&self) -> BodySize { - match self { - ResponseBody::Body(ref body) => body.size(), - ResponseBody::Other(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Stream::poll_next(self, cx) - } -} - -impl Stream for ResponseBody -where - B: MessageBody, - B::Error: Into, -{ - type Item = Result; - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.project() { - ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into), - ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), - } - } -} diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index b6ceb32f..b92de44c 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -72,10 +72,22 @@ mod tests { use actix_rt::pin; use actix_utils::future::poll_fn; use futures_util::stream; + use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; use crate::body::to_bytes; + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + + assert_not_impl_all!(SizedStream>: MessageBody); + assert_not_impl_all!(SizedStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(SizedStream>>: MessageBody); + #[actix_rt::test] async fn skips_empty_chunks() { let body = SizedStream::new( @@ -119,4 +131,37 @@ mod tests { assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); } + + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new(0, stream::once(async { Err("stringy error") })); + assert_eq!(to_bytes(body).await, Ok(Bytes::new())); + + let body = SizedStream::new(1, stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new( + 0, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!(to_bytes(body).await.unwrap(), Bytes::new()); + + let body = SizedStream::new( + 1, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 6cb034b7..62100ff1 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -24,7 +24,7 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use zstd::stream::write::Encoder as ZstdEncoder; use crate::{ - body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody}, + body::{AnyBody, BodySize, MessageBody}, http::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, @@ -50,8 +50,8 @@ impl Encoder { pub fn response( encoding: ContentEncoding, head: &mut ResponseHead, - body: ResponseBody, - ) -> ResponseBody> { + body: AnyBody, + ) -> AnyBody> { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT @@ -59,18 +59,15 @@ impl Encoder { || encoding == ContentEncoding::Auto); let body = match body { - ResponseBody::Other(b) => match b { - Body::None => return ResponseBody::Other(Body::None), - Body::Bytes(buf) => { - if can_encode { - EncoderBody::Bytes(buf) - } else { - return ResponseBody::Other(Body::Bytes(buf)); - } + AnyBody::None => return AnyBody::None, + AnyBody::Bytes(buf) => { + if can_encode { + EncoderBody::Bytes(buf) + } else { + return AnyBody::Bytes(buf); } - Body::Stream(stream) => EncoderBody::BoxedStream(stream), - }, - ResponseBody::Body(stream) => EncoderBody::Stream(stream), + } + AnyBody::Body(body) => EncoderBody::Stream(body), }; if can_encode { @@ -78,7 +75,8 @@ impl Encoder { if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); head.no_chunking(false); - return ResponseBody::Body(Encoder { + + return AnyBody::Body(Encoder { body, eof: false, fut: None, @@ -87,7 +85,7 @@ impl Encoder { } } - ResponseBody::Body(Encoder { + AnyBody::Body(Encoder { body, eof: false, fut: None, @@ -100,7 +98,6 @@ impl Encoder { enum EncoderBody { Bytes(Bytes), Stream(#[pin] B), - BoxedStream(BoxAnyBody), } impl MessageBody for EncoderBody @@ -113,7 +110,6 @@ where match self { EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Stream(ref b) => b.size(), - EncoderBody::BoxedStream(ref b) => b.size(), } } @@ -130,9 +126,6 @@ where } } EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), - EncoderBodyProj::BoxedStream(ref mut b) => { - b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed) - } } } } @@ -348,9 +341,6 @@ pub enum EncoderError { #[display(fmt = "body")] Body(E), - #[display(fmt = "boxed")] - Boxed(Box), - #[display(fmt = "blocking")] Blocking(BlockingError), @@ -362,7 +352,6 @@ impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { EncoderError::Body(err) => Some(err), - EncoderError::Boxed(err) => Some(&**err), EncoderError::Blocking(err) => Some(err), EncoderError::Io(err) => Some(err), } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 844bc61e..163d84f5 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1077,7 +1077,7 @@ mod tests { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( - Response::ok().set_body(AnyBody::from_slice(path)), + Response::ok().set_body(AnyBody::copy_from_slice(path)), )) }) } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index a1cb1a42..e934f94d 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -262,7 +262,7 @@ impl ResponseBuilder { S: Stream> + 'static, E: Into> + 'static, { - self.body(AnyBody::from_message(BodyStream::new(stream))) + self.body(AnyBody::new_boxed(BodyStream::new(stream))) } /// Generate response with an empty body. diff --git a/awc/src/sender.rs b/awc/src/sender.rs index fcd0c71a..02870aea 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -9,7 +9,7 @@ use std::{ }; use actix_http::{ - body::{Body, BodyStream}, + body::{AnyBody, Body, BodyStream}, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, @@ -286,7 +286,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::from_message(BodyStream::new(stream)), + AnyBody::new_boxed(BodyStream::new(stream)), ) } diff --git a/src/dev.rs b/src/dev.rs index 4fac207a..27c206e7 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -14,7 +14,7 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream}; +pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 0a6256fe..752e90f9 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -7,7 +7,7 @@ use std::{ task::{Context, Poll}, }; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::{AnyBody, MessageBody}; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; @@ -124,7 +124,7 @@ where B::Error: Into>, { fn map_body(self) -> ServiceResponse { - self.map_body(|_, body| Body::from_message(body)) + self.map_body(|_, body| AnyBody::new_boxed(body)) } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 4854f4be..3e85cb84 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -10,13 +10,14 @@ use std::{ }; use actix_http::{ - body::{MessageBody, ResponseBody}, + body::{AnyBody, MessageBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, StatusCode, }; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; +use bytes::Bytes; use futures_core::ready; use once_cell::sync::Lazy; use pin_project::pin_project; @@ -61,7 +62,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); @@ -110,7 +111,7 @@ where S: Service, Error = Error>, B: MessageBody, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Future = Either, Ready>>; @@ -142,15 +143,19 @@ where // There is an HTTP header but we cannot match what client as asked for Some(Err(_)) => { - let res = HttpResponse::with_body( - StatusCode::NOT_ACCEPTABLE, - SUPPORTED_ALGORITHM_NAMES.as_str(), - ); - let enc = ContentEncoding::Identity; + let res = HttpResponse::new(StatusCode::NOT_ACCEPTABLE); - Either::right(ok(req.into_response(res.map_body(move |head, body| { - Encoder::response(enc, head, ResponseBody::Other(body.into())) - })))) + let res: HttpResponse>> = res.map_body(move |head, _| { + let body_bytes = Bytes::from(SUPPORTED_ALGORITHM_NAMES.as_bytes()); + + Encoder::response( + ContentEncoding::Identity, + head, + AnyBody::Bytes(body_bytes), + ) + }); + + Either::right(ok(req.into_response(res))) } } } @@ -172,7 +177,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Output = Result>>, Error>; + type Output = Result>>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -186,7 +191,7 @@ where }; Poll::Ready(Ok(resp.map_body(move |head, body| { - Encoder::response(enc, head, ResponseBody::Body(body)) + Encoder::response(enc, head, AnyBody::Body(body)) }))) } Err(e) => Poll::Ready(Err(e)), diff --git a/src/responder.rs b/src/responder.rs index 005bff03..4d2e97c3 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -232,7 +232,7 @@ pub(crate) mod tests { use bytes::{Bytes, BytesMut}; use super::*; - use crate::dev::{Body, ResponseBody}; + use crate::dev::AnyBody; use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; use crate::test::{init_service, TestRequest}; use crate::{error, web, App}; @@ -264,13 +264,13 @@ pub(crate) mod tests { pub(crate) trait BodyTest { fn bin_ref(&self) -> &[u8]; - fn body(&self) -> &Body; + fn body(&self) -> &AnyBody; } impl BodyTest for Body { fn bin_ref(&self) -> &[u8] { match self { - Body::Bytes(ref bin) => bin, + AnyBody::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } @@ -279,27 +279,6 @@ pub(crate) mod tests { } } - impl BodyTest for ResponseBody { - fn bin_ref(&self) -> &[u8] { - match self { - ResponseBody::Body(ref b) => match b { - Body::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - }, - ResponseBody::Other(ref b) => match b { - Body::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - }, - } - } - fn body(&self) -> &Body { - match self { - ResponseBody::Body(ref b) => b, - ResponseBody::Other(ref b) => b, - } - } - } - #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); diff --git a/src/response/builder.rs b/src/response/builder.rs index f6099a01..e42d85f5 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -354,10 +354,10 @@ impl HttpResponseBuilder { #[inline] pub fn streaming(&mut self, stream: S) -> HttpResponse where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into> + 'static, { - self.body(AnyBody::from_message(BodyStream::new(stream))) + self.body(AnyBody::new_boxed(BodyStream::new(stream))) } /// Set a json body and generate `Response` diff --git a/src/response/response.rs b/src/response/response.rs index 09515c83..46360e53 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -227,6 +227,9 @@ impl HttpResponse { } } + // TODO: into_body equivalent + // TODO: into_boxed_body + /// Extract response body pub fn into_body(self) -> B { self.res.into_body() From 668a33c793a41fd8cedb7170f4930fa543f25d74 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 22:10:30 +0000 Subject: [PATCH 54/99] remove internal usage of Body --- actix-http/examples/echo2.rs | 4 +- actix-http/src/body/body.rs | 43 +++++++++---- actix-http/src/body/mod.rs | 68 +++++++++++--------- actix-http/src/error.rs | 25 +++---- actix-http/src/header/shared/quality_item.rs | 1 + actix-http/src/response_builder.rs | 6 +- actix-http/tests/test_openssl.rs | 4 +- actix-http/tests/test_rustls.rs | 6 +- actix-http/tests/test_server.rs | 4 +- awc/src/connect.rs | 4 +- awc/src/frozen.rs | 6 +- awc/src/middleware/redirect.rs | 12 ++-- awc/src/request.rs | 4 +- awc/src/sender.rs | 10 +-- src/app.rs | 4 +- src/dev.rs | 1 + src/error/internal.rs | 4 +- src/responder.rs | 10 +-- src/response/builder.rs | 4 +- src/response/response.rs | 12 ++-- src/scope.rs | 8 +-- src/test.rs | 6 +- tests/test_server.rs | 2 +- 23 files changed, 137 insertions(+), 111 deletions(-) diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index db195d65..6e5ddec7 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,12 +1,12 @@ use std::io; -use actix_http::{body::Body, http::HeaderValue, http::StatusCode}; +use actix_http::{body::AnyBody, http::HeaderValue, http::StatusCode}; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; -async fn handle_request(mut req: Request) -> Result, Error> { +async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); while let Some(item) = req.payload().next().await { body.extend_from_slice(&item?) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index d51173a5..1d88777b 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -14,6 +14,7 @@ use crate::error::Error; use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; +#[deprecated(since = "4.0.0", note = "Renamed to `AnyBody`.")] pub type Body = AnyBody; /// Represents various types of HTTP message body. @@ -116,7 +117,7 @@ where } impl PartialEq for AnyBody { - fn eq(&self, other: &Body) -> bool { + fn eq(&self, other: &AnyBody) -> bool { match *self { AnyBody::None => matches!(*other, AnyBody::None), AnyBody::Bytes(ref b) => match *other { @@ -139,37 +140,37 @@ impl fmt::Debug for AnyBody { } impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Body { + fn from(string: &'static str) -> AnyBody { AnyBody::Bytes(Bytes::from_static(string.as_ref())) } } impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Body { + fn from(bytes: &'static [u8]) -> AnyBody { AnyBody::Bytes(Bytes::from_static(bytes)) } } impl From> for AnyBody { - fn from(vec: Vec) -> Body { + fn from(vec: Vec) -> AnyBody { AnyBody::Bytes(Bytes::from(vec)) } } impl From for AnyBody { - fn from(string: String) -> Body { + fn from(string: String) -> AnyBody { string.into_bytes().into() } } impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Body { + fn from(string: &String) -> AnyBody { AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Body { + fn from(string: Cow<'_, str>) -> AnyBody { match string { Cow::Owned(s) => AnyBody::from(s), Cow::Borrowed(s) => { @@ -180,33 +181,53 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(bytes: Bytes) -> Body { + fn from(bytes: Bytes) -> Self { AnyBody::Bytes(bytes) } } impl From for AnyBody { - fn from(bytes: BytesMut) -> Body { + fn from(bytes: BytesMut) -> Self { AnyBody::Bytes(bytes.freeze()) } } +impl From> for AnyBody> +where + S: Stream> + 'static, + E: Into> + 'static, +{ + fn from(stream: SizedStream) -> Self { + AnyBody::new(stream) + } +} + impl From> for AnyBody where S: Stream> + 'static, E: Into> + 'static, { - fn from(stream: SizedStream) -> Body { + fn from(stream: SizedStream) -> Self { AnyBody::new_boxed(stream) } } +impl From> for AnyBody> +where + S: Stream> + 'static, + E: Into> + 'static, +{ + fn from(stream: BodyStream) -> Self { + AnyBody::new(stream) + } +} + impl From> for AnyBody where S: Stream> + 'static, E: Into> + 'static, { - fn from(stream: BodyStream) -> Body { + fn from(stream: BodyStream) -> Self { AnyBody::new_boxed(stream) } } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 07e5e67c..83299a47 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -14,6 +14,7 @@ mod message_body; mod size; mod sized_stream; +#[allow(deprecated)] pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; pub use self::message_body::MessageBody; @@ -76,10 +77,10 @@ mod tests { use super::*; - impl Body { + impl AnyBody { pub(crate) fn get_ref(&self) -> &[u8] { match *self { - Body::Bytes(ref bin) => bin, + AnyBody::Bytes(ref bin) => bin, _ => panic!(), } } @@ -87,9 +88,9 @@ mod tests { #[actix_rt::test] async fn test_static_str() { - assert_eq!(Body::from("").size(), BodySize::Sized(0)); - assert_eq!(Body::from("test").size(), BodySize::Sized(4)); - assert_eq!(Body::from("test").get_ref(), b"test"); + assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); + assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from("test").get_ref(), b"test"); assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( @@ -103,13 +104,16 @@ mod tests { #[actix_rt::test] async fn test_static_bytes() { - assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - Body::copy_from_slice(b"test".as_ref()).size(), + AnyBody::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); - assert_eq!(Body::copy_from_slice(b"test".as_ref()).get_ref(), b"test"); + assert_eq!( + AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), + b"test" + ); let sb = Bytes::from(&b"test"[..]); pin!(sb); @@ -122,8 +126,8 @@ mod tests { #[actix_rt::test] async fn test_vec() { - assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); + assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); let test_vec = Vec::from("test"); pin!(test_vec); @@ -140,8 +144,8 @@ mod tests { #[actix_rt::test] async fn test_bytes() { let b = Bytes::from("test"); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -154,8 +158,8 @@ mod tests { #[actix_rt::test] async fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -168,10 +172,10 @@ mod tests { #[actix_rt::test] async fn test_string() { let b = "test".to_owned(); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); - assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); - assert_eq!(Body::from(&b).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(&b).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -204,29 +208,33 @@ mod tests { #[actix_rt::test] async fn test_body_eq() { assert!( - Body::Bytes(Bytes::from_static(b"1")) - == Body::Bytes(Bytes::from_static(b"1")) + AnyBody::Bytes(Bytes::from_static(b"1")) + == AnyBody::Bytes(Bytes::from_static(b"1")) ); - assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None); + assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None); } #[actix_rt::test] async fn test_body_debug() { - assert!(format!("{:?}", Body::None).contains("Body::None")); - assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); + assert!(format!("{:?}", AnyBody::::None).contains("Body::None")); + assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] async fn test_serde_json() { use serde_json::{json, Value}; assert_eq!( - Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap()) - .size(), + AnyBody::from( + serde_json::to_vec(&Value::String("test".to_owned())).unwrap() + ) + .size(), BodySize::Sized(6) ); assert_eq!( - Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap()) - .size(), + AnyBody::from( + serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() + ) + .size(), BodySize::Sized(25) ); } @@ -250,11 +258,11 @@ mod tests { #[actix_rt::test] async fn test_to_bytes() { - let body = Body::empty(); + let body = AnyBody::empty(); let bytes = to_bytes(body).await.unwrap(); assert!(bytes.is_empty()); - let body = Body::Bytes(Bytes::from_static(b"123")); + let body = AnyBody::copy_from_slice(b"123"); let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123"[..]); } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index f7d7f696..c7c0cce0 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err use derive_more::{Display, Error, From}; use http::{uri::InvalidUri, StatusCode}; -use crate::{ - body::{AnyBody, Body}, - ws, Response, -}; +use crate::{body::AnyBody, ws, Response}; pub use http::Error as HttpError; @@ -29,6 +26,11 @@ impl Error { } } + pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { + self.inner.cause = Some(cause.into()); + self + } + pub(crate) fn new_http() -> Self { Self::new(Kind::Http) } @@ -49,14 +51,12 @@ impl Error { Self::new(Kind::SendResponse) } - // TODO: remove allow - #[allow(dead_code)] + #[allow(unused)] // reserved for future use (TODO: remove allow when being used) pub(crate) fn new_io() -> Self { Self::new(Kind::Io) } - // used in encoder behind feature flag so ignore unused warning - #[allow(unused)] + #[allow(unused)] // used in encoder behind feature flag so ignore unused warning pub(crate) fn new_encoder() -> Self { Self::new(Kind::Encoder) } @@ -64,11 +64,6 @@ impl Error { pub(crate) fn new_ws() -> Self { Self::new(Kind::Ws) } - - pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { - self.inner.cause = Some(cause.into()); - self - } } impl From for Response { @@ -78,12 +73,12 @@ impl From for Response { _ => StatusCode::INTERNAL_SERVER_ERROR, }; - Response::new(status_code).set_body(Body::from(err.to_string())) + Response::new(status_code).set_body(AnyBody::from(err.to_string())) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] -pub enum Kind { +pub(crate) enum Kind { #[display(fmt = "error processing HTTP")] Http, diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 63fa02e7..431e9fb3 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -195,6 +195,7 @@ mod tests { use super::*; // copy of encoding from actix-web headers + #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt #[derive(Clone, PartialEq, Debug)] pub enum Encoding { Chunked, diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index e934f94d..c5fcb625 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -357,7 +357,7 @@ impl fmt::Debug for ResponseBuilder { #[cfg(test)] mod tests { use super::*; - use crate::body::Body; + use crate::body::AnyBody; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; #[test] @@ -390,13 +390,13 @@ mod tests { fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(Body::empty()); + .body(AnyBody::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } #[test] fn test_into_builder() { - let mut resp: Response = "test".into(); + let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); resp.headers_mut().insert( diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 0eaaabcc..e7dd7817 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -5,7 +5,7 @@ extern crate tls_openssl as openssl; use std::{convert::Infallible, io}; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderValue}, @@ -409,7 +409,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .openssl(tls_config()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index a9f6e99f..320c9ad9 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, @@ -477,7 +477,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; @@ -494,7 +494,7 @@ async fn test_h2_service_error() { async fn test_h1_service_error() { let mut srv = test_server(move || { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index ea78ce11..2dca09e2 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -6,7 +6,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, }; @@ -724,7 +724,7 @@ impl From for Response { async fn test_h1_service_error() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .tcp() }) .await; diff --git a/awc/src/connect.rs b/awc/src/connect.rs index f27a8c36..05f2a649 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -8,7 +8,7 @@ use std::{ use actix_codec::Framed; use actix_http::{ - body::Body, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, + body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, }; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; @@ -30,7 +30,7 @@ pub type BoxConnectorService = Rc< pub type BoxedSocket = Box; pub enum ConnectRequest { - Client(RequestHeadType, Body, Option), + Client(RequestHeadType, AnyBody, Option), Tunnel(RequestHead, Option), } diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index cb8c0f1b..46a00b00 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -5,7 +5,7 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::Body, + body::AnyBody, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, RequestHead, }; @@ -45,7 +45,7 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: Into, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, @@ -158,7 +158,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index f01136d1..12a71f7c 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::Body, + body::AnyBody, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -95,7 +95,7 @@ where }; let body_opt = match body { - Body::Bytes(ref b) => Some(b.clone()), + AnyBody::Bytes(ref b) => Some(b.clone()), _ => None, }; @@ -192,14 +192,14 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => Body::Bytes(bytes.clone()), - // TODO: should this be Body::Empty or Body::None. - _ => Body::empty(), + Some(ref bytes) => AnyBody::Bytes(bytes.clone()), + // TODO: should this be AnyBody::Empty or AnyBody::None. + _ => AnyBody::empty(), } } else { body = None; // remove body - Body::None + AnyBody::None }; let mut headers = headers.take().unwrap(); diff --git a/awc/src/request.rs b/awc/src/request.rs index 812c7631..bc3859e2 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -5,7 +5,7 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::Body, + body::AnyBody, http::{ header::{self, IntoHeaderPair}, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, @@ -350,7 +350,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { let slf = match self.prep_for_sending() { Ok(slf) => slf, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 02870aea..7e1bcd64 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -9,7 +9,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, BodyStream}, + body::{AnyBody, BodyStream}, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, @@ -196,7 +196,7 @@ impl RequestSender { body: B, ) -> SendClientRequest where - B: Into, + B: Into, { let req = match self { RequestSender::Owned(head) => { @@ -236,7 +236,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes(Bytes::from(body)), ) } @@ -265,7 +265,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes(Bytes::from(body)), ) } @@ -297,7 +297,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, Body::empty()) + self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> diff --git a/src/app.rs b/src/app.rs index da5b45f3..a291a959 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,7 @@ use std::future::Future; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::{AnyBody, MessageBody}; use actix_http::{Extensions, Request}; use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::{ @@ -39,7 +39,7 @@ pub struct App { _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { diff --git a/src/dev.rs b/src/dev.rs index 27c206e7..59805b82 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -14,6 +14,7 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; +#[allow(deprecated)] pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; #[cfg(feature = "__compress")] diff --git a/src/error/internal.rs b/src/error/internal.rs index 1d9ca904..3d99012d 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, fmt, io::Write as _}; -use actix_http::{body::Body, header, StatusCode}; +use actix_http::{body::AnyBody, header, StatusCode}; use bytes::{BufMut as _, BytesMut}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; @@ -88,7 +88,7 @@ where header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain; charset=utf-8"), ); - res.set_body(Body::from(buf.into_inner())) + res.set_body(AnyBody::from(buf.into_inner())) } InternalErrorType::Response(ref resp) => { diff --git a/src/responder.rs b/src/responder.rs index 4d2e97c3..8a84be59 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use actix_http::{ - body::Body, + body::AnyBody, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, }; use bytes::{Bytes, BytesMut}; @@ -65,7 +65,7 @@ impl Responder for HttpResponse { } } -impl Responder for actix_http::Response { +impl Responder for actix_http::Response { #[inline] fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from(self) @@ -254,7 +254,7 @@ pub(crate) mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"some")); } @@ -267,14 +267,14 @@ pub(crate) mod tests { fn body(&self) -> &AnyBody; } - impl BodyTest for Body { + impl BodyTest for AnyBody { fn bin_ref(&self) -> &[u8] { match self { AnyBody::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } - fn body(&self) -> &Body { + fn body(&self) -> &AnyBody { self } } diff --git a/src/response/builder.rs b/src/response/builder.rs index e42d85f5..e61f7e16 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -436,7 +436,7 @@ mod tests { use super::*; use crate::{ - dev::Body, + dev::AnyBody, http::{ header::{self, HeaderValue, CONTENT_TYPE}, StatusCode, @@ -475,7 +475,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(Body::empty()); + .body(AnyBody::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/response.rs b/src/response/response.rs index 46360e53..6475a381 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, MessageBody}, + body::{AnyBody, MessageBody}, http::{header::HeaderMap, StatusCode}, Extensions, Response, ResponseHead, }; @@ -273,14 +273,14 @@ impl From> for Response { } } -// Future is only implemented for Body payload type because it's the most useful for making simple -// handlers without async blocks. Making it generic over all MessageBody types requires a future -// impl on Response which would cause it's body field to be, undesirably, Option. +// Future is only implemented for AnyBody payload type because it's the most useful for making +// simple handlers without async blocks. Making it generic over all MessageBody types requires a +// future impl on Response which would cause it's body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. -impl Future for HttpResponse { - type Output = Result, Error>; +impl Future for HttpResponse { + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { if let Some(err) = self.error.take() { diff --git a/src/scope.rs b/src/scope.rs index 7d914f58..c20b5d7c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -580,7 +580,7 @@ mod tests { use bytes::Bytes; use crate::{ - dev::Body, + dev::AnyBody, guard, http::{header, HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, @@ -752,7 +752,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project1")); } @@ -853,7 +853,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project_1")); } @@ -881,7 +881,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); } diff --git a/src/test.rs b/src/test.rs index 43bf612c..77765e26 100644 --- a/src/test.rs +++ b/src/test.rs @@ -22,7 +22,7 @@ use crate::{ app_service::AppInitServiceState, config::AppConfig, data::Data, - dev::{Body, MessageBody, Payload}, + dev::{AnyBody, MessageBody, Payload}, http::header::ContentType, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, @@ -32,14 +32,14 @@ use crate::{ /// Create service that always responds with `HttpResponse::Ok()` and no body. pub fn ok_service( -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { default_service(StatusCode::OK) } /// Create service that always responds with given status code and no body. pub fn default_service( status_code: StatusCode, -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { (move |req: ServiceRequest| { ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) diff --git a/tests/test_server.rs b/tests/test_server.rs index d21dac8c..3f0fbfcc 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -200,7 +200,7 @@ async fn test_body_encoding_override() { .body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - let body = actix_web::dev::Body::Bytes(STR.into()); + let body = actix_web::dev::AnyBody::Bytes(STR.into()); let mut response = HttpResponse::with_body(actix_web::http::StatusCode::OK, body); From 0a135c7dc912191b11ed3350bf18ce82b6045483 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 22:41:24 +0000 Subject: [PATCH 55/99] bump actix-codec to 0.4.1 --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ca34c92..9a00db7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] __compress = [] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-macros = "0.2.3" actix-rt = "2.2" actix-server = "2.0.0-beta.9" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index b111f868..f118d162 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -30,7 +30,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 27a14737..a2e5e284 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -43,7 +43,7 @@ __compress = [] [dependencies] actix-service = "2.0.0" -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-utils = "3.0.0" actix-rt = "2.2" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 58c0d31a..bc660293 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -28,7 +28,7 @@ rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-http = "3.0.0-beta.12" actix-http-test = "3.0.0-beta.6" actix-service = "2.0.0" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 706a90c0..c20e508a 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-http = "3.0.0-beta.12" actix-web = { version = "4.0.0-beta.11", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index ce710d58..7bc99c08 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -53,7 +53,7 @@ trust-dns = ["trust-dns-resolver"] __compress = [] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-service = "2.0.0" actix-http = "3.0.0-beta.12" actix-rt = { version = "2.1", default-features = false } From 84c6d25fd32ea06febf8a84d148ab234fc835efb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 23:07:08 +0000 Subject: [PATCH 56/99] bump env logger dep --- Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a00db7b..23a5fc8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ awc = { version = "3.0.0-beta.10", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } -env_logger = "0.8" +env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false, features = ["std"] } rand = "0.8" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a2e5e284..682104cd 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -86,7 +86,7 @@ actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } -env_logger = "0.8" +env_logger = "0.9" rcgen = "0.8" regex = "1.3" rustls-pemfile = "0.2" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c20e508a..c938c6a1 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -30,5 +30,5 @@ actix-rt = "2.2" actix-test = "0.1.0-beta.6" awc = { version = "3.0.0-beta.10", default-features = false } -env_logger = "0.8" +env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 7bc99c08..048fe78d 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -97,7 +97,7 @@ actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } brotli2 = "0.3.2" -env_logger = "0.8" +env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" From 68a3acb9c290945eab82feeb87a98c47abcf0fac Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 23:22:29 +0000 Subject: [PATCH 57/99] bump zstd dep --- Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23a5fc8c..3f1f54fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ rcgen = "0.8" rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -zstd = "0.7" +zstd = "0.9" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 682104cd..c2de71e1 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -78,7 +78,7 @@ actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = tru # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } -zstd = { version = "0.7", optional = true } +zstd = { version = "0.9", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.9" From 168a7284d300b212f53fb40183d860792cabd5f1 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Wed, 17 Nov 2021 21:13:05 +0800 Subject: [PATCH 58/99] fix actix_http::Error conversion. (#2449) --- actix-http/src/body/body.rs | 46 ++++++++++++++++++------------------- actix-http/src/body/mod.rs | 45 +++++++++++++++++++----------------- actix-http/src/error.rs | 2 +- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 1d88777b..04fc957c 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -139,56 +139,56 @@ impl fmt::Debug for AnyBody { } } -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> AnyBody { - AnyBody::Bytes(Bytes::from_static(string.as_ref())) +impl From<&'static str> for AnyBody { + fn from(string: &'static str) -> Self { + Self::Bytes(Bytes::from_static(string.as_ref())) } } -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> AnyBody { - AnyBody::Bytes(Bytes::from_static(bytes)) +impl From<&'static [u8]> for AnyBody { + fn from(bytes: &'static [u8]) -> Self { + Self::Bytes(Bytes::from_static(bytes)) } } -impl From> for AnyBody { - fn from(vec: Vec) -> AnyBody { - AnyBody::Bytes(Bytes::from(vec)) +impl From> for AnyBody { + fn from(vec: Vec) -> Self { + Self::Bytes(Bytes::from(vec)) } } -impl From for AnyBody { - fn from(string: String) -> AnyBody { - string.into_bytes().into() +impl From for AnyBody { + fn from(string: String) -> Self { + Self::Bytes(Bytes::from(string)) } } -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> AnyBody { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) +impl From<&'_ String> for AnyBody { + fn from(string: &String) -> Self { + Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> AnyBody { +impl From> for AnyBody { + fn from(string: Cow<'_, str>) -> Self { match string { - Cow::Owned(s) => AnyBody::from(s), + Cow::Owned(s) => Self::from(s), Cow::Borrowed(s) => { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) + Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) } } } } -impl From for AnyBody { +impl From for AnyBody { fn from(bytes: Bytes) -> Self { - AnyBody::Bytes(bytes) + Self::Bytes(bytes) } } -impl From for AnyBody { +impl From for AnyBody { fn from(bytes: BytesMut) -> Self { - AnyBody::Bytes(bytes.freeze()) + Self::Bytes(bytes.freeze()) } } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 83299a47..724e2059 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -86,11 +86,14 @@ mod tests { } } + /// AnyBody alias because rustc does not (can not?) infer the default type parameter. + type TestBody = AnyBody; + #[actix_rt::test] async fn test_static_str() { - assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); - assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from("test").get_ref(), b"test"); + assert_eq!(TestBody::from("").size(), BodySize::Sized(0)); + assert_eq!(TestBody::from("test").size(), BodySize::Sized(4)); + assert_eq!(TestBody::from("test").get_ref(), b"test"); assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( @@ -104,14 +107,14 @@ mod tests { #[actix_rt::test] async fn test_static_bytes() { - assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(TestBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).size(), + TestBody::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), + TestBody::copy_from_slice(b"test".as_ref()).get_ref(), b"test" ); let sb = Bytes::from(&b"test"[..]); @@ -126,8 +129,8 @@ mod tests { #[actix_rt::test] async fn test_vec() { - assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); + assert_eq!(TestBody::from(Vec::from("test")).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(Vec::from("test")).get_ref(), b"test"); let test_vec = Vec::from("test"); pin!(test_vec); @@ -144,8 +147,8 @@ mod tests { #[actix_rt::test] async fn test_bytes() { let b = Bytes::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -158,8 +161,8 @@ mod tests { #[actix_rt::test] async fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -172,10 +175,10 @@ mod tests { #[actix_rt::test] async fn test_string() { let b = "test".to_owned(); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(&b).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(&b).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(&b).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -216,22 +219,22 @@ mod tests { #[actix_rt::test] async fn test_body_debug() { - assert!(format!("{:?}", AnyBody::::None).contains("Body::None")); - assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); + assert!(format!("{:?}", TestBody::None).contains("Body::None")); + assert!(format!("{:?}", TestBody::from(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] async fn test_serde_json() { use serde_json::{json, Value}; assert_eq!( - AnyBody::from( + TestBody::from( serde_json::to_vec(&Value::String("test".to_owned())).unwrap() ) .size(), BodySize::Sized(6) ); assert_eq!( - AnyBody::from( + TestBody::from( serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() ) .size(), diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index c7c0cce0..970c0c56 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -66,7 +66,7 @@ impl Error { } } -impl From for Response { +impl From for Response> { fn from(err: Error) -> Self { let status_code = match err.inner.kind { Kind::Parse => StatusCode::BAD_REQUEST, From 1fe309bcc6b0a472f1d92b3dc51e36d2d1f9db4a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 17 Nov 2021 15:32:42 +0000 Subject: [PATCH 59/99] increase ci test timeout --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8b21b7f..8f586d8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: - name: tests uses: actions-rs/cargo@v1 - timeout-minutes: 40 + timeout-minutes: 60 with: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls @@ -174,5 +174,5 @@ jobs: - name: doc tests uses: actions-rs/cargo@v1 - timeout-minutes: 40 + timeout-minutes: 60 with: { command: ci-doctest } From e33618ed6d222ffbf7d095adaa53500e024204ea Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 17 Nov 2021 17:43:24 +0000 Subject: [PATCH 60/99] ensure content disposition header in multipart (#2451) Co-authored-by: Craig Pastro --- actix-multipart/CHANGES.md | 8 + actix-multipart/src/error.rs | 23 ++- actix-multipart/src/server.rs | 228 ++++++++++++++++++------- src/http/header/content_disposition.rs | 24 ++- 4 files changed, 213 insertions(+), 70 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 09cc707b..97c01139 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx +* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] +* Added `MultipartError::NoContentDisposition` variant. [#2451] +* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] +* Added `Field::name` method for getting the field name. [#2451] +* `MultipartError` now marks variants with inner errors as the source. [#2451] +* `MultipartError` is now marked as non-exhaustive. [#2451] + +[#2451]: https://github.com/actix/actix-web/pull/2451 ## 0.4.0-beta.7 - 2021-10-20 diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 5f91c60d..de4594b8 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -2,39 +2,52 @@ use actix_web::error::{ParseError, PayloadError}; use actix_web::http::StatusCode; use actix_web::ResponseError; -use derive_more::{Display, From}; +use derive_more::{Display, Error, From}; /// A set of errors that can occur during parsing multipart streams -#[derive(Debug, Display, From)] +#[non_exhaustive] +#[derive(Debug, Display, From, Error)] pub enum MultipartError { + /// Content-Disposition header is not found or is not equal to "form-data". + /// + /// According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a + /// Content-Disposition header must always be present and equal to "form-data". + #[display(fmt = "No Content-Disposition `form-data` header")] + NoContentDisposition, + /// Content-Type header is not found - #[display(fmt = "No Content-type header found")] + #[display(fmt = "No Content-Type header found")] NoContentType, + /// Can not parse Content-Type header #[display(fmt = "Can not parse Content-Type header")] ParseContentType, + /// Multipart boundary is not found #[display(fmt = "Multipart boundary is not found")] Boundary, + /// Nested multipart is not supported #[display(fmt = "Nested multipart is not supported")] Nested, + /// Multipart stream is incomplete #[display(fmt = "Multipart stream is incomplete")] Incomplete, + /// Error during field parsing #[display(fmt = "{}", _0)] Parse(ParseError), + /// Payload error #[display(fmt = "{}", _0)] Payload(PayloadError), + /// Not consumed #[display(fmt = "Multipart stream is not consumed")] NotConsumed, } -impl std::error::Error for MultipartError {} - /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index b7d25153..43f9ccf5 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1,15 +1,20 @@ //! Multipart response payload support. -use std::cell::{Cell, RefCell, RefMut}; -use std::convert::TryFrom; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, fmt}; +use std::{ + cell::{Cell, RefCell, RefMut}, + cmp, + convert::TryFrom, + fmt, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; -use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; +use actix_web::{ + error::{ParseError, PayloadError}, + http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, +}; use bytes::{Bytes, BytesMut}; use futures_core::stream::{LocalBoxStream, Stream}; use futures_util::stream::StreamExt as _; @@ -40,10 +45,13 @@ enum InnerMultipartItem { enum InnerState { /// Stream eof Eof, + /// Skip data until first boundary FirstBoundary, + /// Reading boundary Boundary, + /// Reading Headers, Headers, } @@ -332,31 +340,55 @@ impl InnerMultipart { return Poll::Pending; }; - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - mt = ct; - } - } - } + // According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a + // Content-Disposition header must always be present and set to "form-data". + + let content_disposition = headers + .get(&header::CONTENT_DISPOSITION) + .and_then(|cd| ContentDisposition::from_raw(cd).ok()) + .filter(|content_disposition| { + let is_form_data = + content_disposition.disposition == header::DispositionType::FormData; + + let has_field_name = content_disposition + .parameters + .iter() + .any(|param| matches!(param, header::DispositionParam::Name(_))); + + is_form_data && has_field_name + }); + + let cd = if let Some(content_disposition) = content_disposition { + content_disposition + } else { + return Poll::Ready(Some(Err(MultipartError::NoContentDisposition))); + }; + + let ct: mime::Mime = headers + .get(&header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| ct.parse().ok()) + .unwrap_or(mime::APPLICATION_OCTET_STREAM); self.state = InnerState::Boundary; - // nested multipart stream - if mt.type_() == mime::MULTIPART { - Poll::Ready(Some(Err(MultipartError::Nested))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field)))) + // nested multipart stream is not supported + if ct.type_() == mime::MULTIPART { + return Poll::Ready(Some(Err(MultipartError::Nested))); } + + let field = + InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?; + + self.item = InnerMultipartItem::Field(Rc::clone(&field)); + + Poll::Ready(Some(Ok(Field::new( + safety.clone(cx), + headers, + ct, + cd, + field, + )))) } } } @@ -371,6 +403,7 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { ct: mime::Mime, + cd: ContentDisposition, headers: HeaderMap, inner: Rc>, safety: Safety, @@ -381,35 +414,51 @@ impl Field { safety: Safety, headers: HeaderMap, ct: mime::Mime, + cd: ContentDisposition, inner: Rc>, ) -> Self { Field { ct, + cd, headers, inner, safety, } } - /// Get a map of headers + /// Returns a reference to the field's header map. pub fn headers(&self) -> &HeaderMap { &self.headers } - /// Get the content type of the field + /// Returns a reference to the field's content (mime) type. pub fn content_type(&self) -> &mime::Mime { &self.ct } - /// Get the content disposition of the field, if it exists - pub fn content_disposition(&self) -> Option { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } + /// Returns the field's Content-Disposition. + /// + /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the + /// disposition type is "form-data". The Content-Disposition header field MUST also contain an + /// additional parameter of "name"; the value of the "name" parameter is the original field name + /// from the form.' + /// + /// This crate validates that it exists before returning a `Field`. As such, it is safe to + /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as + /// a convenience. + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + pub fn content_disposition(&self) -> &ContentDisposition { + &self.cd + } + + /// Returns the field's name. + /// + /// See [content_disposition] regarding guarantees about + pub fn name(&self) -> &str { + self.content_disposition() + .get_name() + .expect("field name should be guaranteed to exist in multipart form-data") } } @@ -451,20 +500,23 @@ struct InnerField { } impl InnerField { + fn new_in_rc( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result>, PayloadError> { + Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) + } + fn new( payload: PayloadRef, boundary: String, headers: &HeaderMap, ) -> Result { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some(len) - } else { - return Err(PayloadError::Incomplete(None)); - } - } else { - return Err(PayloadError::Incomplete(None)); + match len.to_str().ok().and_then(|len| len.parse::().ok()) { + Some(len) => Some(len), + None => return Err(PayloadError::Incomplete(None)), } } else { None @@ -658,9 +710,8 @@ impl Clone for PayloadRef { } } -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. +/// Counter. It tracks of number of clones of payloads and give access to payload only to top most +/// task panics if Safety get destroyed and it not top most task. #[derive(Debug)] struct Safety { task: LocalWaker, @@ -707,11 +758,12 @@ impl Drop for Safety { if Rc::strong_count(&self.payload) != self.level { self.clean.set(true); } + self.task.wake(); } } -/// Payload buffer +/// Payload buffer. struct PayloadBuffer { eof: bool, buf: BytesMut, @@ -719,7 +771,7 @@ struct PayloadBuffer { } impl PayloadBuffer { - /// Create new `PayloadBuffer` instance + /// Constructs new `PayloadBuffer` instance. fn new(stream: S) -> Self where S: Stream> + 'static, @@ -767,7 +819,7 @@ impl PayloadBuffer { } /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { + fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { let res = twoway::find_bytes(&self.buf, line) .map(|idx| self.buf.split_to(idx + line.len()).freeze()); @@ -779,12 +831,12 @@ impl PayloadBuffer { } /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Result, MultipartError> { + fn readline(&mut self) -> Result, MultipartError> { self.read_until(b"\n") } /// Read bytes until new line delimiter or eof - pub fn readline_or_eof(&mut self) -> Result, MultipartError> { + fn readline_or_eof(&mut self) -> Result, MultipartError> { match self.readline() { Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), line => line, @@ -792,7 +844,7 @@ impl PayloadBuffer { } /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { + fn unprocessed(&mut self, data: Bytes) { let buf = BytesMut::from(data.as_ref()); let buf = std::mem::replace(&mut self.buf, buf); self.buf.extend_from_slice(&buf); @@ -914,6 +966,7 @@ mod tests { Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", @@ -965,7 +1018,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await { Some(Ok(mut field)) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1027,7 +1080,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await.unwrap() { Ok(mut field) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1182,4 +1235,59 @@ mod tests { _ => unreachable!(), } } + + #[actix_rt::test] + async fn no_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } + + #[actix_rt::test] + async fn no_name_in_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } } diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index fdd8a7da..6d07a41b 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -34,15 +34,18 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { /// The implied disposition of the content of the HTTP body. #[derive(Clone, Debug, PartialEq)] pub enum DispositionType { - /// Inline implies default processing + /// Inline implies default processing. Inline, + /// Attachment implies that the recipient should prompt the user to save the response locally, /// rather than process it normally (as per its media type). Attachment, - /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. + + /// Used in *multipart/form-data* as defined in [RFC7578](https://tools.ietf.org/html/rfc7578) + /// to carry the field name and optional filename. FormData, - /// Extension type. Should be handled by recipients the same way as Attachment + + /// Extension type. Should be handled by recipients the same way as Attachment. Ext(String), } @@ -76,6 +79,7 @@ pub enum DispositionParam { /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// the form. Name(String), + /// A plain file name. /// /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any @@ -83,14 +87,17 @@ pub enum DispositionParam { /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// in case there are Unicode characters in file names. Filename(String), + /// An extended file name. It must not exist for `ContentType::Formdata` according to /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). FilenameExt(ExtendedValue), + /// An unrecognized regular parameter as defined in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should /// ignore unrecognizable parameters. Unknown(String, String), + /// An unrecognized extended parameter as defined in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single @@ -205,7 +212,6 @@ impl DispositionParam { /// itself, *Content-Disposition* has no effect. /// /// # ABNF - /// ```text /// content-disposition = "Content-Disposition" ":" /// disposition-type *( ";" disposition-parm ) @@ -289,10 +295,12 @@ impl DispositionParam { /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). +// TODO: private fields and use smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type pub disposition: DispositionType, + /// Disposition parameters pub parameters: Vec, } @@ -509,22 +517,28 @@ impl fmt::Display for DispositionParam { // // // See also comments in test_from_raw_unnecessary_percent_decode. + static RE: Lazy = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); + match self { DispositionParam::Name(ref value) => write!(f, "name={}", value), + DispositionParam::Filename(ref value) => { write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) } + DispositionParam::Unknown(ref name, ref value) => write!( f, "{}=\"{}\"", name, &RE.replace_all(value, "\\$0").as_ref() ), + DispositionParam::FilenameExt(ref ext_value) => { write!(f, "filename*={}", ext_value) } + DispositionParam::UnknownExt(ref name, ref ext_value) => { write!(f, "{}*={}", name, ext_value) } From 66620a101211c424ffc7e2f1b1c7bb9e2a78ff87 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 17 Nov 2021 23:11:35 +0300 Subject: [PATCH 61/99] simplify handler.rs (#2450) --- src/handler.rs | 152 ++++++------------------------------------------- src/route.rs | 6 +- 2 files changed, 21 insertions(+), 137 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index bc91ce41..ddefe8d5 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,16 +1,13 @@ use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use actix_service::{Service, ServiceFactory}; -use actix_utils::future::{ready, Ready}; -use futures_core::ready; -use pin_project::pin_project; +use actix_service::{ + boxed::{self, BoxServiceFactory}, + fn_service, +}; use crate::{ service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; /// A request handler is an async function that accepts zero or more parameters that can be @@ -27,139 +24,26 @@ where fn call(&self, param: T) -> R; } -#[doc(hidden)] -/// Extract arguments from request, run factory function and make response. -pub struct HandlerService +pub fn handler_service( + handler: F, +) -> BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()> where F: Handler, T: FromRequest, R: Future, R::Output: Responder, { - hnd: F, - _phantom: PhantomData<(T, R)>, -} - -impl HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - pub fn new(hnd: F) -> Self { - Self { - hnd, - _phantom: PhantomData, + boxed::factory(fn_service(move |req: ServiceRequest| { + let handler = handler.clone(); + async move { + let (req, mut payload) = req.into_parts(); + let res = match T::from_request(&req, &mut payload).await { + Err(err) => HttpResponse::from_error(err), + Ok(data) => handler.call(data).await.respond_to(&req), + }; + Ok(ServiceResponse::new(req, res)) } - } -} - -impl Clone for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - fn clone(&self) -> Self { - Self { - hnd: self.hnd.clone(), - _phantom: PhantomData, - } - } -} - -impl ServiceFactory for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = Self; - type InitError = (); - type Future = Ready>; - - fn new_service(&self, _: ()) -> Self::Future { - ready(Ok(self.clone())) - } -} - -/// HandlerService is both it's ServiceFactory and Service Type. -impl Service for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = HandlerServiceFuture; - - actix_service::always_ready!(); - - fn call(&self, req: ServiceRequest) -> Self::Future { - let (req, mut payload) = req.into_parts(); - let fut = T::from_request(&req, &mut payload); - HandlerServiceFuture::Extract(fut, Some(req), self.hnd.clone()) - } -} - -#[doc(hidden)] -#[pin_project(project = HandlerProj)] -pub enum HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - Extract(#[pin] T::Future, Option, F), - Handle(#[pin] R, Option), -} - -impl Future for HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - // Error type in this future is a placeholder type. - // all instances of error must be converted to ServiceResponse and return in Ok. - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - loop { - match self.as_mut().project() { - HandlerProj::Extract(fut, req, handle) => { - match ready!(fut.poll(cx)) { - Ok(item) => { - let fut = handle.call(item); - let state = HandlerServiceFuture::Handle(fut, req.take()); - self.as_mut().set(state); - } - Err(err) => { - let req = req.take().unwrap(); - let res = HttpResponse::from_error(err.into()); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - }; - } - HandlerProj::Handle(fut, req) => { - let res = ready!(fut.poll(cx)); - let req = req.take().unwrap(); - let res = res.respond_to(&req); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - } - } - } + })) } /// FromRequest trait impl for tuples diff --git a/src/route.rs b/src/route.rs index d85b940b..0c069943 100644 --- a/src/route.rs +++ b/src/route.rs @@ -11,7 +11,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ guard::{self, Guard}, - handler::{Handler, HandlerService}, + handler::{handler_service, Handler}, service::{ServiceRequest, ServiceResponse}, Error, FromRequest, HttpResponse, Responder, }; @@ -30,7 +30,7 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: boxed::factory(HandlerService::new(HttpResponse::NotFound)), + service: handler_service(HttpResponse::NotFound), guards: Rc::new(Vec::new()), } } @@ -182,7 +182,7 @@ impl Route { R: Future + 'static, R::Output: Responder + 'static, { - self.service = boxed::factory(HandlerService::new(handler)); + self.service = handler_service(handler); self } From 56ee97f722292cfc7c9dabd6c9039b1d9cee218f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 18 Nov 2021 18:14:34 +0000 Subject: [PATCH 62/99] add files path traversal tests --- actix-files/src/path_buf.rs | 24 +++++++++++++++++++++++- actix-files/tests/traversal.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 actix-files/tests/traversal.rs diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 76f58930..0e0d4f51 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -8,7 +8,7 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest}; use crate::error::UriSegmentError; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct PathBufWrap(PathBuf); impl FromStr for PathBufWrap { @@ -21,6 +21,8 @@ impl FromStr for PathBufWrap { impl PathBufWrap { /// Parse a path, giving the choice of allowing hidden files to be considered valid segments. + /// + /// Path traversal is guarded by this method. pub fn parse_path(path: &str, hidden_files: bool) -> Result { let mut buf = PathBuf::new(); @@ -115,4 +117,24 @@ mod tests { PathBuf::from_iter(vec!["test", ".tt"]) ); } + + #[test] + fn path_traversal() { + assert_eq!( + PathBufWrap::parse_path("/../README.md", false).unwrap().0, + PathBuf::from_iter(vec!["README.md"]) + ); + + assert_eq!( + PathBufWrap::parse_path("/../README.md", true).unwrap().0, + PathBuf::from_iter(vec!["README.md"]) + ); + + assert_eq!( + PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false) + .unwrap() + .0, + PathBuf::from_iter(vec!["etc/passwd"]) + ); + } } diff --git a/actix-files/tests/traversal.rs b/actix-files/tests/traversal.rs new file mode 100644 index 00000000..c890b3fe --- /dev/null +++ b/actix-files/tests/traversal.rs @@ -0,0 +1,27 @@ +use actix_files::Files; +use actix_web::{ + http::StatusCode, + test::{self, TestRequest}, + App, +}; + +#[actix_rt::test] +async fn test_directory_traversal_prevention() { + let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; + + let req = + TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri( + "/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", + ) + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); +} From 194a69153751e5d28df78ec69444daa80e93f84c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 19 Nov 2021 14:04:12 +0000 Subject: [PATCH 63/99] files: 304 Not Modified responses omit Content-Length header (#2453) --- actix-files/CHANGES.md | 3 ++ actix-files/src/named.rs | 25 ++++++---- actix-http-test/src/lib.rs | 6 +-- actix-http/src/body/body.rs | 2 + actix-http/src/config.rs | 2 + actix-http/src/h1/client.rs | 2 +- actix-http/src/h1/codec.rs | 24 ++++----- actix-http/src/h1/encoder.rs | 21 ++++++-- actix-http/src/message.rs | 2 +- actix-http/tests/test_server.rs | 87 +++++++++++++++++++++++++++++++++ actix-test/src/lib.rs | 6 +-- 11 files changed, 145 insertions(+), 35 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index e1a2c90c..41336c21 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] + +[#2453]: https://github.com/actix/actix-web/pull/2453 ## 0.6.0-beta.8 - 2021-10-20 diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 241e78cf..dac54870 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -1,17 +1,22 @@ -use actix_service::{Service, ServiceFactory}; -use actix_utils::future::{ok, ready, Ready}; -use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef}; -use std::fs::{File, Metadata}; -use std::io; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + fs::{File, Metadata}, + io, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +use actix_http::body::AnyBody; +use actix_service::{Service, ServiceFactory}; +use actix_utils::future::{ok, ready, Ready}; use actix_web::{ - dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream}, + dev::{ + AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, + ServiceResponse, SizedStream, + }, http::{ header::{ self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, @@ -443,7 +448,7 @@ impl NamedFile { if precondition_failed { return resp.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).finish(); + return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None); } let reader = ChunkedReadFile::new(length, offset, self.file); diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index cda98cea..699bb266 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -273,12 +273,12 @@ impl TestServer { self.client.headers() } - /// Gracefully stop HTTP server. + /// Stop HTTP server. /// - /// Waits for spawned `Server` and `System` to shutdown gracefully. + /// Waits for spawned `Server` and `System` to (force) shutdown. pub async fn stop(&mut self) { // signal server to stop - self.server.stop(true).await; + self.server.stop(false).await; // also signal system to stop // though this is handled by `ServerBuilder::exit_system` too diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 04fc957c..c6439b55 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -32,6 +32,8 @@ pub enum AnyBody { } impl AnyBody { + // TODO: a None body constructor + /// Constructs a new, empty body. pub fn empty() -> Self { Self::Bytes(Bytes::new()) diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 069099b8..5d020edf 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -20,8 +20,10 @@ pub(crate) const DATE_VALUE_LENGTH: usize = 29; pub enum KeepAlive { /// Keep alive in seconds Timeout(usize), + /// Rely on OS to shutdown tcp connection Os, + /// Disabled Disabled, } diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 4a610468..bec16797 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -120,7 +120,7 @@ impl Decoder for ClientCodec { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - if let Some(ctype) = req.ctype() { + if let Some(ctype) = req.conn_type() { // do not use peer's keep-alive self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 634ca25e..29f6f417 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -29,7 +29,7 @@ pub struct Codec { decoder: decoder::MessageDecoder, payload: Option, version: Version, - ctype: ConnectionType, + conn_type: ConnectionType, // encoder part flags: Flags, @@ -65,7 +65,7 @@ impl Codec { decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, - ctype: ConnectionType::Close, + conn_type: ConnectionType::Close, encoder: encoder::MessageEncoder::default(), } } @@ -73,13 +73,13 @@ impl Codec { /// Check if request is upgrade. #[inline] pub fn upgrade(&self) -> bool { - self.ctype == ConnectionType::Upgrade + self.conn_type == ConnectionType::Upgrade } /// Check if last response is keep-alive. #[inline] pub fn keepalive(&self) -> bool { - self.ctype == ConnectionType::KeepAlive + self.conn_type == ConnectionType::KeepAlive } /// Check if keep-alive enabled on server level. @@ -124,11 +124,11 @@ impl Decoder for Codec { let head = req.head(); self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.version = head.version; - self.ctype = head.connection_type(); - if self.ctype == ConnectionType::KeepAlive + self.conn_type = head.connection_type(); + if self.conn_type == ConnectionType::KeepAlive && !self.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.ctype = ConnectionType::Close + self.conn_type = ConnectionType::Close } match payload { PayloadType::None => self.payload = None, @@ -159,14 +159,14 @@ impl Encoder, BodySize)>> for Codec { res.head_mut().version = self.version; // connection status - self.ctype = if let Some(ct) = res.head().ctype() { + self.conn_type = if let Some(ct) = res.head().conn_type() { if ct == ConnectionType::KeepAlive { - self.ctype + self.conn_type } else { ct } } else { - self.ctype + self.conn_type }; // encode message @@ -177,10 +177,9 @@ impl Encoder, BodySize)>> for Codec { self.flags.contains(Flags::STREAM), self.version, length, - self.ctype, + self.conn_type, &self.config, )?; - // self.headers_size = (dst.len() - len) as u32; } Message::Chunk(Some(bytes)) => { self.encoder.encode_chunk(bytes.as_ref(), dst)?; @@ -189,6 +188,7 @@ impl Encoder, BodySize)>> for Codec { self.encoder.encode_eof(dst)?; } } + Ok(()) } } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index e07c3295..cc26a200 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -56,7 +56,7 @@ pub(crate) trait MessageType: Sized { dst: &mut BytesMut, version: Version, mut length: BodySize, - ctype: ConnectionType, + conn_type: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { let chunked = self.chunked(); @@ -71,14 +71,23 @@ pub(crate) trait MessageType: Sized { | StatusCode::PROCESSING | StatusCode::NO_CONTENT => { // skip content-length and transfer-encoding headers - // See https://tools.ietf.org/html/rfc7230#section-3.3.1 + // see https://tools.ietf.org/html/rfc7230#section-3.3.1 // and https://tools.ietf.org/html/rfc7230#section-3.3.2 skip_len = true; length = BodySize::None } + + StatusCode::NOT_MODIFIED => { + // 304 responses should never have a body but should retain a manually set + // content-length header see https://tools.ietf.org/html/rfc7232#section-4.1 + skip_len = false; + length = BodySize::None; + } + _ => {} } } + match length { BodySize::Stream => { if chunked { @@ -102,7 +111,7 @@ pub(crate) trait MessageType: Sized { } // Connection - match ctype { + match conn_type { ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::KeepAlive if version < Version::HTTP_11 => { if camel_case { @@ -327,7 +336,7 @@ impl MessageEncoder { stream: bool, version: Version, length: BodySize, - ctype: ConnectionType, + conn_type: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { // transfer encoding @@ -349,7 +358,7 @@ impl MessageEncoder { } message.encode_status(dst)?; - message.encode_headers(dst, version, length, ctype, config) + message.encode_headers(dst, version, length, conn_type, config) } } @@ -363,10 +372,12 @@ pub(crate) struct TransferEncoding { enum TransferEncodingKind { /// An Encoder for when Transfer-Encoding includes `chunked`. Chunked(bool), + /// An Encoder for when Content-Length is set. /// /// Enforces that the body is not longer than the Content-Length header. Length(u64), + /// An Encoder for when Content-Length is not known. /// /// Application decides when to stop writing. diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 84125fb3..e0bed063 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -317,7 +317,7 @@ impl ResponseHead { } #[inline] - pub(crate) fn ctype(&self) -> Option { + pub(crate) fn conn_type(&self) -> Option { if self.flags.contains(Flags::CLOSE) { Some(ConnectionType::Close) } else if self.flags.contains(Flags::KEEP_ALIVE) { diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 2dca09e2..11bc8e93 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -759,3 +759,90 @@ async fn test_h1_on_connect() { srv.stop().await; } + +/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1. +/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 +#[actix_rt::test] +async fn test_not_modified_spec_h1() { + // TODO: this test needing a few seconds to complete reveals some weirdness with either the + // dispatcher or the client, though similar hangs occur on other tests in this file, only + // succeeding, it seems, because of the keepalive timer + + static CL: header::HeaderName = header::CONTENT_LENGTH; + + let mut srv = test_server(|| { + HttpService::build() + .h1(|req: Request| { + let res: Response = match req.path() { + // with no content-length + "/none" => { + Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None) + } + + // with no content-length + "/body" => Response::with_body( + StatusCode::NOT_MODIFIED, + AnyBody::from("1234"), + ), + + // with manual content-length header and specific None body + "/cl-none" => { + let mut res = + Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None); + res.headers_mut() + .insert(CL.clone(), header::HeaderValue::from_static("24")); + res + } + + // with manual content-length header and ignore-able body + "/cl-body" => { + let mut res = Response::with_body( + StatusCode::NOT_MODIFIED, + AnyBody::from("1234"), + ); + res.headers_mut() + .insert(CL.clone(), header::HeaderValue::from_static("4")); + res + } + + _ => panic!("unknown route"), + }; + + ok::<_, Infallible>(res) + }) + .tcp() + }) + .await; + + let res = srv.get("/none").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!(res.headers().get(&CL), None); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/body").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!(res.headers().get(&CL), None); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/cl-none").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!( + res.headers().get(&CL), + Some(&header::HeaderValue::from_static("24")), + ); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/cl-body").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!( + res.headers().get(&CL), + Some(&header::HeaderValue::from_static("4")), + ); + // server does not prevent payload from being sent but clients may choose not to read it + // TODO: this is probably a bug, especially since CL header can differ in length from the body + assert!(!srv.load_body(res).await.unwrap().is_empty()); + + // TODO: add stream response tests + + srv.stop().await; +} diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index cf5738aa..6c776a87 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -520,12 +520,12 @@ impl TestServer { self.client.headers() } - /// Gracefully stop HTTP server. + /// Stop HTTP server. /// - /// Waits for spawned `Server` and `System` to shutdown gracefully. + /// Waits for spawned `Server` and `System` to shutdown (force) shutdown. pub async fn stop(mut self) { // signal server to stop - self.server.stop(true).await; + self.server.stop(false).await; // also signal system to stop // though this is handled by `ServerBuilder::exit_system` too From dd347e0bd069f00c67992fdcf805a21a8c2274cc Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Mon, 22 Nov 2021 09:19:09 +0800 Subject: [PATCH 64/99] implement io-uring for actix-files (#2408) Co-authored-by: Rob Ede --- .cargo/config.toml | 22 ++- .github/workflows/ci.yml | 53 ++++--- Cargo.toml | 5 +- actix-files/CHANGES.md | 6 + actix-files/Cargo.toml | 7 +- actix-files/src/chunked.rs | 294 +++++++++++++++++++++++++++++------- actix-files/src/files.rs | 22 ++- actix-files/src/lib.rs | 105 +++++++------ actix-files/src/named.rs | 170 +++++++++++++++------ actix-files/src/path_buf.rs | 2 +- actix-files/src/service.rs | 220 +++++++++++++++------------ actix-http-test/CHANGES.md | 3 + actix-http-test/src/lib.rs | 31 ++-- actix-test/CHANGES.md | 3 + actix-test/src/lib.rs | 281 ++++++++++++++++++---------------- src/middleware/compress.rs | 3 +- 16 files changed, 794 insertions(+), 433 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 40a513ef..606c30de 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,14 +1,12 @@ [alias] -chk = "check --workspace --all-features --tests --examples --bins" -lint = "clippy --workspace --all-features --tests --examples --bins" -ci-min = "hack check --workspace --no-default-features" -ci-min-test = "hack check --workspace --no-default-features --tests --examples" -ci-default = "check --workspace --bins --tests --examples" -ci-full = "check --workspace --all-features --bins --tests --examples" -ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" -ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" +lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo" +lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo" -ci-feature-powerset-check-no-tls="hack --workspace --feature-powerset --skip=__compress,rustls,openssl check" -ci-feature-powerset-check-rustls="hack --workspace --feature-powerset --features=rustls --skip=__compress,openssl check" -ci-feature-powerset-check-openssl="hack --workspace --feature-powerset --features=openssl --skip=__compress,rustls check" -ci-feature-powerset-check-all="hack --workspace --feature-powerset --skip=__compress check" +# lib checking +ci-check-min = "hack --workspace check --no-default-features" +ci-check-default = "hack --workspace check" +ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check" +ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" + +# testing +ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f586d8d..38c066d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,26 +62,34 @@ jobs: - name: check minimal uses: actions-rs/cargo@v1 - with: { command: ci-min } - - - name: check minimal + tests - uses: actions-rs/cargo@v1 - with: { command: ci-min-test } + with: { command: ci-check-min } - name: check default uses: actions-rs/cargo@v1 - with: { command: ci-default } - - - name: check full - uses: actions-rs/cargo@v1 - with: { command: ci-full } + with: { command: ci-check-default } - name: tests - uses: actions-rs/cargo@v1 timeout-minutes: 60 - with: - command: ci-test - args: --skip=test_reading_deflate_encoding_large_random_rustls + run: | + cargo test --lib --tests -p=actix-router --all-features + cargo test --lib --tests -p=actix-http --all-features + cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls + cargo test --lib --tests -p=actix-web-codegen --all-features + cargo test --lib --tests -p=awc --all-features + cargo test --lib --tests -p=actix-http-test --all-features + cargo test --lib --tests -p=actix-test --all-features + cargo test --lib --tests -p=actix-files + cargo test --lib --tests -p=actix-multipart --all-features + cargo test --lib --tests -p=actix-web-actors --all-features + + - name: tests (io-uring) + if: matrix.target.os == 'ubuntu-latest' + timeout-minutes: 60 + run: > + sudo bash -c "ulimit -Sl 512 + && ulimit -Hl 512 + && PATH=$PATH:/usr/share/rust/.cargo/bin + && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" - name: Clear the cargo caches run: | @@ -114,9 +122,12 @@ jobs: args: cargo-hack - name: check feature combinations - # if: github.ref == 'refs/heads/master' uses: actions-rs/cargo@v1 - with: { command: ci-feature-powerset-check-all } + with: { command: ci-check-all-feature-powerset } + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset-linux } coverage: name: coverage @@ -166,11 +177,11 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack + # - name: Install cargo-hack + # uses: actions-rs/cargo@v1 + # with: + # command: install + # args: cargo-hack - name: doc tests uses: actions-rs/cargo@v1 diff --git a/Cargo.toml b/Cargo.toml index 3f1f54fc..537d1b5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,10 +65,13 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] +# io-uring feature only avaiable for Linux OSes. +experimental-io-uring = ["actix-server/io-uring"] + [dependencies] actix-codec = "0.4.1" actix-macros = "0.2.3" -actix-rt = "2.2" +actix-rt = "2.3" actix-server = "2.0.0-beta.9" actix-service = "2.0.0" actix-utils = "3.0.0" diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 41336c21..7da77560 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,8 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx +* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] +* Add `NamedFile::open_async`. [#2408] * Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] +* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408] +* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408] +* Add `impl Clone` for `FilesService`. [#2408] +[#2408]: https://github.com/actix/actix-web/pull/2408 [#2453]: https://github.com/actix/actix-web/pull/2453 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index bbb9f551..c0ff1867 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -14,11 +14,13 @@ edition = "2018" name = "actix_files" path = "src/lib.rs" +[features] +experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] + [dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false } actix-http = "3.0.0-beta.12" actix-service = "2.0.0" -actix-utils = "3.0.0" askama_escape = "0.10" bitflags = "1" @@ -30,6 +32,9 @@ log = "0.4" mime = "0.3" mime_guess = "2.0.1" percent-encoding = "2.1" +pin-project-lite = "0.2.7" + +tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index f639848c..fbb46e41 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -1,98 +1,278 @@ use std::{ cmp, fmt, - fs::File, future::Future, - io::{self, Read, Seek}, + io, pin::Pin, task::{Context, Poll}, }; -use actix_web::{ - error::{BlockingError, Error}, - rt::task::{spawn_blocking, JoinHandle}, -}; +use actix_web::error::Error; use bytes::Bytes; use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - state: ChunkedReadFileState, - counter: u64, -} +use super::named::File; -enum ChunkedReadFileState { - File(Option), - Future(JoinHandle>), -} - -impl ChunkedReadFile { - pub(crate) fn new(size: u64, offset: u64, file: File) -> Self { - Self { - size, - offset, - state: ChunkedReadFileState::File(Some(file)), - counter: 0, - } +pin_project! { + /// Adapter to read a `std::file::File` in chunks. + #[doc(hidden)] + pub struct ChunkedReadFile { + size: u64, + offset: u64, + #[pin] + state: ChunkedReadFileState, + counter: u64, + callback: F, } } -impl fmt::Debug for ChunkedReadFile { +#[cfg(not(feature = "experimental-io-uring"))] +pin_project! { + #[project = ChunkedReadFileStateProj] + #[project_replace = ChunkedReadFileStateProjReplace] + enum ChunkedReadFileState { + File { file: Option, }, + Future { #[pin] fut: Fut }, + } +} + +#[cfg(feature = "experimental-io-uring")] +pin_project! { + #[project = ChunkedReadFileStateProj] + #[project_replace = ChunkedReadFileStateProjReplace] + enum ChunkedReadFileState { + File { file: Option<(File, BytesMut)> }, + Future { #[pin] fut: Fut }, + } +} + +impl fmt::Debug for ChunkedReadFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("ChunkedReadFile") } } -impl Stream for ChunkedReadFile { +pub(crate) fn new_chunked_read( + size: u64, + offset: u64, + file: File, +) -> impl Stream> { + ChunkedReadFile { + size, + offset, + #[cfg(not(feature = "experimental-io-uring"))] + state: ChunkedReadFileState::File { file: Some(file) }, + #[cfg(feature = "experimental-io-uring")] + state: ChunkedReadFileState::File { + file: Some((file, BytesMut::new())), + }, + counter: 0, + callback: chunked_read_file_callback, + } +} + +#[cfg(not(feature = "experimental-io-uring"))] +async fn chunked_read_file_callback( + mut file: File, + offset: u64, + max_bytes: usize, +) -> Result<(File, Bytes), Error> { + use io::{Read as _, Seek as _}; + + let res = actix_web::rt::task::spawn_blocking(move || { + let mut buf = Vec::with_capacity(max_bytes); + + file.seek(io::SeekFrom::Start(offset))?; + + let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + Err(io::Error::from(io::ErrorKind::UnexpectedEof)) + } else { + Ok((file, Bytes::from(buf))) + } + }) + .await + .map_err(|_| actix_web::error::BlockingError)??; + + Ok(res) +} + +#[cfg(feature = "experimental-io-uring")] +async fn chunked_read_file_callback( + file: File, + offset: u64, + max_bytes: usize, + mut bytes_mut: BytesMut, +) -> io::Result<(File, Bytes, BytesMut)> { + bytes_mut.reserve(max_bytes); + + let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await; + let n_bytes = res?; + + if n_bytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + + let bytes = bytes_mut.split_to(n_bytes).freeze(); + + Ok((file, bytes, bytes_mut)) +} + +#[cfg(feature = "experimental-io-uring")] +impl Stream for ChunkedReadFile +where + F: Fn(File, u64, usize, BytesMut) -> Fut, + Fut: Future>, +{ type Item = Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.as_mut().get_mut(); - match this.state { - ChunkedReadFileState::File(ref mut file) => { - let size = this.size; - let offset = this.offset; - let counter = this.counter; + let mut this = self.as_mut().project(); + match this.state.as_mut().project() { + ChunkedReadFileStateProj::File { file } => { + let size = *this.size; + let offset = *this.offset; + let counter = *this.counter; if size == counter { Poll::Ready(None) } else { - let mut file = file + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let (file, bytes_mut) = file .take() .expect("ChunkedReadFile polled after completion"); - let fut = spawn_blocking(move || { - let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + let fut = (this.callback)(file, offset, max_bytes, bytes_mut); - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; + this.state + .project_replace(ChunkedReadFileState::Future { fut }); - let n_bytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - - if n_bytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - - Ok((file, Bytes::from(buf))) - }); - this.state = ChunkedReadFileState::Future(fut); self.poll_next(cx) } } - ChunkedReadFileState::Future(ref mut fut) => { - let (file, bytes) = - ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; - this.state = ChunkedReadFileState::File(Some(file)); + ChunkedReadFileStateProj::Future { fut } => { + let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?; - this.offset += bytes.len() as u64; - this.counter += bytes.len() as u64; + this.state.project_replace(ChunkedReadFileState::File { + file: Some((file, bytes_mut)), + }); + + *this.offset += bytes.len() as u64; + *this.counter += bytes.len() as u64; Poll::Ready(Some(Ok(bytes))) } } } } + +#[cfg(not(feature = "experimental-io-uring"))] +impl Stream for ChunkedReadFile +where + F: Fn(File, u64, usize) -> Fut, + Fut: Future>, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.as_mut().project(); + match this.state.as_mut().project() { + ChunkedReadFileStateProj::File { file } => { + let size = *this.size; + let offset = *this.offset; + let counter = *this.counter; + + if size == counter { + Poll::Ready(None) + } else { + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let file = file + .take() + .expect("ChunkedReadFile polled after completion"); + + let fut = (this.callback)(file, offset, max_bytes); + + this.state + .project_replace(ChunkedReadFileState::Future { fut }); + + self.poll_next(cx) + } + } + ChunkedReadFileStateProj::Future { fut } => { + let (file, bytes) = ready!(fut.poll(cx))?; + + this.state + .project_replace(ChunkedReadFileState::File { file: Some(file) }); + + *this.offset += bytes.len() as u64; + *this.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + } + } +} + +#[cfg(feature = "experimental-io-uring")] +use bytes_mut::BytesMut; + +// TODO: remove new type and use bytes::BytesMut directly +#[doc(hidden)] +#[cfg(feature = "experimental-io-uring")] +mod bytes_mut { + use std::ops::{Deref, DerefMut}; + + use tokio_uring::buf::{IoBuf, IoBufMut}; + + #[derive(Debug)] + pub struct BytesMut(bytes::BytesMut); + + impl BytesMut { + pub(super) fn new() -> Self { + Self(bytes::BytesMut::new()) + } + } + + impl Deref for BytesMut { + type Target = bytes::BytesMut; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for BytesMut { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + unsafe impl IoBuf for BytesMut { + fn stable_ptr(&self) -> *const u8 { + self.0.as_ptr() + } + + fn bytes_init(&self) -> usize { + self.0.len() + } + + fn bytes_total(&self) -> usize { + self.0.capacity() + } + } + + unsafe impl IoBufMut for BytesMut { + fn stable_mut_ptr(&mut self) -> *mut u8 { + self.0.as_mut_ptr() + } + + unsafe fn set_init(&mut self, init_len: usize) { + if self.len() < init_len { + self.0.set_len(init_len); + } + } + } +} diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 68879822..06909bf0 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -6,7 +6,6 @@ use std::{ }; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; -use actix_utils::future::ok; use actix_web::{ dev::{ AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, @@ -20,8 +19,9 @@ use actix_web::{ use futures_core::future::LocalBoxFuture; use crate::{ - directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, - MimeOverride, PathFilter, + directory_listing, named, + service::{FilesService, FilesServiceInner}, + Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter, }; /// Static files handling service. @@ -283,11 +283,17 @@ impl Files { /// Setting a fallback static file handler: /// ``` /// use actix_files::{Files, NamedFile}; + /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service}; /// /// # fn run() -> Result<(), actix_web::Error> { /// let files = Files::new("/", "./static") /// .index_file("index.html") - /// .default_handler(NamedFile::open("./static/404.html")?); + /// .default_handler(fn_service(|req: ServiceRequest| async { + /// let (req, _) = req.into_parts(); + /// let file = NamedFile::open_async("./static/404.html").await?; + /// let res = file.into_response(&req); + /// Ok(ServiceResponse::new(req, res)) + /// })); /// # Ok(()) /// # } /// ``` @@ -353,7 +359,7 @@ impl ServiceFactory for Files { type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - let mut srv = FilesService { + let mut inner = FilesServiceInner { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, @@ -372,14 +378,14 @@ impl ServiceFactory for Files { Box::pin(async { match fut.await { Ok(default) => { - srv.default = Some(default); - Ok(srv) + inner.default = Some(default); + Ok(FilesService(Rc::new(inner))) } Err(_) => Err(()), } }) } else { - Box::pin(ok(srv)) + Box::pin(async move { Ok(FilesService(Rc::new(inner))) }) } } } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 175c6eae..3af5282f 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -33,12 +33,12 @@ mod path_buf; mod range; mod service; -pub use crate::chunked::ChunkedReadFile; -pub use crate::directory::Directory; -pub use crate::files::Files; -pub use crate::named::NamedFile; -pub use crate::range::HttpRange; -pub use crate::service::FilesService; +pub use self::chunked::ChunkedReadFile; +pub use self::directory::Directory; +pub use self::files::Files; +pub use self::named::NamedFile; +pub use self::range::HttpRange; +pub use self::service::FilesService; use self::directory::{directory_listing, DirectoryRenderer}; use self::error::FilesError; @@ -62,13 +62,12 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; #[cfg(test)] mod tests { use std::{ - fs::{self, File}, + fs::{self}, ops::Add, time::{Duration, SystemTime}, }; use actix_service::ServiceFactory; - use actix_utils::future::ok; use actix_web::{ guard, http::{ @@ -82,6 +81,7 @@ mod tests { }; use super::*; + use crate::named::File; #[actix_web::test] async fn test_file_extension_to_mime() { @@ -100,7 +100,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_without_if_none_match() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() @@ -112,7 +112,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_without_if_none_match_same() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() @@ -124,7 +124,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_with_if_none_match() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() @@ -137,7 +137,7 @@ mod tests { #[actix_rt::test] async fn test_if_unmodified_since() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() @@ -149,7 +149,7 @@ mod tests { #[actix_rt::test] async fn test_if_unmodified_since_failed() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::UNIX_EPOCH); let req = TestRequest::default() @@ -161,8 +161,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_text() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml").unwrap(); + assert!(NamedFile::open_async("test--").await.is_err()); + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; @@ -185,8 +185,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_disposition() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml").unwrap(); + assert!(NamedFile::open_async("test--").await.is_err()); + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; @@ -202,7 +202,8 @@ mod tests { "inline; filename=\"Cargo.toml\"" ); - let file = NamedFile::open("Cargo.toml") + let file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .disable_content_disposition(); let req = TestRequest::default().to_http_request(); @@ -212,8 +213,19 @@ mod tests { #[actix_rt::test] async fn test_named_file_non_ascii_file_name() { - let mut file = - NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); + let file = { + #[cfg(feature = "experimental-io-uring")] + { + crate::named::File::open("Cargo.toml").await.unwrap() + } + + #[cfg(not(feature = "experimental-io-uring"))] + { + crate::named::File::open("Cargo.toml").unwrap() + } + }; + + let mut file = NamedFile::from_file(file, "貨物.toml").unwrap(); { file.file(); let _f: &File = &file; @@ -236,7 +248,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_set_content_type() { - let mut file = NamedFile::open("Cargo.toml") + let mut file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_type(mime::TEXT_XML); { @@ -261,7 +274,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_image() { - let mut file = NamedFile::open("tests/test.png").unwrap(); + let mut file = NamedFile::open_async("tests/test.png").await.unwrap(); { file.file(); let _f: &File = &file; @@ -284,7 +297,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_javascript() { - let file = NamedFile::open("tests/test.js").unwrap(); + let file = NamedFile::open_async("tests/test.js").await.unwrap(); let req = TestRequest::default().to_http_request(); let resp = file.respond_to(&req).await.unwrap(); @@ -304,7 +317,8 @@ mod tests { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], }; - let mut file = NamedFile::open("tests/test.png") + let mut file = NamedFile::open_async("tests/test.png") + .await .unwrap() .set_content_disposition(cd); { @@ -329,7 +343,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_binary() { - let mut file = NamedFile::open("tests/test.binary").unwrap(); + let mut file = NamedFile::open_async("tests/test.binary").await.unwrap(); { file.file(); let _f: &File = &file; @@ -352,7 +366,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_status_code_text() { - let mut file = NamedFile::open("Cargo.toml") + let mut file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_status_code(StatusCode::NOT_FOUND); { @@ -568,7 +583,8 @@ mod tests { async fn test_named_file_content_encoding() { let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { - NamedFile::open("Cargo.toml") + NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_encoding(header::ContentEncoding::Identity) }), @@ -588,7 +604,8 @@ mod tests { async fn test_named_file_content_encoding_gzip() { let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { - NamedFile::open("Cargo.toml") + NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_encoding(header::ContentEncoding::Gzip) }), @@ -614,7 +631,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_allowed_method() { let req = TestRequest::default().method(Method::GET).to_http_request(); - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let resp = file.respond_to(&req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } @@ -705,8 +722,8 @@ mod tests { #[actix_rt::test] async fn test_default_handler_file_missing() { let st = Files::new("/", ".") - .default_handler(|req: ServiceRequest| { - ok(req.into_response(HttpResponse::Ok().body("default content"))) + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .new_service(()) .await @@ -789,9 +806,8 @@ mod tests { #[actix_rt::test] async fn test_serve_named_file() { - let srv = - test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap())) - .await; + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = test::init_service(App::new().service(factory)).await; let req = TestRequest::get().uri("/Cargo.toml").to_request(); let res = test::call_service(&srv, req).await; @@ -808,11 +824,9 @@ mod tests { #[actix_rt::test] async fn test_serve_named_file_prefix() { - let srv = test::init_service( - App::new() - .service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())), - ) - .await; + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = + test::init_service(App::new().service(web::scope("/test").service(factory))).await; let req = TestRequest::get().uri("/test/Cargo.toml").to_request(); let res = test::call_service(&srv, req).await; @@ -829,10 +843,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_default_service() { - let srv = test::init_service( - App::new().default_service(NamedFile::open("Cargo.toml").unwrap()), - ) - .await; + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = test::init_service(App::new().default_service(factory)).await; for route in ["/foobar", "/baz", "/"].iter() { let req = TestRequest::get().uri(route).to_request(); @@ -847,8 +859,9 @@ mod tests { #[actix_rt::test] async fn test_default_handler_named_file() { + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); let st = Files::new("/", ".") - .default_handler(NamedFile::open("Cargo.toml").unwrap()) + .default_handler(factory) .new_service(()) .await .unwrap(); @@ -926,8 +939,8 @@ mod tests { #[actix_rt::test] async fn test_default_handler_filter() { let st = Files::new("/", ".") - .default_handler(|req: ServiceRequest| { - ok(req.into_response(HttpResponse::Ok().body("default content"))) + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .path_filter(|path, _| path.extension() == Some("png".as_ref())) .new_service(()) diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index dac54870..547048bb 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -1,5 +1,6 @@ use std::{ - fs::{File, Metadata}, + fmt, + fs::Metadata, io, ops::{Deref, DerefMut}, path::{Path, PathBuf}, @@ -11,7 +12,6 @@ use std::os::unix::fs::MetadataExt; use actix_http::body::AnyBody; use actix_service::{Service, ServiceFactory}; -use actix_utils::future::{ok, ready, Ready}; use actix_web::{ dev::{ AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, @@ -26,9 +26,9 @@ use actix_web::{ Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; use bitflags::bitflags; +use futures_core::future::LocalBoxFuture; use mime_guess::from_path; -use crate::ChunkedReadFile; use crate::{encoding::equiv_utf8_text, range::HttpRange}; bitflags! { @@ -53,9 +53,9 @@ impl Default for Flags { /// use actix_web::App; /// use actix_files::NamedFile; /// -/// # fn run() -> Result<(), Box> { -/// let app = App::new() -/// .service(NamedFile::open("./static/index.html")?); +/// # async fn run() -> Result<(), Box> { +/// let file = NamedFile::open_async("./static/index.html").await?; +/// let app = App::new().service(file); /// # Ok(()) /// # } /// ``` @@ -67,10 +67,9 @@ impl Default for Flags { /// /// #[get("/")] /// async fn index() -> impl Responder { -/// NamedFile::open("./static/index.html") +/// NamedFile::open_async("./static/index.html").await /// } /// ``` -#[derive(Debug)] pub struct NamedFile { path: PathBuf, file: File, @@ -83,6 +82,37 @@ pub struct NamedFile { pub(crate) encoding: Option, } +impl fmt::Debug for NamedFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NamedFile") + .field("path", &self.path) + .field( + "file", + #[cfg(feature = "experimental-io-uring")] + { + &"tokio_uring::File" + }, + #[cfg(not(feature = "experimental-io-uring"))] + { + &self.file + }, + ) + .field("modified", &self.modified) + .field("md", &self.md) + .field("flags", &self.flags) + .field("status_code", &self.status_code) + .field("content_type", &self.content_type) + .field("content_disposition", &self.content_disposition) + .field("encoding", &self.encoding) + .finish() + } +} + +#[cfg(not(feature = "experimental-io-uring"))] +pub(crate) use std::fs::File; +#[cfg(feature = "experimental-io-uring")] +pub(crate) use tokio_uring::fs::File; + impl NamedFile { /// Creates an instance from a previously opened file. /// @@ -90,8 +120,7 @@ impl NamedFile { /// `ContentDisposition` headers. /// /// # Examples - /// - /// ``` + /// ```ignore /// use actix_files::NamedFile; /// use std::io::{self, Write}; /// use std::env; @@ -152,7 +181,30 @@ impl NamedFile { (ct, cd) }; - let md = file.metadata()?; + let md = { + #[cfg(not(feature = "experimental-io-uring"))] + { + file.metadata()? + } + + #[cfg(feature = "experimental-io-uring")] + { + use std::os::unix::prelude::{AsRawFd, FromRawFd}; + + let fd = file.as_raw_fd(); + + // SAFETY: fd is borrowed and lives longer than the unsafe block + unsafe { + let file = std::fs::File::from_raw_fd(fd); + let md = file.metadata(); + // SAFETY: forget the fd before exiting block in success or error case but don't + // run destructor (that would close file handle) + std::mem::forget(file); + md? + } + } + }; + let modified = md.modified().ok(); let encoding = None; @@ -169,17 +221,45 @@ impl NamedFile { }) } + #[cfg(not(feature = "experimental-io-uring"))] /// Attempts to open a file in read-only mode. /// /// # Examples - /// /// ``` /// use actix_files::NamedFile; - /// /// let file = NamedFile::open("foo.txt"); /// ``` pub fn open>(path: P) -> io::Result { - Self::from_file(File::open(&path)?, path) + let file = File::open(&path)?; + Self::from_file(file, path) + } + + /// Attempts to open a file asynchronously in read-only mode. + /// + /// When the `experimental-io-uring` crate feature is enabled, this will be async. + /// Otherwise, it will be just like [`open`][Self::open]. + /// + /// # Examples + /// ``` + /// use actix_files::NamedFile; + /// # async fn open() { + /// let file = NamedFile::open_async("foo.txt").await.unwrap(); + /// # } + /// ``` + pub async fn open_async>(path: P) -> io::Result { + let file = { + #[cfg(not(feature = "experimental-io-uring"))] + { + File::open(&path)? + } + + #[cfg(feature = "experimental-io-uring")] + { + File::open(&path).await? + } + }; + + Self::from_file(file, path) } /// Returns reference to the underlying `File` object. @@ -191,13 +271,12 @@ impl NamedFile { /// Retrieve the path of this file. /// /// # Examples - /// /// ``` /// # use std::io; /// use actix_files::NamedFile; /// - /// # fn path() -> io::Result<()> { - /// let file = NamedFile::open("test.txt")?; + /// # async fn path() -> io::Result<()> { + /// let file = NamedFile::open_async("test.txt").await?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); /// # Ok(()) /// # } @@ -337,7 +416,7 @@ impl NamedFile { res.encoding(current_encoding); } - let reader = ChunkedReadFile::new(self.md.len(), 0, self.file); + let reader = super::chunked::new_chunked_read(self.md.len(), 0, self.file); return res.streaming(reader); } @@ -451,7 +530,7 @@ impl NamedFile { return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None); } - let reader = ChunkedReadFile::new(length, offset, self.file); + let reader = super::chunked::new_chunked_read(length, offset, self.file); if offset != 0 || length != self.md.len() { resp.status(StatusCode::PARTIAL_CONTENT); @@ -461,20 +540,6 @@ impl NamedFile { } } -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut File { - &mut self.file - } -} - /// Returns true if `req` has no `If-Match` header or one which matches `etag`. fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { @@ -515,6 +580,20 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} + impl Responder for NamedFile { fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) @@ -525,14 +604,16 @@ impl ServiceFactory for NamedFile { type Response = ServiceResponse; type Error = Error; type Config = (); - type InitError = (); type Service = NamedFileService; - type Future = Ready>; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - ok(NamedFileService { + let service = NamedFileService { path: self.path.clone(), - }) + }; + + Box::pin(async move { Ok(service) }) } } @@ -545,18 +626,19 @@ pub struct NamedFileService { impl Service for NamedFileService { type Response = ServiceResponse; type Error = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, _) = req.into_parts(); - ready( - NamedFile::open(&self.path) - .map_err(|e| e.into()) - .map(|f| f.into_response(&req)) - .map(|res| ServiceResponse::new(req, res)), - ) + + let path = self.path.clone(); + Box::pin(async move { + let file = NamedFile::open_async(path).await?; + let res = file.into_response(&req); + Ok(ServiceResponse::new(req, res)) + }) } } diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 0e0d4f51..8c8bca6c 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -1,9 +1,9 @@ use std::{ + future::{ready, Ready}, path::{Path, PathBuf}, str::FromStr, }; -use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, FromRequest, HttpRequest}; use crate::error::UriSegmentError; diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 09122c63..f6e1c2e1 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,7 +1,6 @@ -use std::{fmt, io, path::PathBuf, rc::Rc}; +use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; use actix_service::Service; -use actix_utils::future::ok; use actix_web::{ dev::{ServiceRequest, ServiceResponse}, error::Error, @@ -17,7 +16,18 @@ use crate::{ }; /// Assembled file serving service. -pub struct FilesService { +#[derive(Clone)] +pub struct FilesService(pub(crate) Rc); + +impl Deref for FilesService { + type Target = FilesServiceInner; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +pub struct FilesServiceInner { pub(crate) directory: PathBuf, pub(crate) index: Option, pub(crate) show_index: bool, @@ -31,20 +41,50 @@ pub struct FilesService { pub(crate) hidden_files: bool, } +impl fmt::Debug for FilesServiceInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FilesServiceInner") + } +} + impl FilesService { - fn handle_err( + async fn handle_err( &self, err: io::Error, req: ServiceRequest, - ) -> LocalBoxFuture<'static, Result> { + ) -> Result { log::debug!("error handling {}: {}", req.path(), err); if let Some(ref default) = self.default { - Box::pin(default.call(req)) + default.call(req).await } else { - Box::pin(ok(req.error_response(err))) + Ok(req.error_response(err)) } } + + fn serve_named_file( + &self, + req: ServiceRequest, + mut named_file: NamedFile, + ) -> ServiceResponse { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + let res = named_file.into_response(&req); + ServiceResponse::new(req, res) + } + + fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { + let dir = Directory::new(self.directory.clone(), path); + + let (req, _) = req.into_parts(); + + (self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req)) + } } impl fmt::Debug for FilesService { @@ -56,7 +96,7 @@ impl fmt::Debug for FilesService { impl Service for FilesService { type Response = ServiceResponse; type Error = Error; - type Future = LocalBoxFuture<'static, Result>; + type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); @@ -69,103 +109,87 @@ impl Service for FilesService { matches!(*req.method(), Method::HEAD | Method::GET) }; - if !is_method_valid { - return Box::pin(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) - .body("Request did not meet this resource's requirements."), - ))); - } + let this = self.clone(); - let real_path = - match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { - Ok(item) => item, - Err(e) => return Box::pin(ok(req.error_response(e))), - }; + Box::pin(async move { + if !is_method_valid { + return Ok(req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) + .body("Request did not meet this resource's requirements."), + )); + } - if let Some(filter) = &self.path_filter { - if !filter(real_path.as_ref(), req.head()) { - if let Some(ref default) = self.default { - return Box::pin(default.call(req)); - } else { - return Box::pin(ok( - req.into_response(actix_web::HttpResponse::NotFound().finish()) + let real_path = + match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) { + Ok(item) => item, + Err(e) => return Ok(req.error_response(e)), + }; + + if let Some(filter) = &this.path_filter { + if !filter(real_path.as_ref(), req.head()) { + if let Some(ref default) = this.default { + return default.call(req).await; + } else { + return Ok( + req.into_response(actix_web::HttpResponse::NotFound().finish()) + ); + } + } + } + + // full file path + let path = this.directory.join(&real_path); + if let Err(err) = path.canonicalize() { + return this.handle_err(err, req).await; + } + + if path.is_dir() { + if this.redirect_to_slash + && !req.path().ends_with('/') + && (this.index.is_some() || this.show_index) + { + let redirect_to = format!("{}/", req.path()); + + return Ok(req.into_response( + HttpResponse::Found() + .insert_header((header::LOCATION, redirect_to)) + .finish(), )); } - } - } - // full file path - let path = self.directory.join(&real_path); - if let Err(err) = path.canonicalize() { - return Box::pin(self.handle_err(err, req)); - } - - if path.is_dir() { - if self.redirect_to_slash - && !req.path().ends_with('/') - && (self.index.is_some() || self.show_index) - { - let redirect_to = format!("{}/", req.path()); - - return Box::pin(ok(req.into_response( - HttpResponse::Found() - .insert_header((header::LOCATION, redirect_to)) - .finish(), - ))); - } - - let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - named_file.flags = self.file_flags; - - let (req, _) = req.into_parts(); - let res = named_file.into_response(&req); - Box::pin(ok(ServiceResponse::new(req, res))) - }; - - let show_index = |req: ServiceRequest| { - let dir = Directory::new(self.directory.clone(), path.clone()); - - let (req, _) = req.into_parts(); - let x = (self.renderer)(&dir, &req); - - Box::pin(match x { - Ok(resp) => ok(resp), - Err(err) => ok(ServiceResponse::from_err(err, req)), - }) - }; - - match self.index { - Some(ref index) => match NamedFile::open(path.join(index)) { - Ok(named_file) => serve_named_file(req, named_file), - Err(_) if self.show_index => show_index(req), - Err(err) => self.handle_err(err, req), - }, - None if self.show_index => show_index(req), - _ => Box::pin(ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - ))), - } - } else { - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; + match this.index { + Some(ref index) => { + let named_path = path.join(index); + match NamedFile::open_async(named_path).await { + Ok(named_file) => Ok(this.serve_named_file(req, named_file)), + Err(_) if this.show_index => Ok(this.show_index(req, path)), + Err(err) => this.handle_err(err, req).await, + } } - named_file.flags = self.file_flags; - - let (req, _) = req.into_parts(); - let res = named_file.into_response(&req); - Box::pin(ok(ServiceResponse::new(req, res))) + None if this.show_index => Ok(this.show_index(req, path)), + _ => Ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + )), + } + } else { + match NamedFile::open_async(&path).await { + Ok(mut named_file) => { + if let Some(ref mime_override) = this.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = this.file_flags; + + let (req, _) = req.into_parts(); + let res = named_file.into_response(&req); + Ok(ServiceResponse::new(req, res)) + } + Err(err) => this.handle_err(err, req).await, } - Err(err) => self.handle_err(err, req), } - } + }) } } diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index ea00acb0..3356f533 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] + +[#2408]: https://github.com/actix/actix-web/pull/2408 ## 3.0.0-beta.6 - 2021-11-15 diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 699bb266..a4bc6b2b 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -66,25 +66,24 @@ pub async fn test_server_with_addr>( // run server in separate thread thread::spawn(move || { - let sys = System::new(); - let local_addr = tcp.local_addr().unwrap(); + System::new().block_on(async move { + let local_addr = tcp.local_addr().unwrap(); - let srv = Server::build() - .workers(1) - .disable_signals() - .listen("test", tcp, factory) - .expect("test server could not be created"); + let srv = Server::build() + .workers(1) + .disable_signals() + .system_exit() + .listen("test", tcp, factory) + .expect("test server could not be created"); - let srv = srv.run(); - started_tx - .send((System::current(), srv.handle(), local_addr)) - .unwrap(); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - // drive server loop - sys.block_on(srv).unwrap(); - - // start system event loop - sys.run().unwrap(); + // drive server loop + srv.await.unwrap(); + }); // notify TestServer that server and system have shut down // all thread managed resources should be dropped at this point diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 5c22139a..78fd4e4c 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] + +[#2408]: https://github.com/actix/actix-web/pull/2408 ## 0.1.0-beta.6 - 2021-11-15 diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 6c776a87..b80918ec 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -146,156 +146,183 @@ where // run server in separate orphaned thread thread::spawn(move || { - let sys = rt::System::new(); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - let factory = factory.clone(); - let srv_cfg = cfg.clone(); - let timeout = cfg.client_timeout; - let builder = Server::build().workers(1).disable_signals().system_exit(); + rt::System::new().block_on(async move { + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + let factory = factory.clone(); + let srv_cfg = cfg.clone(); + let timeout = cfg.client_timeout; - let srv = match srv_cfg.stream { - StreamType::Tcp => match srv_cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let builder = Server::build().workers(1).disable_signals().system_exit(); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + let srv = match srv_cfg.stream { + StreamType::Tcp => match srv_cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h1(map_config(fac, move |_| app_cfg.clone())) - .tcp() - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h2(map_config(fac, move |_| app_cfg.clone())) - .tcp() - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .finish(map_config(fac, move |_| app_cfg.clone())) - .tcp() - }), - }, - #[cfg(feature = "openssl")] - StreamType::Openssl(acceptor) => match cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + }, + #[cfg(feature = "openssl")] + StreamType::Openssl(acceptor) => match cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h1(map_config(fac, move |_| app_cfg.clone())) - .openssl(acceptor.clone()) - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h2(map_config(fac, move |_| app_cfg.clone())) - .openssl(acceptor.clone()) - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .finish(map_config(fac, move |_| app_cfg.clone())) - .openssl(acceptor.clone()) - }), - }, - #[cfg(feature = "rustls")] - StreamType::Rustls(config) => match cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + }, + #[cfg(feature = "rustls")] + StreamType::Rustls(config) => match cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h1(map_config(fac, move |_| app_cfg.clone())) - .rustls(config.clone()) - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .h2(map_config(fac, move |_| app_cfg.clone())) - .rustls(config.clone()) - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = - AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let fac = factory() - .into_factory() - .map_err(|err| err.into().error_response()); + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); - HttpService::build() - .client_timeout(timeout) - .finish(map_config(fac, move |_| app_cfg.clone())) - .rustls(config.clone()) - }), - }, - } - .expect("test server could not be created"); + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); - let srv = srv.run(); - started_tx - .send((System::current(), srv.handle(), local_addr)) - .unwrap(); + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + }, + } + .expect("test server could not be created"); - // drive server loop - sys.block_on(srv).unwrap(); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - // start system event loop - sys.run().unwrap(); + // drive server loop + srv.await.unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + }); - // notify TestServer that server and system have shut down - // all thread managed resources should be dropped at this point let _ = thread_stop_tx.send(()); }); diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 3e85cb84..d8878a82 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -82,7 +82,8 @@ pub struct CompressMiddleware { } static SUPPORTED_ALGORITHM_NAMES: Lazy = Lazy::new(|| { - let mut encoding = vec![]; + #[allow(unused_mut)] // only unused when no compress features enabled + let mut encoding: Vec<&str> = vec![]; #[cfg(feature = "compress-brotli")] { From a2a42ec152b41655345eefacea94af0f526a6d2e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 21 Nov 2021 23:34:58 +0000 Subject: [PATCH 65/99] use anybody in doc test --- actix-http/src/body/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 724e2059..29d7593d 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -28,15 +28,15 @@ pub use self::sized_stream::SizedStream; /// /// # Examples /// ``` -/// use actix_http::body::{Body, to_bytes}; +/// use actix_http::body::{AnyBody, to_bytes}; /// use bytes::Bytes; /// /// # async fn test_to_bytes() { -/// let body = Body::None; +/// let body = AnyBody::None; /// let bytes = to_bytes(body).await.unwrap(); /// assert!(bytes.is_empty()); /// -/// let body = Body::Bytes(Bytes::from_static(b"123")); +/// let body = AnyBody::Bytes(Bytes::from_static(b"123")); /// let bytes = to_bytes(body).await.unwrap(); /// assert_eq!(bytes, b"123"[..]); /// # } From a172f5968d828431122cab21b34ea0e5314684e8 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 15:37:23 +0000 Subject: [PATCH 66/99] prepare for actix-tls v3 beta 9 (#2456) --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-http/CHANGES.md | 2 ++ actix-http/Cargo.toml | 4 +-- actix-http/src/body/body.rs | 7 +++-- actix-http/src/body/mod.rs | 50 +++++++++++++++--------------- actix-http/src/h1/service.rs | 30 ++++++++++-------- actix-http/src/h2/service.rs | 60 +++++++++++++++++++----------------- actix-http/src/service.rs | 37 +++++++++++++--------- awc/Cargo.toml | 4 +-- 10 files changed, 109 insertions(+), 89 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 537d1b5f..2ac04a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ actix-rt = "2.3" actix-server = "2.0.0-beta.9" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true } actix-http = "3.0.0-beta.12" actix-router = "0.5.0-beta.2" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index f118d162..c670464a 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.1" -actix-tls = "3.0.0-beta.7" +actix-tls = "3.0.0-beta.9" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.9" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 71cdd6d4..7e7e9f4d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - 2021-xx-xx ### Added * `body::AnyBody::empty` for quickly creating an empty body. [#2446] +* `body::AnyBody::none` for quickly creating a "none" body. [#2456] * `impl Clone` for `body::AnyBody where S: Clone`. [#2448] * `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] @@ -21,6 +22,7 @@ * `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 +[#2456]: https://github.com/actix/actix-web/pull/2456 ## 3.0.0-beta.12 - 2021-11-15 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index c2de71e1..852c5ce2 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -73,7 +73,7 @@ sha-1 = "0.9" smallvec = "1.6.1" # tls -actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } @@ -83,7 +83,7 @@ zstd = { version = "0.9", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.9" actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.9", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.9" diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index c6439b55..e8861024 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -32,9 +32,12 @@ pub enum AnyBody { } impl AnyBody { - // TODO: a None body constructor + /// Constructs a "body" representing an empty response. + pub fn none() -> Self { + Self::None + } - /// Constructs a new, empty body. + /// Constructs a new, 0-length body. pub fn empty() -> Self { Self::Bytes(Bytes::new()) } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 29d7593d..df6c6b08 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -32,11 +32,11 @@ pub use self::sized_stream::SizedStream; /// use bytes::Bytes; /// /// # async fn test_to_bytes() { -/// let body = AnyBody::None; +/// let body = AnyBody::none(); /// let bytes = to_bytes(body).await.unwrap(); /// assert!(bytes.is_empty()); /// -/// let body = AnyBody::Bytes(Bytes::from_static(b"123")); +/// let body = AnyBody::copy_from_slice(b"123"); /// let bytes = to_bytes(body).await.unwrap(); /// assert_eq!(bytes, b"123"[..]); /// # } @@ -75,7 +75,7 @@ mod tests { use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; - use super::*; + use super::{to_bytes, AnyBody as TestAnyBody, BodySize, MessageBody as _}; impl AnyBody { pub(crate) fn get_ref(&self) -> &[u8] { @@ -87,13 +87,13 @@ mod tests { } /// AnyBody alias because rustc does not (can not?) infer the default type parameter. - type TestBody = AnyBody; + type AnyBody = TestAnyBody; #[actix_rt::test] async fn test_static_str() { - assert_eq!(TestBody::from("").size(), BodySize::Sized(0)); - assert_eq!(TestBody::from("test").size(), BodySize::Sized(4)); - assert_eq!(TestBody::from("test").get_ref(), b"test"); + assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); + assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from("test").get_ref(), b"test"); assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( @@ -107,14 +107,14 @@ mod tests { #[actix_rt::test] async fn test_static_bytes() { - assert_eq!(TestBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - TestBody::copy_from_slice(b"test".as_ref()).size(), + AnyBody::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); assert_eq!( - TestBody::copy_from_slice(b"test".as_ref()).get_ref(), + AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), b"test" ); let sb = Bytes::from(&b"test"[..]); @@ -129,8 +129,8 @@ mod tests { #[actix_rt::test] async fn test_vec() { - assert_eq!(TestBody::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(Vec::from("test")).get_ref(), b"test"); + assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); let test_vec = Vec::from("test"); pin!(test_vec); @@ -147,8 +147,8 @@ mod tests { #[actix_rt::test] async fn test_bytes() { let b = Bytes::from("test"); - assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -161,8 +161,8 @@ mod tests { #[actix_rt::test] async fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -175,10 +175,10 @@ mod tests { #[actix_rt::test] async fn test_string() { let b = "test".to_owned(); - assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); - assert_eq!(TestBody::from(&b).size(), BodySize::Sized(4)); - assert_eq!(TestBody::from(&b).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(&b).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -219,22 +219,22 @@ mod tests { #[actix_rt::test] async fn test_body_debug() { - assert!(format!("{:?}", TestBody::None).contains("Body::None")); - assert!(format!("{:?}", TestBody::from(Bytes::from_static(b"1"))).contains('1')); + assert!(format!("{:?}", AnyBody::None).contains("Body::None")); + assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] async fn test_serde_json() { use serde_json::{json, Value}; assert_eq!( - TestBody::from( + AnyBody::from( serde_json::to_vec(&Value::String("test".to_owned())).unwrap() ) .size(), BodySize::Sized(6) ); assert_eq!( - TestBody::from( + AnyBody::from( serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() ) .size(), diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index dbad8cfa..970c45ef 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -102,7 +102,6 @@ where mod openssl { use super::*; - use actix_service::ServiceFactoryExt; use actix_tls::accept::{ openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, TlsError, @@ -133,7 +132,7 @@ mod openssl { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create openssl based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -145,11 +144,13 @@ mod openssl { InitError = (), > { Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) - .and_then(|io: TlsStream| { + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { let peer_addr = io.get_ref().peer_addr().ok(); - ready(Ok((io, peer_addr))) + (io, peer_addr) }) .and_then(self.map_err(TlsError::Service)) } @@ -158,16 +159,17 @@ mod openssl { #[cfg(feature = "rustls")] mod rustls { - use super::*; use std::io; - use actix_service::ServiceFactoryExt; + use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ rustls::{Acceptor, ServerConfig, TlsStream}, TlsError, }; + use super::*; + impl H1Service, S, B, X, U> where S: ServiceFactory, @@ -193,7 +195,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create rustls based service + /// Create Rustls based service. pub fn rustls( self, config: ServerConfig, @@ -205,11 +207,13 @@ mod rustls { InitError = (), > { Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) - .and_then(|io: TlsStream| { + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { let peer_addr = io.get_ref().0.peer_addr().ok(); - ready(Ok((io, peer_addr))) + (io, peer_addr) }) .and_then(self.map_err(TlsError::Service)) } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 32dae8ac..794397bf 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -101,9 +101,11 @@ where #[cfg(feature = "openssl")] mod openssl { - use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, + TlsError, + }; use super::*; @@ -118,7 +120,7 @@ mod openssl { B: MessageBody + 'static, B::Error: Into>, { - /// Create OpenSSL based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -130,16 +132,14 @@ mod openssl { InitError = S::InitError, > { Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) - .and_then(fn_factory(|| { - ready(Ok::<_, S::InitError>(fn_service( - |io: TlsStream| { - let peer_addr = io.get_ref().peer_addr().ok(); - ready(Ok((io, peer_addr))) - }, - ))) - })) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().peer_addr().ok(); + (io, peer_addr) + }) .and_then(self.map_err(TlsError::Service)) } } @@ -147,12 +147,16 @@ mod openssl { #[cfg(feature = "rustls")] mod rustls { - use super::*; - use actix_service::ServiceFactoryExt; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::accept::TlsError; use std::io; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls::{Acceptor, ServerConfig, TlsStream}, + TlsError, + }; + + use super::*; + impl H2Service, S, B> where S: ServiceFactory, @@ -164,7 +168,7 @@ mod rustls { B: MessageBody + 'static, B::Error: Into>, { - /// Create Rustls based service + /// Create Rustls based service. pub fn rustls( self, mut config: ServerConfig, @@ -180,16 +184,14 @@ mod rustls { config.alpn_protocols = protos; Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) - .and_then(fn_factory(|| { - ready(Ok::<_, S::InitError>(fn_service( - |io: TlsStream| { - let peer_addr = io.get_ref().0.peer_addr().ok(); - ready(Ok((io, peer_addr))) - }, - ))) - })) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().0.peer_addr().ok(); + (io, peer_addr) + }) .and_then(self.map_err(TlsError::Service)) } } diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 62c96887..aa3e54a8 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -195,9 +195,11 @@ where #[cfg(feature = "openssl")] mod openssl { - use actix_service::ServiceFactoryExt; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, + TlsError, + }; use super::*; @@ -227,7 +229,7 @@ mod openssl { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create openssl based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -239,9 +241,11 @@ mod openssl { InitError = (), > { Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) - .and_then(|io: TlsStream| async { + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") { Protocol::Http2 @@ -251,8 +255,9 @@ mod openssl { } else { Protocol::Http1 }; + let peer_addr = io.get_ref().peer_addr().ok(); - Ok((io, proto, peer_addr)) + (io, proto, peer_addr) }) .and_then(self.map_err(TlsError::Service)) } @@ -263,11 +268,13 @@ mod openssl { mod rustls { use std::io; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls::{Acceptor, ServerConfig, TlsStream}, + TlsError, + }; use super::*; - use actix_service::ServiceFactoryExt; impl HttpService, S, B, X, U> where @@ -295,7 +302,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create rustls based service + /// Create Rustls based service. pub fn rustls( self, mut config: ServerConfig, @@ -311,8 +318,10 @@ mod rustls { config.alpn_protocols = protos; Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) .and_then(|io: TlsStream| async { let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") { diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 048fe78d..c4062159 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -57,7 +57,7 @@ actix-codec = "0.4.1" actix-service = "2.0.0" actix-http = "3.0.0-beta.12" actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0-beta.7", features = ["connect"] } +actix-tls = { version = "3.0.0-beta.9", features = ["connect"] } actix-utils = "3.0.0" ahash = "0.7" @@ -93,7 +93,7 @@ actix-http = { version = "3.0.0-beta.12", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.9" -actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } brotli2 = "0.3.2" From e7987e74293f86bf511d83baca23735df5f3d00c Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 23 Nov 2021 02:16:56 +0800 Subject: [PATCH 67/99] awc: support http2 over plain tcp with feature flag (#2439) Co-authored-by: Rob Ede --- actix-http-test/src/lib.rs | 18 +++---- awc/Cargo.toml | 5 ++ awc/src/client/connector.rs | 101 ++++++++++++++++++++++++++++++++++-- awc/src/request.rs | 2 +- awc/tests/test_client.rs | 35 ++++++++----- awc/tests/test_connector.rs | 2 +- 6 files changed, 136 insertions(+), 27 deletions(-) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index a4bc6b2b..7f55a0bf 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -21,16 +21,15 @@ use http::Method; use socket2::{Domain, Protocol, Socket, Type}; use tokio::sync::mpsc; -/// Start test server +/// Start test server. /// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. +/// `TestServer` is very simple test server that simplify process of writing integration tests cases +/// for HTTP applications. /// /// # Examples -/// -/// ``` +/// ```no_run /// use actix_http::HttpService; -/// use actix_http_test::TestServer; +/// use actix_http_test::test_server; /// use actix_web::{web, App, HttpResponse, Error}; /// /// async fn my_handler() -> Result { @@ -39,10 +38,9 @@ use tokio::sync::mpsc; /// /// #[actix_web::test] /// async fn test_example() { -/// let mut srv = TestServer::start( -/// || HttpService::new( -/// App::new().service( -/// web::resource("/").to(my_handler)) +/// let mut srv = TestServer::start(|| +/// HttpService::new( +/// App::new().service(web::resource("/").to(my_handler)) /// ) /// ); /// diff --git a/awc/Cargo.toml b/awc/Cargo.toml index c4062159..1dbb0ec1 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -52,6 +52,11 @@ trust-dns = ["trust-dns-resolver"] # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] +# Enable dangerous feature for testing and local network usage: +# - HTTP/2 over TCP(No Tls). +# DO NOT enable this over any internet use case. +dangerous-h2c = [] + [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 8a162c4f..54778d31 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -67,9 +67,9 @@ impl Connector<()> { > + Clone, > { Connector { - ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), connector: new_connector(resolver::resolver()), config: ConnectorConfig::default(), + ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), } } @@ -189,7 +189,7 @@ where http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()], _ => { - unimplemented!("actix-http:client: supported versions http/1.1, http/2") + unimplemented!("actix-http client only supports versions http/1.1 & http/2") } }; self.ssl = Connector::build_ssl(versions); @@ -279,7 +279,63 @@ where }; let tls_service = match self.ssl { - SslConnector::None => None, + SslConnector::None => { + #[cfg(not(feature = "dangerous-h2c"))] + { + None + } + #[cfg(feature = "dangerous-h2c")] + { + use std::{ + future::{ready, Ready}, + io, + }; + + use actix_tls::connect::Connection; + + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let io = self.into_parts().0; + (io, Protocol::Http2) + } + } + + /// With the `dangerous-h2c` feature enabled, this connector uses a no-op TLS + /// connection service that passes through plain TCP as a TLS connection. + /// + /// The protocol version of this fake TLS connection is set to be HTTP/2. + #[derive(Clone)] + struct NoOpTlsConnectorService; + + impl Service> for NoOpTlsConnectorService + where + U: ActixStream + 'static, + { + type Response = Connection>; + type Error = io::Error; + type Future = Ready>; + + actix_service::always_ready!(); + + fn call(&self, connection: Connection) -> Self::Future { + let (io, connection) = connection.replace_io(()); + let (_, connection) = connection.replace_io(Box::new(io) as _); + + ready(Ok(connection)) + } + } + + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: NoOpTlsConnectorService, + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) + } + } #[cfg(feature = "openssl")] SslConnector::Openssl(tls) => { const H2: &[u8] = b"h2"; @@ -760,3 +816,42 @@ mod resolver { }) } } + +#[cfg(feature = "dangerous-h2c")] +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use actix_http::{HttpService, Request, Response, Version}; + use actix_http_test::test_server; + use actix_service::ServiceFactoryExt as _; + + use super::*; + use crate::Client; + + #[actix_rt::test] + async fn h2c_connector() { + let mut srv = test_server(|| { + HttpService::build() + .h2(|_req: Request| async { Ok::<_, Infallible>(Response::ok()) }) + .tcp() + .map_err(|_| ()) + }) + .await; + + let connector = Connector { + connector: new_connector(resolver::resolver()), + config: ConnectorConfig::default(), + ssl: SslConnector::None, + }; + + let client = Client::builder().connector(connector).finish(); + + let request = client.get(srv.surl("/")).send(); + let response = request.await.unwrap(); + assert!(response.status().is_success()); + assert_eq!(response.version(), Version::HTTP_2); + + srv.stop().await; + } +} diff --git a/awc/src/request.rs b/awc/src/request.rs index bc3859e2..f364b43c 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -115,10 +115,10 @@ impl ClientRequest { &self.head.method } - #[doc(hidden)] /// Set HTTP version of this request. /// /// By default requests's HTTP version depends on network stream + #[doc(hidden)] #[inline] pub fn version(mut self, version: Version) -> Self { self.head.version = version; diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index a0af0cab..856a4ace 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,20 +1,26 @@ -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::net::{IpAddr, Ipv4Addr}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; +use std::{ + collections::HashMap, + io::{Read, Write}, + net::{IpAddr, Ipv4Addr}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; use actix_utils::future::ok; -use brotli2::write::BrotliEncoder; use bytes::Bytes; use cookie::Cookie; -use flate2::read::GzDecoder; -use flate2::write::GzEncoder; -use flate2::Compression; use futures_util::stream; use rand::Rng; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliEncoder; + +#[cfg(feature = "compress-gzip")] +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + use actix_http::{ http::{self, StatusCode}, HttpService, @@ -24,7 +30,6 @@ use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; use actix_web::{ dev::{AppConfig, BodyEncoding}, http::header, - middleware::Compress, web, App, Error, HttpRequest, HttpResponse, }; use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; @@ -463,11 +468,12 @@ async fn test_with_query_parameter() { assert!(res.status().is_success()); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_no_decompress() { let srv = actix_test::start(|| { App::new() - .wrap(Compress::default()) + .wrap(actix_web::middleware::Compress::default()) .service(web::resource("/").route(web::to(|| { let mut res = HttpResponse::Ok().body(STR); res.encoding(header::ContentEncoding::Gzip); @@ -507,6 +513,7 @@ async fn test_no_decompress() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding() { let srv = actix_test::start(|| { @@ -530,6 +537,7 @@ async fn test_client_gzip_encoding() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding_large() { let srv = actix_test::start(|| { @@ -553,6 +561,7 @@ async fn test_client_gzip_encoding_large() { assert_eq!(bytes, Bytes::from(STR.repeat(10))); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding_large_random() { let data = rand::thread_rng() @@ -581,6 +590,7 @@ async fn test_client_gzip_encoding_large_random() { assert_eq!(bytes, Bytes::from(data)); } +#[cfg(feature = "compress-brotli")] #[actix_rt::test] async fn test_client_brotli_encoding() { let srv = actix_test::start(|| { @@ -603,6 +613,7 @@ async fn test_client_brotli_encoding() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-brotli")] #[actix_rt::test] async fn test_client_brotli_encoding_large_random() { let data = rand::thread_rng() diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index 632f68b7..588c5146 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -39,7 +39,7 @@ fn tls_config() -> SslAcceptor { #[actix_rt::test] async fn test_connection_window_size() { - let srv = test_server(move || { + let srv = test_server(|| { HttpService::build() .h2(map_config( App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), From 88e074879d564bcbac5da68b06699069423536c9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:19:09 +0000 Subject: [PATCH 68/99] prepare actix-http release 3.0.0-beta.13 --- Cargo.toml | 2 +- actix-files/Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-http/CHANGES.md | 3 +++ actix-http/Cargo.toml | 2 +- actix-http/README.md | 4 ++-- actix-multipart/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 4 ++-- 10 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ac04a66..21a0d57a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true } -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" actix-router = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.5" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c0ff1867..e61a140c 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -19,7 +19,7 @@ experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false } -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" actix-service = "2.0.0" askama_escape = "0.10" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index c670464a..7fbc3bfb 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -52,4 +52,4 @@ tokio = { version = "1.2", features = ["sync"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 7e7e9f4d..ee8b9e99 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.13 - 2021-11-22 ### Added * `body::AnyBody::empty` for quickly creating an empty body. [#2446] * `body::AnyBody::none` for quickly creating a "none" body. [#2456] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 852c5ce2..da2a9787 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.12" +version = "3.0.0-beta.13" 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 536d1707..d1f451e3 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.12)](https://docs.rs/actix-http/3.0.0-beta.12) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http/3.0.0-beta.13) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.12/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.12) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.13) [![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-multipart/Cargo.toml b/actix-multipart/Cargo.toml index b2f3e391..65299864 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -29,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index bc660293..b8e55467 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -29,7 +29,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" actix-http-test = "3.0.0-beta.6" actix-service = "2.0.0" actix-utils = "3.0.0" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c938c6a1..7d011ee2 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.1" -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 1dbb0ec1..f24246e3 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -60,7 +60,7 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" -actix-http = "3.0.0-beta.12" +actix-http = "3.0.0-beta.13" actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.0.0-beta.9", features = ["connect"] } actix-utils = "3.0.0" @@ -94,7 +94,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.12", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.13", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.9" From 5f5bd2184ebd3ea1e150d8d10922f4c77f5f029d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:20:55 +0000 Subject: [PATCH 69/99] prepare actix-web release 4.0.0-beta.12 --- CHANGES.md | 6 +++++- Cargo.toml | 2 +- README.md | 4 ++-- actix-http/CHANGES.md | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 784500d9..290d1dca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.12 - 2021-11-22 ### Changed * Compress middleware's response type is now `AnyBody>`. [#2448] @@ -10,7 +13,8 @@ ### Removed * `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] -[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 ## 4.0.0-beta.11 - 2021-11-15 diff --git a/Cargo.toml b/Cargo.toml index 21a0d57a..600cb252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.11" +version = "4.0.0-beta.12" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] diff --git a/README.md b/README.md index 9444f130..cc68b509 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.11)](https://docs.rs/actix-web/4.0.0-beta.11) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.12)](https://docs.rs/actix-web/4.0.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.11) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.12)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index ee8b9e99..1cc7ae5f 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -25,6 +25,7 @@ * `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 [#2456]: https://github.com/actix/actix-web/pull/2456 From 99e6a9c26d65c5fe0ffee5bf2ad8bdbedd7ab74c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:41:43 +0000 Subject: [PATCH 70/99] prepare awc release 3.0.0-beta.11 --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/CHANGES.md | 3 +++ awc/Cargo.toml | 2 +- awc/README.md | 4 ++-- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 600cb252..15af2565 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.10", features = ["openssl"] } +awc = { version = "3.0.0-beta.11", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 7fbc3bfb..05a907ce 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.9" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.9" -awc = { version = "3.0.0-beta.10", default-features = false } +awc = { version = "3.0.0-beta.11", default-features = false } base64 = "0.13" bytes = "1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index b8e55467..75b41627 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -35,7 +35,7 @@ actix-service = "2.0.0" actix-utils = "3.0.0" actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.10", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.11", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 7d011ee2..6924e09e 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -29,6 +29,6 @@ tokio = { version = "1", features = ["sync"] } actix-rt = "2.2" actix-test = "0.1.0-beta.6" -awc = { version = "3.0.0-beta.10", default-features = false } +awc = { version = "3.0.0-beta.11", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 98998fd5..6331d30a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.11 - 2021-11-22 + + ## 3.0.0-beta.10 - 2021-11-15 * No significant changes from `3.0.0-beta.9`. diff --git a/awc/Cargo.toml b/awc/Cargo.toml index f24246e3..0fd0a2df 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.10" +version = "3.0.0-beta.11" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/awc/README.md b/awc/README.md index 96c5ed40..7070337d 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.10)](https://docs.rs/awc/3.0.0-beta.10) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.11)](https://docs.rs/awc/3.0.0-beta.11) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.10/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.10) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.11/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.11) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 0062d99b6f1ff9d6dd67937fafe8542d3ddf8a0e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:46:19 +0000 Subject: [PATCH 71/99] prepare actix-files release 0.6.0-beta.9 --- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 8 ++++++-- actix-files/README.md | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 7da77560..63d8efc3 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.9 - 2021-11-22 * Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] * Add `NamedFile::open_async`. [#2408] * Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index e61a140c..11ec2948 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "actix-files" -version = "0.6.0-beta.8" -authors = ["Nikolay Kim "] +version = "0.6.0-beta.9" +authors = [ + "Nikolay Kim ", + "fakeshadow <24548779@qq.com>", + "Rob Ede ", +] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" diff --git a/actix-files/README.md b/actix-files/README.md index eac7339a..84e556fa 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.8)](https://docs.rs/actix-files/0.6.0-beta.8) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.9)](https://docs.rs/actix-files/0.6.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.8/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.9/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From b806b4773c726ae707fc2ae341aa406c4b7fc7e1 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:46:58 +0000 Subject: [PATCH 72/99] prepare actix-http-test release 3.0.0-beta.7 --- actix-http-test/CHANGES.md | 3 +++ actix-http-test/Cargo.toml | 2 +- actix-http-test/README.md | 4 ++-- actix-http/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 3356f533..37de57d4 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.7 - 2021-11-22 * Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 05a907ce..cfc32b52 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.6" +version = "3.0.0-beta.7" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 3eee6645..057cf6a1 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.6)](https://docs.rs/actix-http-test/3.0.0-beta.6) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http-test/3.0.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.6) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index da2a9787..1c7cd198 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -82,7 +82,7 @@ zstd = { version = "0.9", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.9" -actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.9", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 75b41627..432b230d 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" actix-http = "3.0.0-beta.13" -actix-http-test = "3.0.0-beta.6" +actix-http-test = "3.0.0-beta.7" actix-service = "2.0.0" actix-utils = "3.0.0" actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 0fd0a2df..dd22f0f6 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -95,7 +95,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } actix-http = { version = "3.0.0-beta.13", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.9" actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] } From 18b8ef076564f6c6073c4ba6e62b3ee443cb66c7 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:47:43 +0000 Subject: [PATCH 73/99] prepare actix-test release 0.1.0-beta.7 --- Cargo.toml | 2 +- actix-files/Cargo.toml | 2 +- actix-test/CHANGES.md | 3 +++ actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15af2565..dbe28df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.11", features = ["openssl"] } brotli2 = "0.3.2" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 11ec2948..f0ae00b9 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -43,4 +43,4 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" actix-web = "4.0.0-beta.11" -actix-test = "0.1.0-beta.6" +actix-test = "0.1.0-beta.7" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 78fd4e4c..b739011f 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.7 - 2021-11-22 * Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 432b230d..afbbd0cc 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.6" +version = "0.1.0-beta.7" authors = [ "Nikolay Kim ", "Rob Ede ", diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 6924e09e..9712e065 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.6" +actix-test = "0.1.0-beta.7" awc = { version = "3.0.0-beta.11", default-features = false } env_logger = "0.9" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index a407d00f..8497f0b2 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,7 +23,7 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.6" +actix-test = "0.1.0-beta.7" actix-utils = "3.0.0" actix-web = "4.0.0-beta.11" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index dd22f0f6..a8d0e3e3 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -99,7 +99,7 @@ actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.9" actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.9" From ab5eb7c1aa0356b523afbfdbf5876f6d88697694 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Nov 2021 18:48:14 +0000 Subject: [PATCH 74/99] prepare actix-multipart release 0.4.0-beta.8 --- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 2 +- actix-multipart/README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 97c01139..1ef4aab3 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.8 - 2021-11-22 * Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] * Added `MultipartError::NoContentDisposition` variant. [#2451] * Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 65299864..b1dea483 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.7" +version = "0.4.0-beta.8" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 67481429..75379629 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.7)](https://docs.rs/actix-multipart/0.4.0-beta.7) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.8)](https://docs.rs/actix-multipart/0.4.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.7/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.7) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.8/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.8) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From bcbbc115aa3132c751c7290d9190a35f98ddca1a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 23 Nov 2021 15:12:55 +0000 Subject: [PATCH 75/99] fix awc changelog --- .github/workflows/ci.yml | 8 +------- awc/CHANGES.md | 1 + awc/src/builder.rs | 18 ++++++++---------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38c066d6..d9b98a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,7 @@ jobs: with: { file: cobertura.xml } rustdoc: - name: rustdoc + name: doc tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -177,12 +177,6 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 - # - name: Install cargo-hack - # uses: actions-rs/cargo@v1 - # with: - # command: install - # args: cargo-hack - - name: doc tests uses: actions-rs/cargo@v1 timeout-minutes: 60 diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6331d30a..788dce05 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -4,6 +4,7 @@ ## 3.0.0-beta.11 - 2021-11-22 +* No significant changes from `3.0.0-beta.10`. ## 3.0.0-beta.10 - 2021-11-15 diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 11ececa7..fda7d93a 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,18 +1,16 @@ -use std::convert::TryFrom; -use std::fmt; -use std::net::IpAddr; -use std::rc::Rc; -use std::time::Duration; +use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}; use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; -use crate::client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}; -use crate::connect::DefaultConnector; -use crate::error::SendRequestError; -use crate::middleware::{NestTransform, Redirect, Transform}; -use crate::{Client, ClientConfig, ConnectRequest, ConnectResponse}; +use crate::{ + client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}, + connect::DefaultConnector, + error::SendRequestError, + middleware::{NestTransform, Redirect, Transform}, + Client, ClientConfig, ConnectRequest, ConnectResponse, +}; /// An HTTP Client builder /// From 9bdd334bb4687170351d6bfcbbde756eaeaffa0a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 23 Nov 2021 15:57:18 +0000 Subject: [PATCH 76/99] add test for duplicate dynamic segent name --- actix-router/src/resource.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index dcd65535..d5f738a0 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -1770,6 +1770,12 @@ mod tests { match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); } + #[test] + #[should_panic] + fn duplicate_segment_name() { + ResourceDef::new("/user/{id}/post/{id}"); + } + #[test] #[should_panic] fn invalid_dynamic_segment_delimiter() { From 3e6e9779dcc1493ca035d13d056827659b25f8ff Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 24 Nov 2021 20:16:15 +0000 Subject: [PATCH 77/99] fix big5 charset parsing --- actix-http/src/header/shared/charset.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs index b482f6bc..3cc0f3e2 100644 --- a/actix-http/src/header/shared/charset.rs +++ b/actix-http/src/header/shared/charset.rs @@ -88,7 +88,7 @@ impl Charset { Iso_8859_8_E => "ISO-8859-8-E", Iso_8859_8_I => "ISO-8859-8-I", Gb2312 => "GB2312", - Big5 => "big5", + Big5 => "Big5", Koi8_R => "KOI8-R", Ext(ref s) => s, } @@ -128,7 +128,7 @@ impl FromStr for Charset { "ISO-8859-8-E" => Iso_8859_8_E, "ISO-8859-8-I" => Iso_8859_8_I, "GB2312" => Gb2312, - "big5" => Big5, + "BIG5" => Big5, "KOI8-R" => Koi8_R, s => Ext(s.to_owned()), }) From 52bbbd1d7399d37ca587c67f7c130e25b61f89b1 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 25 Nov 2021 04:53:11 +0800 Subject: [PATCH 78/99] Mnior cleanup of multipart API. (#2461) --- actix-multipart/Cargo.toml | 2 +- actix-multipart/src/server.rs | 43 +++++++++++++---------------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index b1dea483..5376ba12 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -20,7 +20,6 @@ actix-utils = "3.0.0" bytes = "1" derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } -futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } httparse = "1.3" local-waker = "0.1" log = "0.4" @@ -30,5 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" actix-http = "3.0.0-beta.13" +futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 43f9ccf5..55ac00ff 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -17,7 +17,6 @@ use actix_web::{ }; use bytes::{Bytes, BytesMut}; use futures_core::stream::{LocalBoxStream, Stream}; -use futures_util::stream::StreamExt as _; use local_waker::LocalWaker; use crate::error::MultipartError; @@ -67,7 +66,7 @@ impl Multipart { /// Create multipart instance for boundary. pub fn new(headers: &HeaderMap, stream: S) -> Multipart where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, { match Self::boundary(headers) { Ok(boundary) => Multipart::from_boundary(boundary, stream), @@ -77,36 +76,29 @@ impl Multipart { /// Extract boundary info from headers. pub(crate) fn boundary(headers: &HeaderMap) -> Result { - if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - if let Some(boundary) = ct.get_param(mime::BOUNDARY) { - Ok(boundary.as_str().to_owned()) - } else { - Err(MultipartError::Boundary) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::NoContentType) - } + headers + .get(&header::CONTENT_TYPE) + .ok_or(MultipartError::NoContentType)? + .to_str() + .ok() + .and_then(|content_type| content_type.parse::().ok()) + .ok_or(MultipartError::ParseContentType)? + .get_param(mime::BOUNDARY) + .map(|boundary| boundary.as_str().to_owned()) + .ok_or(MultipartError::Boundary) } /// Create multipart instance for given boundary and stream pub(crate) fn from_boundary(boundary: String, stream: S) -> Multipart where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, { Multipart { error: None, safety: Safety::new(), inner: Some(Rc::new(RefCell::new(InnerMultipart { boundary, - payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), + payload: PayloadRef::new(PayloadBuffer::new(stream)), state: InnerState::FirstBoundary, item: InnerMultipartItem::None, }))), @@ -690,10 +682,7 @@ impl PayloadRef { } } - fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option> - where - 'a: 'b, - { + fn get_mut(&self, s: &Safety) -> Option> { if s.current() { Some(self.payload.borrow_mut()) } else { @@ -779,7 +768,7 @@ impl PayloadBuffer { PayloadBuffer { eof: false, buf: BytesMut::new(), - stream: stream.boxed_local(), + stream: Box::pin(stream), } } @@ -860,7 +849,7 @@ mod tests { use actix_web::test::TestRequest; use actix_web::FromRequest; use bytes::Bytes; - use futures_util::future::lazy; + use futures_util::{future::lazy, StreamExt}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; From 89c6d626569af3ca1deb22be64f111d121389476 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 25 Nov 2021 08:10:53 +0800 Subject: [PATCH 79/99] clean up multipart and field stream trait impl (#2462) --- actix-multipart/src/server.rs | 75 +++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 55ac00ff..c9642cfa 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -32,7 +32,7 @@ const MAX_HEADERS: usize = 32; pub struct Multipart { safety: Safety, error: Option, - inner: Option>>, + inner: Option, } enum InnerMultipartItem { @@ -96,12 +96,12 @@ impl Multipart { Multipart { error: None, safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { + inner: Some(InnerMultipart { boundary, payload: PayloadRef::new(PayloadBuffer::new(stream)), state: InnerState::FirstBoundary, item: InnerMultipartItem::None, - }))), + }), } } @@ -118,20 +118,27 @@ impl Multipart { impl Stream for Multipart { type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if let Some(err) = self.error.take() { - Poll::Ready(Some(Err(err))) - } else if self.safety.current() { - let this = self.get_mut(); - let mut inner = this.inner.as_mut().unwrap().borrow_mut(); - if let Some(mut payload) = inner.payload.get_mut(&this.safety) { - payload.poll_stream(cx)?; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + + match this.inner.as_mut() { + Some(inner) => { + if let Some(mut buffer) = inner.payload.get_mut(&this.safety) { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(MultipartError::NotConsumed))); + } else { + return Poll::Pending; + } + + inner.poll(&this.safety, cx) } - inner.poll(&this.safety, cx) - } else if !self.safety.is_clean() { - Poll::Ready(Some(Err(MultipartError::NotConsumed))) - } else { - Poll::Pending + None => Poll::Ready(Some(Err(this + .error + .take() + .expect("Multipart polled after finish")))), } } } @@ -152,17 +159,15 @@ impl InnerMultipart { Ok(httparse::Status::Complete((_, hdrs))) => { // convert headers let mut headers = HeaderMap::with_capacity(hdrs.len()); + for h in hdrs { - if let Ok(name) = HeaderName::try_from(h.name) { - if let Ok(value) = HeaderValue::try_from(h.value) { - headers.append(name, value); - } else { - return Err(ParseError::Header.into()); - } - } else { - return Err(ParseError::Header.into()); - } + let name = + HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; + let value = HeaderValue::try_from(h.value) + .map_err(|_| ParseError::Header)?; + headers.append(name, value); } + Ok(Some(headers)) } Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), @@ -458,17 +463,19 @@ impl Stream for Field { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if self.safety.current() { - let mut inner = self.inner.borrow_mut(); - if let Some(mut payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) { - payload.poll_stream(cx)?; - } - inner.poll(&self.safety) - } else if !self.safety.is_clean() { - Poll::Ready(Some(Err(MultipartError::NotConsumed))) + let this = self.get_mut(); + let mut inner = this.inner.borrow_mut(); + if let Some(mut buffer) = inner.payload.as_ref().unwrap().get_mut(&this.safety) { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(MultipartError::NotConsumed))); } else { - Poll::Pending + return Poll::Pending; } + + inner.poll(&this.safety) } } From 39243095b5a3399b5a39913fb2d1b0f494c3a4ba Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 28 Nov 2021 19:23:29 +0000 Subject: [PATCH 80/99] guarantee ordering of header map get_all (#2467) --- actix-http/CHANGES.md | 5 ++ actix-http/src/header/map.rs | 102 +++++++++++++++++++++++++++++++-- actix-http/src/header/mod.rs | 16 +++--- actix-http/src/header/utils.rs | 1 + actix-http/src/ws/proto.rs | 2 +- 5 files changed, 111 insertions(+), 15 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 1cc7ae5f..d3aa7fc0 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] +* Expose `header::{GetAll, Removed}` iterators. [#2467] + +[#2467]: https://github.com/actix/actix-web/pull/2467 ## 3.0.0-beta.13 - 2021-11-22 diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index a8fd9715..c7e4921a 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -288,7 +288,7 @@ impl HeaderMap { /// Returns an iterator over all values associated with a header name. /// /// The returned iterator does not incur any allocations and will yield no items if there are no - /// values associated with the key. Iteration order is **not** guaranteed to be the same as + /// values associated with the key. Iteration order is guaranteed to be the same as /// insertion order. /// /// # Examples @@ -355,6 +355,19 @@ impl HeaderMap { /// /// assert_eq!(map.len(), 1); /// ``` + /// + /// A convenience method is provided on the returned iterator to check if the insertion replaced + /// any values. + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(removed.is_empty()); + /// + /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html")); + /// assert!(!removed.is_empty()); + /// ``` pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed { let value = self.inner.insert(key, Value::one(val)); Removed::new(value) @@ -393,6 +406,9 @@ impl HeaderMap { /// Removes all headers for a particular header name from the map. /// + /// Providing an invalid header names (as a string argument) will have no effect and return + /// without error. + /// /// # Examples /// ``` /// # use actix_http::http::{header, HeaderMap, HeaderValue}; @@ -409,6 +425,21 @@ impl HeaderMap { /// assert!(removed.next().is_none()); /// /// assert!(map.is_empty()); + /// ``` + /// + /// A convenience method is provided on the returned iterator to check if the `remove` call + /// actually removed any values. + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let removed = map.remove("accept"); + /// assert!(removed.is_empty()); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/html")); + /// let removed = map.remove("accept"); + /// assert!(!removed.is_empty()); + /// ``` pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { let value = match key.try_as_name(super::as_name::Seal) { Ok(Cow::Borrowed(name)) => self.inner.remove(name), @@ -571,7 +602,7 @@ impl<'a> IntoIterator for &'a HeaderMap { } } -/// Iterator for all values with the same header name. +/// Iterator for references of [`HeaderValue`]s with the same associated [`HeaderName`]. /// /// See [`HeaderMap::get_all`]. #[derive(Debug)] @@ -613,18 +644,32 @@ impl<'a> Iterator for GetAll<'a> { } } -/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from methods -/// on [`HeaderMap`] that remove or replace items. +/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from +/// methods that remove or replace items. +/// +/// See [`HeaderMap::insert`] and [`HeaderMap::remove`]. #[derive(Debug)] pub struct Removed { inner: Option>, } -impl<'a> Removed { +impl Removed { fn new(value: Option) -> Self { let inner = value.map(|value| value.inner.into_iter()); Self { inner } } + + /// Returns true if iterator contains no elements, without consuming it. + /// + /// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate + /// wether any items were actually replaced or removed, respectively. + pub fn is_empty(&self) -> bool { + match self.inner { + // size hint lower bound of smallvec is the correct length + Some(ref iter) => iter.size_hint().0 == 0, + None => true, + } + } } impl Iterator for Removed { @@ -945,6 +990,53 @@ mod tests { assert_eq!(vals.next(), removed.next().as_ref()); } + #[test] + fn get_all_iteration_order_matches_insertion_order() { + let mut map = HeaderMap::new(); + + let mut vals = map.get_all(header::COOKIE); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("1")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("2")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert_eq!(vals.next().unwrap().as_bytes(), b"2"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("3")); + map.append(header::COOKIE, HeaderValue::from_static("4")); + map.append(header::COOKIE, HeaderValue::from_static("5")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert_eq!(vals.next().unwrap().as_bytes(), b"2"); + assert_eq!(vals.next().unwrap().as_bytes(), b"3"); + assert_eq!(vals.next().unwrap().as_bytes(), b"4"); + assert_eq!(vals.next().unwrap().as_bytes(), b"5"); + assert!(vals.next().is_none()); + + let _ = map.insert(header::COOKIE, HeaderValue::from_static("6")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"6"); + assert!(vals.next().is_none()); + + let _ = map.insert(header::COOKIE, HeaderValue::from_static("7")); + let _ = map.insert(header::COOKIE, HeaderValue::from_static("8")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"8"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("9")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"8"); + assert_eq!(vals.next().unwrap().as_bytes(), b"9"); + assert!(vals.next().is_none()); + } + fn owned_pair<'a>( (name, val): (&'a HeaderName, &'a HeaderValue), ) -> (HeaderName, HeaderValue) { diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 18494f55..a9483a9f 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -29,16 +29,14 @@ pub use http::header::{ X_FRAME_OPTIONS, X_XSS_PROTECTION, }; -use crate::error::ParseError; -use crate::HttpMessage; +use crate::{error::ParseError, HttpMessage}; mod as_name; mod into_pair; mod into_value; -mod utils; - pub(crate) mod map; mod shared; +mod utils; #[doc(hidden)] pub use self::shared::*; @@ -46,10 +44,10 @@ pub use self::shared::*; pub use self::as_name::AsHeaderName; pub use self::into_pair::IntoHeaderPair; pub use self::into_value::IntoHeaderValue; -#[doc(hidden)] -pub use self::map::GetAll; -pub use self::map::HeaderMap; -pub use self::utils::*; +pub use self::map::{GetAll, HeaderMap, Removed}; +pub use self::utils::{ + fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, +}; /// A trait for any object that already represents a valid header field and value. pub trait Header: IntoHeaderValue { @@ -68,7 +66,7 @@ impl From for HeaderMap { } /// This encode set is used for HTTP header values and is defined at -/// https://tools.ietf.org/html/rfc5987#section-3.2. +/// . pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index 5e965238..cf8636e9 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -56,6 +56,7 @@ where /// Percent encode a sequence of bytes with a character set defined in /// +#[inline] pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); fmt::Display::fmt(&encoded, f) diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index fdcde5ea..8ec04a5c 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -220,7 +220,7 @@ impl> From<(CloseCode, T)> for CloseReason { } } -/// The WebSocket GUID as stated in the spec. See https://tools.ietf.org/html/rfc6455#section-1.3. +/// The WebSocket GUID as stated in the spec. See . static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; /// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. From cf5438853471e27cf8acf8806f2ed4e0f81c2caf Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Mon, 29 Nov 2021 09:23:27 +0800 Subject: [PATCH 81/99] re-work from request macro. (#2469) --- Cargo.toml | 1 + src/extract.rs | 231 +++++++++++++++++++++++++++---------------------- 2 files changed, 130 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbe28df4..96c8fef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ log = "0.4" mime = "0.3" paste = "1" pin-project = "1.0.0" +pin-project-lite = "0.2.7" regex = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/extract.rs b/src/extract.rs index 29fd0d05..bb2dabb9 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -10,6 +10,7 @@ use std::{ use actix_http::http::{Method, Uri}; use actix_utils::future::{ok, Ready}; use futures_core::ready; +use pin_project_lite::pin_project; use crate::{dev::Payload, Error, HttpRequest}; @@ -139,10 +140,11 @@ where } } -#[pin_project::pin_project] -pub struct FromRequestOptFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct FromRequestOptFuture { + #[pin] + fut: Fut, + } } impl Future for FromRequestOptFuture @@ -226,10 +228,11 @@ where } } -#[pin_project::pin_project] -pub struct FromRequestResFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct FromRequestResFuture { + #[pin] + fut: Fut, + } } impl Future for FromRequestResFuture @@ -297,102 +300,104 @@ impl FromRequest for () { } } -macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { - - // This module is a trick to get around the inability of - // `macro_rules!` macros to make new idents. We want to make - // a new `FutWrapper` struct for each distinct invocation of - // this macro. Ideally, we would name it something like - // `FutWrapper_$fut_type`, but this can't be done in a macro_rules - // macro. - // - // Instead, we put everything in a module named `$fut_type`, thus allowing - // us to use the name `FutWrapper` without worrying about conflicts. - // This macro only exists to generate trait impls for tuples - these - // are inherently global, so users don't have to care about this - // weird trick. - #[allow(non_snake_case)] - mod $fut_type { - - // Bring everything into scope, so we don't need - // redundant imports - use super::*; - - /// A helper struct to allow us to pin-project through - /// to individual fields - #[pin_project::pin_project] - struct FutWrapper<$($T: FromRequest),+>($(#[pin] $T::Future),+); - - /// FromRequest implementation for tuple - #[doc(hidden)] - #[allow(unused_parens)] - impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) - { - type Error = Error; - type Future = $fut_type<$($T),+>; - - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - $fut_type { - items: <($(Option<$T>,)+)>::default(), - futs: FutWrapper($($T::from_request(req, payload),)+), - } - } - } - - #[doc(hidden)] - #[pin_project::pin_project] - pub struct $fut_type<$($T: FromRequest),+> { - items: ($(Option<$T>,)+), - #[pin] - futs: FutWrapper<$($T,)+>, - } - - impl<$($T: FromRequest),+> Future for $fut_type<$($T),+> - { - type Output = Result<($($T,)+), Error>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.project(); - - let mut ready = true; - $( - if this.items.$n.is_none() { - match this.futs.as_mut().project().$n.poll(cx) { - Poll::Ready(Ok(item)) => { - this.items.$n = Some(item); - } - Poll::Pending => ready = false, - Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())), - } - } - )+ - - if ready { - Poll::Ready(Ok( - ($(this.items.$n.take().unwrap(),)+) - )) - } else { - Poll::Pending - } - } - } - } -}); - -#[rustfmt::skip] -mod m { +#[doc(hidden)] +#[allow(non_snake_case)] +mod tuple_from_req { use super::*; - tuple_from_req!(TupleFromRequest1, (0, A)); - tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); - tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); - tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); - tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); - tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); + macro_rules! tuple_from_req { + ($fut: ident; $($T: ident),*) => { + /// FromRequest implementation for tuple + #[allow(unused_parens)] + impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) + { + type Error = Error; + type Future = $fut<$($T),+>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + $fut { + $( + $T: ExtractFuture::Future { + fut: $T::from_request(req, payload) + }, + )+ + } + } + } + + pin_project! { + pub struct $fut<$($T: FromRequest),+> { + $( + #[pin] + $T: ExtractFuture<$T::Future, $T>, + )+ + } + } + + impl<$($T: FromRequest),+> Future for $fut<$($T),+> + { + type Output = Result<($($T,)+), Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + let mut ready = true; + $( + match this.$T.as_mut().project() { + ExtractProj::Future { fut } => match fut.poll(cx) { + Poll::Ready(Ok(output)) => { + let _ = this.$T.as_mut().project_replace(ExtractFuture::Done { output }); + }, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())), + Poll::Pending => ready = false, + }, + ExtractProj::Done { .. } => {}, + ExtractProj::Empty => unreachable!("FromRequest polled after finished"), + } + )+ + + if ready { + Poll::Ready(Ok( + ($( + match this.$T.project_replace(ExtractFuture::Empty) { + ExtractReplaceProj::Done { output } => output, + _ => unreachable!("FromRequest polled after finished"), + }, + )+) + )) + } else { + Poll::Pending + } + } + } + }; + } + + pin_project! { + #[project = ExtractProj] + #[project_replace = ExtractReplaceProj] + enum ExtractFuture { + Future { + #[pin] + fut: Fut + }, + Done { + output: Res, + }, + Empty + } + } + + tuple_from_req! { TupleFromRequest1; A } + tuple_from_req! { TupleFromRequest2; A, B } + tuple_from_req! { TupleFromRequest3; A, B, C } + tuple_from_req! { TupleFromRequest4; A, B, C, D } + tuple_from_req! { TupleFromRequest5; A, B, C, D, E } + tuple_from_req! { TupleFromRequest6; A, B, C, D, E, F } + tuple_from_req! { TupleFromRequest7; A, B, C, D, E, F, G } + tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H } + tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I } + tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } } #[cfg(test)] @@ -494,4 +499,26 @@ mod tests { let method = Method::extract(&req).await.unwrap(); assert_eq!(method, Method::GET); } + + #[actix_rt::test] + async fn test_concurrent() { + let (req, mut pl) = TestRequest::default() + .uri("/foo/bar") + .method(Method::GET) + .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) + .insert_header((header::CONTENT_LENGTH, "11")) + .set_payload(Bytes::from_static(b"hello=world")) + .to_http_parts(); + let (method, uri, form) = <(Method, Uri, Form)>::from_request(&req, &mut pl) + .await + .unwrap(); + assert_eq!(method, Method::GET); + assert_eq!(uri.path(), "/foo/bar"); + assert_eq!( + form, + Form(Info { + hello: "world".into() + }) + ); + } } From 654dc64a0925ae47931bcb2d5ba3b9d7bd29cf46 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Mon, 29 Nov 2021 05:00:24 +0300 Subject: [PATCH 82/99] don't hang after dropping mutipart (#2463) --- actix-multipart/CHANGES.md | 2 ++ actix-multipart/src/server.rs | 53 ++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 1ef4aab3..deb99987 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -10,8 +10,10 @@ * Added `Field::name` method for getting the field name. [#2451] * `MultipartError` now marks variants with inner errors as the source. [#2451] * `MultipartError` is now marked as non-exhaustive. [#2451] +* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2451]: https://github.com/actix/actix-web/pull/2451 +[#2463]: https://github.com/actix/actix-web/pull/2463 ## 0.4.0-beta.7 - 2021-10-20 diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index c9642cfa..319e7986 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -706,8 +706,11 @@ impl Clone for PayloadRef { } } -/// Counter. It tracks of number of clones of payloads and give access to payload only to top most -/// task panics if Safety get destroyed and it not top most task. +/// Counter. It tracks of number of clones of payloads and give access to payload only to top most. +/// * When dropped, parent task is awakened. This is to support the case where Field is +/// dropped in a separate task than Multipart. +/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to. +/// * If dropped and is not top most owner, is_clean flag is set to false. #[derive(Debug)] struct Safety { task: LocalWaker, @@ -750,9 +753,9 @@ impl Safety { impl Drop for Safety { fn drop(&mut self) { - // parent task is dead if Rc::strong_count(&self.payload) != self.level { - self.clean.set(true); + // Multipart dropped leaving a Field + self.clean.set(false); } self.task.wake(); @@ -853,10 +856,12 @@ mod tests { use actix_http::h1::Payload; use actix_web::http::header::{DispositionParam, DispositionType}; + use actix_web::rt; use actix_web::test::TestRequest; use actix_web::FromRequest; use bytes::Bytes; use futures_util::{future::lazy, StreamExt}; + use std::time::Duration; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -1286,4 +1291,44 @@ mod tests { MultipartError::NoContentDisposition, )); } + + #[actix_rt::test] + async fn test_drop_multipart_dont_hang() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_simple_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + drop(multipart); + + // should fail immediately + match field.next().await { + Some(Err(MultipartError::NotConsumed)) => {} + _ => panic!(), + }; + } + + #[actix_rt::test] + async fn test_drop_field_awaken_multipart() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_simple_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + let task = rt::spawn(async move { + rt::time::sleep(Duration::from_secs(1)).await; + assert_eq!(field.next().await.unwrap().unwrap(), "test"); + drop(field); + }); + + // dropping field should awaken current task + let _ = multipart.next().await.unwrap().unwrap(); + task.await.unwrap(); + } } From fc4cdf81ebd4137c6df85f6ff9b29aae79a343d9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 29 Nov 2021 02:22:47 +0000 Subject: [PATCH 83/99] expose `header::map` module (#2470) --- actix-http/CHANGES.md | 4 +- actix-http/src/header/as_name.rs | 9 ++-- actix-http/src/header/into_pair.rs | 9 +++- actix-http/src/header/into_value.rs | 6 ++- actix-http/src/header/map.rs | 54 ++++++++++++++++--- actix-http/src/header/mod.rs | 6 +-- .../src/header/shared/content_encoding.rs | 11 ++-- actix-http/src/header/utils.rs | 2 + src/response/response.rs | 2 +- 9 files changed, 79 insertions(+), 24 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index d3aa7fc0..9e004e13 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,9 +3,11 @@ ## Unreleased - 2021-xx-xx ### Changed * Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] -* Expose `header::{GetAll, Removed}` iterators. [#2467] +* Expose `header::map` module. [#2467] +* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] [#2467]: https://github.com/actix/actix-web/pull/2467 +[#2470]: https://github.com/actix/actix-web/pull/2470 ## 3.0.0-beta.13 - 2021-11-22 diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs index 5ce32156..04d32c41 100644 --- a/actix-http/src/header/as_name.rs +++ b/actix-http/src/header/as_name.rs @@ -1,11 +1,12 @@ -//! Helper trait for types that can be effectively borrowed as a [HeaderValue]. -//! -//! [HeaderValue]: crate::http::HeaderValue +//! Sealed [`AsHeaderName`] trait and implementations. -use std::{borrow::Cow, str::FromStr}; +use std::{borrow::Cow, str::FromStr as _}; use http::header::{HeaderName, InvalidHeaderName}; +/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`]. +/// +/// [`HeaderValue`]: crate::http::HeaderValue pub trait AsHeaderName: Sealed {} pub struct Seal; diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs index d0d6e732..47270054 100644 --- a/actix-http/src/header/into_pair.rs +++ b/actix-http/src/header/into_pair.rs @@ -1,4 +1,6 @@ -use std::convert::TryFrom; +//! [`IntoHeaderPair`] trait and implementations. + +use std::convert::TryFrom as _; use http::{ header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, @@ -7,7 +9,10 @@ use http::{ use super::{Header, IntoHeaderValue}; -/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s. +/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for +/// insertion into a [`HeaderMap`]. +/// +/// [`HeaderMap`]: crate::http::HeaderMap pub trait IntoHeaderPair: Sized { type Error: Into; diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs index 4ba58e72..bad05db6 100644 --- a/actix-http/src/header/into_value.rs +++ b/actix-http/src/header/into_value.rs @@ -1,10 +1,12 @@ -use std::convert::TryFrom; +//! [`IntoHeaderValue`] trait and implementations. + +use std::convert::TryFrom as _; use bytes::Bytes; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use mime::Mime; -/// A trait for any object that can be Converted to a `HeaderValue` +/// An interface for types that can be converted into a [`HeaderValue`]. pub trait IntoHeaderValue: Sized { /// The type returned in the event of a conversion error. type Error: Into; diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index c7e4921a..dd852b02 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -1,6 +1,6 @@ //! A multi-value [`HeaderMap`] and its iterators. -use std::{borrow::Cow, collections::hash_map, ops}; +use std::{borrow::Cow, collections::hash_map, iter, ops}; use ahash::AHashMap; use http::header::{HeaderName, HeaderValue}; @@ -581,7 +581,8 @@ impl HeaderMap { } } -/// Note that this implementation will clone a [HeaderName] for each value. +/// Note that this implementation will clone a [HeaderName] for each value. Consider using +/// [`drain`](Self::drain) to control header name cloning. impl IntoIterator for HeaderMap { type Item = (HeaderName, HeaderValue); type IntoIter = IntoIter; @@ -602,7 +603,7 @@ impl<'a> IntoIterator for &'a HeaderMap { } } -/// Iterator for references of [`HeaderValue`]s with the same associated [`HeaderName`]. +/// Iterator over borrowed values with the same associated name. /// /// See [`HeaderMap::get_all`]. #[derive(Debug)] @@ -644,10 +645,14 @@ impl<'a> Iterator for GetAll<'a> { } } -/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from -/// methods that remove or replace items. +impl ExactSizeIterator for GetAll<'_> {} + +impl iter::FusedIterator for GetAll<'_> {} + +/// Iterator over removed, owned values with the same associated name. /// -/// See [`HeaderMap::insert`] and [`HeaderMap::remove`]. +/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] +/// and [`HeaderMap::remove`]. #[derive(Debug)] pub struct Removed { inner: Option>, @@ -689,7 +694,11 @@ impl Iterator for Removed { } } -/// Iterator over all [`HeaderName`]s in the map. +impl ExactSizeIterator for Removed {} + +impl iter::FusedIterator for Removed {} + +/// Iterator over all names in the map. #[derive(Debug)] pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); @@ -707,6 +716,11 @@ impl<'a> Iterator for Keys<'a> { } } +impl ExactSizeIterator for Keys<'_> {} + +impl iter::FusedIterator for Keys<'_> {} + +/// Iterator over borrowed name-value pairs. #[derive(Debug)] pub struct Iter<'a> { inner: hash_map::Iter<'a, HeaderName, Value>, @@ -758,6 +772,10 @@ impl<'a> Iterator for Iter<'a> { } } +impl ExactSizeIterator for Iter<'_> {} + +impl iter::FusedIterator for Iter<'_> {} + /// Iterator over drained name-value pairs. /// /// Iterator items are `(Option, HeaderValue)` to avoid cloning. @@ -809,6 +827,10 @@ impl<'a> Iterator for Drain<'a> { } } +impl ExactSizeIterator for Drain<'_> {} + +impl iter::FusedIterator for Drain<'_> {} + /// Iterator over owned name-value pairs. /// /// Implementation necessarily clones header names for each value. @@ -859,12 +881,27 @@ impl Iterator for IntoIter { } } +impl ExactSizeIterator for IntoIter {} + +impl iter::FusedIterator for IntoIter {} + #[cfg(test)] mod tests { + use std::iter::FusedIterator; + use http::header; + use static_assertions::assert_impl_all; use super::*; + assert_impl_all!(HeaderMap: IntoIterator); + assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator); + #[test] fn create() { let map = HeaderMap::new(); @@ -1035,6 +1072,9 @@ mod tests { assert_eq!(vals.next().unwrap().as_bytes(), b"8"); assert_eq!(vals.next().unwrap().as_bytes(), b"9"); assert!(vals.next().is_none()); + + // check for fused-ness + assert!(vals.next().is_none()); } fn owned_pair<'a>( diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index a9483a9f..125f7ef1 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -34,7 +34,7 @@ use crate::{error::ParseError, HttpMessage}; mod as_name; mod into_pair; mod into_value; -pub(crate) mod map; +pub mod map; mod shared; mod utils; @@ -44,12 +44,12 @@ pub use self::shared::*; pub use self::as_name::AsHeaderName; pub use self::into_pair::IntoHeaderPair; pub use self::into_value::IntoHeaderValue; -pub use self::map::{GetAll, HeaderMap, Removed}; +pub use self::map::HeaderMap; pub use self::utils::{ fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, }; -/// A trait for any object that already represents a valid header field and value. +/// An interface for types that already represent a valid header. pub trait Header: IntoHeaderValue { /// Returns the name of the header field fn name() -> HeaderName; diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index 1af109c0..073d90dc 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -9,14 +9,17 @@ use crate::{ HttpMessage, }; -/// Error return when a content encoding is unknown. -/// -/// Example: 'compress' +/// Error returned when a content encoding is unknown. #[derive(Debug, Display, Error)] #[display(fmt = "unsupported content encoding")] pub struct ContentEncodingParseError; /// Represents a supported content encoding. +/// +/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings. +/// See [IANA HTTP Content Coding Registry]. +/// +/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml #[derive(Debug, Clone, Copy, PartialEq)] #[non_exhaustive] pub enum ContentEncoding { @@ -32,7 +35,7 @@ pub enum ContentEncoding { /// Gzip algorithm. Gzip, - // Zstd algorithm. + /// Zstd algorithm. Zstd, /// Indicates the identity function (i.e. no compression, nor modification). diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index cf8636e9..c40d1cc9 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -1,3 +1,5 @@ +//! Header parsing utilities. + use std::{fmt, str::FromStr}; use super::HeaderValue; diff --git a/src/response/response.rs b/src/response/response.rs index 6475a381..23562ab0 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -296,7 +296,7 @@ impl Future for HttpResponse { #[cfg(feature = "cookies")] pub struct CookieIter<'a> { - iter: header::GetAll<'a>, + iter: header::map::GetAll<'a>, } #[cfg(feature = "cookies")] From fa82b698b765fcec9d0852896cede905d8d753c8 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 30 Nov 2021 19:16:53 +0800 Subject: [PATCH 84/99] remove pin-project from actix-web. (#2471) --- Cargo.toml | 1 - src/middleware/compat.rs | 10 +++-- src/middleware/compress.rs | 21 ++++----- src/middleware/default_headers.rs | 14 +++--- src/middleware/err_handlers.rs | 29 ++++++------ src/middleware/logger.rs | 73 +++++++++++++++---------------- src/types/either.rs | 62 ++++++++++++++------------ 7 files changed, 110 insertions(+), 100 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96c8fef7..c65b0732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,6 @@ once_cell = "1.5" log = "0.4" mime = "0.3" paste = "1" -pin-project = "1.0.0" pin-project-lite = "0.2.7" regex = "1.4" serde = { version = "1.0", features = ["derive"] } diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 752e90f9..a7533598 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -10,6 +10,7 @@ use std::{ use actix_http::body::{AnyBody, MessageBody}; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; +use pin_project_lite::pin_project; use crate::{error::Error, service::ServiceResponse}; @@ -89,10 +90,11 @@ where } } -#[pin_project::pin_project] -pub struct CompatMiddlewareFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct CompatMiddlewareFuture { + #[pin] + fut: Fut, + } } impl Future for CompatMiddlewareFuture diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index d8878a82..3b99fd6b 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -20,7 +20,7 @@ use actix_utils::future::{ok, Either, Ready}; use bytes::Bytes; use futures_core::ready; use once_cell::sync::Lazy; -use pin_project::pin_project; +use pin_project_lite::pin_project; use crate::{ dev::BodyEncoding, @@ -162,15 +162,16 @@ where } } -#[pin_project] -pub struct CompressResponse -where - S: Service, -{ - #[pin] - fut: S::Future, - encoding: ContentEncoding, - _phantom: PhantomData, +pin_project! { + pub struct CompressResponse + where + S: Service, + { + #[pin] + fut: S::Future, + encoding: ContentEncoding, + _phantom: PhantomData, + } } impl Future for CompressResponse diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index d8a947aa..42681024 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -11,6 +11,7 @@ use std::{ use actix_utils::future::{ready, Ready}; use futures_core::ready; +use pin_project_lite::pin_project; use crate::{ dev::{Service, Transform}, @@ -153,12 +154,13 @@ where } } -#[pin_project::pin_project] -pub struct DefaultHeaderFuture, B> { - #[pin] - fut: S::Future, - inner: Rc, - _body: PhantomData, +pin_project! { + pub struct DefaultHeaderFuture, B> { + #[pin] + fut: S::Future, + inner: Rc, + _body: PhantomData, + } } impl Future for DefaultHeaderFuture diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 75cc819b..1a834c1e 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -10,6 +10,7 @@ use std::{ use actix_service::{Service, Transform}; use ahash::AHashMap; use futures_core::{future::LocalBoxFuture, ready}; +use pin_project_lite::pin_project; use crate::{ dev::{ServiceRequest, ServiceResponse}, @@ -130,19 +131,21 @@ where } } -#[pin_project::pin_project(project = ErrorHandlersProj)] -pub enum ErrorHandlersFuture -where - Fut: Future, -{ - ServiceFuture { - #[pin] - fut: Fut, - handlers: Handlers, - }, - HandlerFuture { - fut: LocalBoxFuture<'static, Fut::Output>, - }, +pin_project! { + #[project = ErrorHandlersProj] + pub enum ErrorHandlersFuture + where + Fut: Future, + { + ServiceFuture { + #[pin] + fut: Fut, + handlers: Handlers, + }, + HandlerFuture { + fut: LocalBoxFuture<'static, Fut::Output>, + }, + } } impl Future for ErrorHandlersFuture diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index b4d100b3..6ab16a4e 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -13,10 +13,11 @@ use std::{ }; use actix_service::{Service, Transform}; -use actix_utils::future::{ok, Ready}; +use actix_utils::future::{ready, Ready}; use bytes::Bytes; use futures_core::ready; use log::{debug, warn}; +use pin_project_lite::pin_project; use regex::{Regex, RegexSet}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; @@ -180,8 +181,8 @@ where { type Response = ServiceResponse>; type Error = Error; - type InitError = (); type Transform = LoggerMiddleware; + type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { @@ -195,10 +196,10 @@ where } } - ok(LoggerMiddleware { + ready(Ok(LoggerMiddleware { service, inner: self.0.clone(), - }) + })) } } @@ -246,17 +247,18 @@ where } } -#[pin_project::pin_project] -pub struct LoggerResponse -where - B: MessageBody, - S: Service, -{ - #[pin] - fut: S::Future, - time: OffsetDateTime, - format: Option, - _phantom: PhantomData, +pin_project! { + pub struct LoggerResponse + where + B: MessageBody, + S: Service, + { + #[pin] + fut: S::Future, + time: OffsetDateTime, + format: Option, + _phantom: PhantomData, + } } impl Future for LoggerResponse @@ -296,28 +298,25 @@ where } } -use pin_project::{pin_project, pinned_drop}; - -#[pin_project(PinnedDrop)] -pub struct StreamLog { - #[pin] - body: B, - format: Option, - size: usize, - time: OffsetDateTime, -} - -#[pinned_drop] -impl PinnedDrop for StreamLog { - fn drop(self: Pin<&mut Self>) { - if let Some(ref format) = self.format { - let render = |fmt: &mut fmt::Formatter<'_>| { - for unit in &format.0 { - unit.render(fmt, self.size, self.time)?; - } - Ok(()) - }; - log::info!("{}", FormatDisplay(&render)); +pin_project! { + pub struct StreamLog { + #[pin] + body: B, + format: Option, + size: usize, + time: OffsetDateTime, + } + impl PinnedDrop for StreamLog { + fn drop(this: Pin<&mut Self>) { + if let Some(ref format) = this.format { + let render = |fmt: &mut fmt::Formatter<'_>| { + for unit in &format.0 { + unit.render(fmt, this.size, this.time)?; + } + Ok(()) + }; + log::info!("{}", FormatDisplay(&render)); + } } } } diff --git a/src/types/either.rs b/src/types/either.rs index 5700b63c..0a8a9013 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -9,6 +9,7 @@ use std::{ use bytes::Bytes; use futures_core::ready; +use pin_project_lite::pin_project; use crate::{ dev, @@ -198,37 +199,40 @@ where } } -#[pin_project::pin_project] -pub struct EitherExtractFut -where - R: FromRequest, - L: FromRequest, -{ - req: HttpRequest, - #[pin] - state: EitherExtractState, +pin_project! { + pub struct EitherExtractFut + where + R: FromRequest, + L: FromRequest, + { + req: HttpRequest, + #[pin] + state: EitherExtractState, + } } -#[pin_project::pin_project(project = EitherExtractProj)] -pub enum EitherExtractState -where - L: FromRequest, - R: FromRequest, -{ - Bytes { - #[pin] - bytes: ::Future, - }, - Left { - #[pin] - left: L::Future, - fallback: Bytes, - }, - Right { - #[pin] - right: R::Future, - left_err: Option, - }, +pin_project! { + #[project = EitherExtractProj] + pub enum EitherExtractState + where + L: FromRequest, + R: FromRequest, + { + Bytes { + #[pin] + bytes: ::Future, + }, + Left { + #[pin] + left: L::Future, + fallback: Bytes, + }, + Right { + #[pin] + right: R::Future, + left_err: Option, + }, + } } impl Future for EitherExtractFut From a978b417f38716859343fb128a91ab6e8aa8d32c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 30 Nov 2021 13:11:41 +0000 Subject: [PATCH 85/99] use actix ready future in remaining return types --- actix-files/Cargo.toml | 3 ++- actix-files/src/path_buf.rs | 2 +- awc/src/client/connector.rs | 6 ++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index f0ae00b9..16c6e98d 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -24,7 +24,8 @@ experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false } actix-http = "3.0.0-beta.13" -actix-service = "2.0.0" +actix-service = "2" +actix-utils = "3" askama_escape = "0.10" bitflags = "1" diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 8c8bca6c..0e0d4f51 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -1,9 +1,9 @@ use std::{ - future::{ready, Ready}, path::{Path, PathBuf}, str::FromStr, }; +use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, FromRequest, HttpRequest}; use crate::error::UriSegmentError; diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 54778d31..be53437b 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -286,12 +286,10 @@ where } #[cfg(feature = "dangerous-h2c")] { - use std::{ - future::{ready, Ready}, - io, - }; + use std::io; use actix_tls::connect::Connection; + use actix_utils::future::{ready, Ready}; impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { From e045418038a9674628d6fc5bb90d6c779826c556 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 30 Nov 2021 14:12:04 +0000 Subject: [PATCH 86/99] prepare for actix-tls rc.1 (#2474) --- CHANGES.md | 7 ++ Cargo.toml | 15 +++- README.md | 4 +- actix-files/Cargo.toml | 2 +- actix-http-test/CHANGES.md | 6 ++ actix-http-test/Cargo.toml | 6 +- actix-http-test/README.md | 4 +- actix-http/CHANGES.md | 5 ++ actix-http/Cargo.toml | 6 +- actix-http/README.md | 4 +- actix-http/src/h1/service.rs | 7 +- actix-http/src/h2/service.rs | 7 +- actix-http/src/service.rs | 7 +- actix-http/tests/test_rustls.rs | 2 +- actix-multipart/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/CHANGES.md | 6 ++ awc/Cargo.toml | 10 +-- awc/README.md | 4 +- awc/src/builder.rs | 8 +- awc/src/client/connector.rs | 141 +++++++++++++++++--------------- awc/src/client/error.rs | 4 +- awc/src/client/mod.rs | 2 +- awc/src/lib.rs | 4 +- awc/tests/test_client.rs | 2 +- awc/tests/test_rustls_client.rs | 2 +- scripts/bump | 111 +++++++++++++++++++++++++ scripts/ci-test.sh | 16 ++++ src/error/response_error.rs | 2 +- src/server.rs | 10 +-- 31 files changed, 296 insertions(+), 114 deletions(-) create mode 100755 scripts/bump create mode 100755 scripts/ci-test.sh diff --git a/CHANGES.md b/CHANGES.md index 290d1dca..00bf85f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,13 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.13 - 2021-11-30 +### Changed +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + ## 4.0.0-beta.12 - 2021-11-22 ### Changed * Compress middleware's response type is now `AnyBody>`. [#2448] diff --git a/Cargo.toml b/Cargo.toml index c65b0732..425bdbbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.12" +version = "4.0.0-beta.13" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -75,9 +75,9 @@ actix-rt = "2.3" actix-server = "2.0.0-beta.9" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true } +actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" actix-router = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.5" @@ -143,6 +143,15 @@ actix-web-actors = { path = "actix-web-actors" } actix-web-codegen = { path = "actix-web-codegen" } awc = { path = "awc" } +# uncomment for quick testing against local actix-net repo +# actix-service = { path = "../actix-net/actix-service" } +# actix-macros = { path = "../actix-net/actix-macros" } +# actix-rt = { path = "../actix-net/actix-rt" } +# actix-codec = { path = "../actix-net/actix-codec" } +# actix-utils = { path = "../actix-net/actix-utils" } +# actix-tls = { path = "../actix-net/actix-tls" } +# actix-server = { path = "../actix-net/actix-server" } + [[test]] name = "test_server" required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] diff --git a/README.md b/README.md index cc68b509..c363ece9 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.12)](https://docs.rs/actix-web/4.0.0-beta.12) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.13)](https://docs.rs/actix-web/4.0.0-beta.13) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.12) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.13)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 16c6e98d..6b6d6d24 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -23,7 +23,7 @@ experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false } -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" actix-service = "2" actix-utils = "3" diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 37de57d4..6984e596 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.8 - 2021-11-30 +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + ## 3.0.0-beta.7 - 2021-11-22 * Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index cfc32b52..8d347d4e 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.7" +version = "3.0.0-beta.8" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -31,7 +31,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.1" -actix-tls = "3.0.0-beta.9" +actix-tls = "3.0.0-rc.1" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.9" @@ -52,4 +52,4 @@ tokio = { version = "1.2", features = ["sync"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 057cf6a1..c3e99d25 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http-test/3.0.0-beta.7) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http-test/3.0.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.7) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.8) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9e004e13..797cde99 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,13 +1,18 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.14 - 2021-11-30 ### Changed * Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] * Expose `header::map` module. [#2467] * Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2467]: https://github.com/actix/actix-web/pull/2467 [#2470]: https://github.com/actix/actix-web/pull/2470 +[#2474]: https://github.com/actix/actix-web/pull/2474 ## 3.0.0-beta.13 - 2021-11-22 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 1c7cd198..f8b15df7 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.13" +version = "3.0.0-beta.14" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -73,7 +73,7 @@ sha-1 = "0.9" smallvec = "1.6.1" # tls -actix-tls = { version = "3.0.0-beta.9", default-features = false, optional = true } +actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } @@ -83,7 +83,7 @@ zstd = { version = "0.9", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.9" actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.9", features = ["openssl"] } +actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.9" diff --git a/actix-http/README.md b/actix-http/README.md index d1f451e3..92b86d2a 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.13)](https://docs.rs/actix-http/3.0.0-beta.13) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.14)](https://docs.rs/actix-http/3.0.0-beta.14) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.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.13/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.13) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.14/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.14) [![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/h1/service.rs b/actix-http/src/h1/service.rs index 970c45ef..8a50417d 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -103,7 +103,10 @@ mod openssl { use super::*; use actix_tls::accept::{ - openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, TlsError, }; @@ -164,7 +167,7 @@ mod rustls { use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - rustls::{Acceptor, ServerConfig, TlsStream}, + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, TlsError, }; diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 794397bf..79874023 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -103,7 +103,10 @@ where mod openssl { use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, TlsError, }; @@ -151,7 +154,7 @@ mod rustls { use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - rustls::{Acceptor, ServerConfig, TlsStream}, + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, TlsError, }; diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index aa3e54a8..a47dda73 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -197,7 +197,10 @@ where mod openssl { use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, TlsError, }; @@ -270,7 +273,7 @@ mod rustls { use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - rustls::{Acceptor, ServerConfig, TlsStream}, + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, TlsError, }; diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 320c9ad9..b5289bf7 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -20,7 +20,7 @@ use actix_http::{ }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; -use actix_tls::connect::tls::rustls::webpki_roots_cert_store; +use actix_tls::connect::rustls::webpki_roots_cert_store; use actix_utils::future::{err, ok}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 5376ba12..7554feeb 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -28,7 +28,7 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index afbbd0cc..dcaa3e9a 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -29,7 +29,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" actix-http-test = "3.0.0-beta.7" actix-service = "2.0.0" actix-utils = "3.0.0" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 9712e065..e7b8cd0f 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.1" -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 788dce05..ab3362b7 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.12 - 2021-11-30 +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + ## 3.0.0-beta.11 - 2021-11-22 * No significant changes from `3.0.0-beta.10`. diff --git a/awc/Cargo.toml b/awc/Cargo.toml index a8d0e3e3..851e5cd4 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.11" +version = "3.0.0-beta.12" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -60,9 +60,9 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" -actix-http = "3.0.0-beta.13" +actix-http = "3.0.0-beta.14" actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0-beta.9", features = ["connect"] } +actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] } actix-utils = "3.0.0" ahash = "0.7" @@ -94,11 +94,11 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.13", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.14", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.9" -actix-tls = { version = "3.0.0-beta.9", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } brotli2 = "0.3.2" diff --git a/awc/README.md b/awc/README.md index 7070337d..b0faedc6 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.11)](https://docs.rs/awc/3.0.0-beta.11) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.12)](https://docs.rs/awc/3.0.0-beta.12) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.11/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.11) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.12/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.12) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/builder.rs b/awc/src/builder.rs index fda7d93a..70a28c41 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -5,7 +5,7 @@ use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; use crate::{ - client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}, + client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, connect::DefaultConnector, error::SendRequestError, middleware::{NestTransform, Redirect, Transform}, @@ -33,7 +33,7 @@ impl ClientBuilder { #[allow(clippy::new_ret_no_self)] pub fn new() -> ClientBuilder< impl Service< - TcpConnect, + ConnectInfo, Response = TcpConnection, Error = TcpConnectError, > + Clone, @@ -56,7 +56,7 @@ impl ClientBuilder { impl ClientBuilder where - S: Service, Response = TcpConnection, Error = TcpConnectError> + S: Service, Response = TcpConnection, Error = TcpConnectError> + Clone + 'static, Io: ActixStream + fmt::Debug + 'static, @@ -65,7 +65,7 @@ where pub fn connector(self, connector: Connector) -> ClientBuilder where S1: Service< - TcpConnect, + ConnectInfo, Response = TcpConnection, Error = TcpConnectError, > + Clone diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index be53437b..40b3c4d3 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -15,8 +15,8 @@ use actix_rt::{ }; use actix_service::Service; use actix_tls::connect::{ - new_connector, Connect as TcpConnect, ConnectError as TcpConnectError, - Connection as TcpConnection, Resolver, + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, + Connector as TcpConnector, Resolver, }; use futures_core::{future::LocalBoxFuture, ready}; use http::Uri; @@ -28,13 +28,15 @@ use super::error::ConnectError; use super::pool::ConnectionPool; use super::Connect; -enum SslConnector { - #[allow(dead_code)] +enum OurTlsConnector { + #[allow(dead_code)] // only dead when no TLS feature is enabled None, + #[cfg(feature = "openssl")] - Openssl(actix_tls::connect::ssl::openssl::SslConnector), + Openssl(actix_tls::connect::openssl::reexports::SslConnector), + #[cfg(feature = "rustls")] - Rustls(std::sync::Arc), + Rustls(std::sync::Arc), } /// Manages HTTP client network connectivity. @@ -53,21 +55,22 @@ enum SslConnector { pub struct Connector { connector: T, config: ConnectorConfig, - #[allow(dead_code)] - ssl: SslConnector, + + #[allow(dead_code)] // only dead when no TLS feature is enabled + ssl: OurTlsConnector, } impl Connector<()> { #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] pub fn new() -> Connector< impl Service< - TcpConnect, + ConnectInfo, Response = TcpConnection, Error = actix_tls::connect::ConnectError, > + Clone, > { Connector { - connector: new_connector(resolver::resolver()), + connector: TcpConnector::new(resolver::resolver()).service(), config: ConnectorConfig::default(), ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), } @@ -75,16 +78,16 @@ impl Connector<()> { /// Provides an empty TLS connector when no TLS feature is enabled. #[cfg(not(any(feature = "openssl", feature = "rustls")))] - fn build_ssl(_: Vec>) -> SslConnector { - SslConnector::None + fn build_ssl(_: Vec>) -> OurTlsConnector { + OurTlsConnector::None } /// Build TLS connector with rustls, based on supplied ALPN protocols /// /// Note that if both `openssl` and `rustls` features are enabled, rustls will be used. #[cfg(feature = "rustls")] - fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::tls::rustls::{webpki_roots_cert_store, ClientConfig}; + fn build_ssl(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls::{reexports::ClientConfig, webpki_roots_cert_store}; let mut config = ClientConfig::builder() .with_safe_defaults() @@ -93,13 +96,13 @@ impl Connector<()> { config.alpn_protocols = protocols; - SslConnector::Rustls(std::sync::Arc::new(config)) + OurTlsConnector::Rustls(std::sync::Arc::new(config)) } /// Build TLS connector with openssl, based on supplied ALPN protocols #[cfg(all(feature = "openssl", not(feature = "rustls")))] - fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod}; + fn build_ssl(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::openssl::reexports::{SslConnector, SslMethod}; use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); @@ -108,12 +111,12 @@ impl Connector<()> { alpn.put(proto.as_slice()); } - let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); if let Err(err) = ssl.set_alpn_protos(&alpn) { log::error!("Can not set ALPN protocol: {:?}", err); } - SslConnector::Openssl(ssl.build()) + OurTlsConnector::Openssl(ssl.build()) } } @@ -123,7 +126,7 @@ impl Connector { where Io1: ActixStream + fmt::Debug + 'static, S1: Service< - TcpConnect, + ConnectInfo, Response = TcpConnection, Error = TcpConnectError, > + Clone, @@ -136,7 +139,7 @@ impl Connector { } } -impl Connector +impl Connector where // Note: // Input Io type is bound to ActixStream trait but internally in client module they @@ -145,8 +148,8 @@ where // // This remap is to hide ActixStream's trait methods. They are not meant to be called // from user code. - Io: ActixStream + fmt::Debug + 'static, - S: Service, Response = TcpConnection, Error = TcpConnectError> + IO: ActixStream + fmt::Debug + 'static, + S: Service, Response = TcpConnection, Error = TcpConnectError> + Clone + 'static, { @@ -166,18 +169,21 @@ where #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. - pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self { - self.ssl = SslConnector::Openssl(connector); + pub fn ssl( + mut self, + connector: actix_tls::connect::openssl::reexports::SslConnector, + ) -> Self { + self.ssl = OurTlsConnector::Openssl(connector); self } #[cfg(feature = "rustls")] - /// Use custom `SslConnector` instance. + /// Use custom `ClientConfig` instance. pub fn rustls( mut self, - connector: std::sync::Arc, + connector: std::sync::Arc, ) -> Self { - self.ssl = SslConnector::Rustls(connector); + self.ssl = OurTlsConnector::Rustls(connector); self } @@ -266,7 +272,7 @@ where /// Finish configuration process and create connector service. /// The Connector builder always concludes by calling `finish()` last in /// its combinator chain. - pub fn finish(self) -> ConnectorService { + pub fn finish(self) -> ConnectorService { let local_address = self.config.local_address; let timeout = self.config.timeout; @@ -279,11 +285,12 @@ where }; let tls_service = match self.ssl { - SslConnector::None => { + OurTlsConnector::None => { #[cfg(not(feature = "dangerous-h2c"))] { None } + #[cfg(feature = "dangerous-h2c")] { use std::io; @@ -305,17 +312,17 @@ where #[derive(Clone)] struct NoOpTlsConnectorService; - impl Service> for NoOpTlsConnectorService + impl Service> for NoOpTlsConnectorService where - U: ActixStream + 'static, + IO: ActixStream + 'static, { - type Response = Connection>; + type Response = Connection>; type Error = io::Error; type Future = Ready>; actix_service::always_ready!(); - fn call(&self, connection: Connection) -> Self::Future { + fn call(&self, connection: Connection) -> Self::Future { let (io, connection) = connection.replace_io(()); let (_, connection) = connection.replace_io(Box::new(io) as _); @@ -334,13 +341,14 @@ where Some(actix_service::boxed::rc_service(tls_service)) } } + #[cfg(feature = "openssl")] - SslConnector::Openssl(tls) => { + OurTlsConnector::Openssl(tls) => { const H2: &[u8] = b"h2"; - use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream}; + use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector}; - impl IntoConnectionIo for TcpConnection> { + impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; let h2 = sock @@ -359,19 +367,20 @@ where let tls_service = TlsConnectorService { tcp_service: tcp_service_inner, - tls_service: OpensslConnector::service(tls), + tls_service: TlsConnector::service(tls), timeout: handshake_timeout, }; Some(actix_service::boxed::rc_service(tls_service)) } + #[cfg(feature = "rustls")] - SslConnector::Rustls(tls) => { + OurTlsConnector::Rustls(tls) => { const H2: &[u8] = b"h2"; - use actix_tls::connect::ssl::rustls::{RustlsConnector, TlsStream}; + use actix_tls::connect::rustls::{reexports::AsyncTlsStream, TlsConnector}; - impl IntoConnectionIo for TcpConnection> { + impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; let h2 = sock @@ -391,7 +400,7 @@ where let tls_service = TlsConnectorService { tcp_service: tcp_service_inner, - tls_service: RustlsConnector::service(tls), + tls_service: TlsConnector::service(tls), timeout: handshake_timeout, }; @@ -460,26 +469,28 @@ where /// service for establish tcp connection and do client tls handshake. /// operation is canceled when timeout limit reached. -struct TlsConnectorService { - /// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting. - tcp_service: S, - /// tls connection is canceled on `TlsConnectorService`'s timeout setting. - tls_service: St, +struct TlsConnectorService { + /// TCP connection is canceled on `TcpConnectorInnerService`'s timeout setting. + tcp_service: Tcp, + + /// TLS connection is canceled on `TlsConnectorService`'s timeout setting. + tls_service: Tls, + timeout: Duration, } -impl Service for TlsConnectorService +impl Service for TlsConnectorService where - S: Service, Error = ConnectError> + Tcp: Service, Error = ConnectError> + Clone + 'static, - St: Service, Error = std::io::Error> + Clone + 'static, - Io: ConnectionIo, - St::Response: IntoConnectionIo, + Tls: Service, Error = std::io::Error> + Clone + 'static, + Tls::Response: IntoConnectionIo, + IO: ConnectionIo, { type Response = (Box, Protocol); type Error = ConnectError; - type Future = TlsConnectorFuture; + type Future = TlsConnectorFuture; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { ready!(self.tcp_service.poll_ready(cx))?; @@ -579,7 +590,7 @@ impl TcpConnectorInnerService { impl Service for TcpConnectorInnerService where - S: Service, Response = TcpConnection, Error = TcpConnectError> + S: Service, Response = TcpConnection, Error = TcpConnectError> + Clone + 'static, { @@ -590,7 +601,7 @@ where actix_service::forward_ready!(service); fn call(&self, req: Connect) -> Self::Future { - let mut req = TcpConnect::new(req.uri).set_addr(req.addr); + let mut req = ConnectInfo::new(req.uri).set_addr(req.addr); if let Some(local_addr) = self.local_address { req = req.set_local_addr(local_addr); @@ -629,8 +640,8 @@ where } /// Connector service for pooled Plain/Tls Tcp connections. -pub type ConnectorService = ConnectorServicePriv< - TcpConnectorService>, +pub type ConnectorService = ConnectorServicePriv< + TcpConnectorService>, Rc< dyn Service< Connect, @@ -642,7 +653,7 @@ pub type ConnectorService = ConnectorServicePriv< >, >, >, - Io, + IO, Box, >; @@ -741,7 +752,7 @@ mod resolver { use super::*; pub(super) fn resolver() -> Resolver { - Resolver::Default + Resolver::default() } } @@ -783,8 +794,7 @@ mod resolver { } } - // dns struct is cached in thread local. - // so new client constructor can reuse the existing dns resolver. + // resolver struct is cached in thread local so new clients can reuse the existing instance thread_local! { static TRUST_DNS_RESOLVER: RefCell> = RefCell::new(None); } @@ -792,8 +802,10 @@ mod resolver { // get from thread local or construct a new trust-dns resolver. TRUST_DNS_RESOLVER.with(|local| { let resolver = local.borrow().as_ref().map(Clone::clone); + match resolver { Some(resolver) => resolver, + None => { let (cfg, opts) = match read_system_conf() { Ok((cfg, opts)) => (cfg, opts), @@ -806,8 +818,9 @@ mod resolver { let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap(); // box trust dns resolver and put it in thread local. - let resolver = Resolver::new_custom(TrustDnsResolver(resolver)); + let resolver = Resolver::custom(TrustDnsResolver(resolver)); *local.borrow_mut() = Some(resolver.clone()); + resolver } } @@ -838,9 +851,9 @@ mod tests { .await; let connector = Connector { - connector: new_connector(resolver::resolver()), + connector: TcpConnector::new(resolver::resolver()).service(), config: ConnectorConfig::default(), - ssl: SslConnector::None, + ssl: OurTlsConnector::None, }; let client = Client::builder().connector(connector).finish(); diff --git a/awc/src/client/error.rs b/awc/src/client/error.rs index 0f3b1fde..68ffb6fb 100644 --- a/awc/src/client/error.rs +++ b/awc/src/client/error.rs @@ -7,7 +7,7 @@ use actix_http::{ http::Error as HttpError, }; #[cfg(feature = "openssl")] -use actix_tls::accept::openssl::SslError; +use actix_tls::accept::openssl::reexports::Error as OpenSslError; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] @@ -20,7 +20,7 @@ pub enum ConnectError { /// SSL error #[cfg(feature = "openssl")] #[display(fmt = "{}", _0)] - SslError(SslError), + SslError(OpenSslError), /// Failed to resolve the hostname #[display(fmt = "Failed resolving hostname: {}", _0)] diff --git a/awc/src/client/mod.rs b/awc/src/client/mod.rs index 3abbf50a..0d5c899b 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -11,7 +11,7 @@ mod h2proto; mod pool; pub use actix_tls::connect::{ - Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection, + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, }; pub use self::connection::{Connection, ConnectionIo}; diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 05f97aa3..fc99419e 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -137,7 +137,7 @@ use actix_http::{ use actix_rt::net::TcpStream; use actix_service::Service; -use self::client::{TcpConnect, TcpConnectError, TcpConnection}; +use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; /// An asynchronous HTTP and WebSocket client. /// @@ -186,7 +186,7 @@ impl Client { /// This function is equivalent of `ClientBuilder::new()`. pub fn builder() -> ClientBuilder< impl Service< - TcpConnect, + ConnectInfo, Response = TcpConnection, Error = TcpConnectError, > + Clone, diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 856a4ace..5abb63e3 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -127,7 +127,7 @@ async fn test_timeout() { }); let connector = awc::Connector::new() - .connector(actix_tls::connect::default_connector()) + .connector(actix_tls::connect::ConnectorService::default()) .timeout(Duration::from_secs(15)); let client = awc::Client::builder() diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 355fcb6f..652997de 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -14,7 +14,7 @@ use std::{ use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; -use actix_tls::connect::tls::rustls::webpki_roots_cert_store; +use actix_tls::connect::rustls::webpki_roots_cert_store; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ diff --git a/scripts/bump b/scripts/bump new file mode 100755 index 00000000..8b6a3c42 --- /dev/null +++ b/scripts/bump @@ -0,0 +1,111 @@ +#!/bin/sh + +# developed on macOS and probably doesn't work on Linux yet due to minor +# differences in flags on sed + +# requires github cli tool for automatic release draft creation + +set -euo pipefail + +DIR=$1 + +LINUX="" +MACOS="" + +if [ "$(uname)" = "Darwin" ]; then + MACOS="1" +fi + +CARGO_MANIFEST=$DIR/Cargo.toml +CHANGELOG_FILE=$DIR/CHANGES.md +README_FILE=$DIR/README.md + +# get current version +PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" +CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")" + +CHANGE_CHUNK_FILE="$(mktemp)" +echo saving changelog to $CHANGE_CHUNK_FILE +echo + +# get changelog chunk and save to temp file +cat "$CHANGELOG_FILE" | + # skip up to unreleased heading + sed '1,/Unreleased/ d' | + # take up to previous version heading + sed "/$CURRENT_VERSION/ q" | + # drop last line + sed '$d' \ + >"$CHANGE_CHUNK_FILE" + +# if word count of changelog chunk is 0 then insert filler changelog chunk +if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then + echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" +fi + +if [ -n "${2-}" ]; then + NEW_VERSION="$2" +else + echo + echo "--- Changes since $CURRENT_VERSION ----" + cat "$CHANGE_CHUNK_FILE" + echo + read -p "Update version to: " NEW_VERSION +fi + +DATE="$(date -u +"%Y-%m-%d")" +echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)" + +# update package.version field +sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_MANIFEST" + +# update readme +[ -f "$README_FILE" ] && sed -i.bak -E "s#$CURRENT_VERSION([/)])#$NEW_VERSION\1#g" "$README_FILE" + +# update changelog file +( + sed '/Unreleased/ q' "$CHANGELOG_FILE" # up to unreleased heading + echo # blank line + echo # blank line + echo "## $NEW_VERSION - $DATE" # new version heading + cat "$CHANGE_CHUNK_FILE" # previously unreleased changes + sed "/$CURRENT_VERSION/ q" "$CHANGELOG_FILE" | tail -n 1 # the previous version heading + sed "1,/$CURRENT_VERSION/ d" "$CHANGELOG_FILE" # everything after previous version heading +) >"$CHANGELOG_FILE.bak" +mv "$CHANGELOG_FILE.bak" "$CHANGELOG_FILE" + +# done; remove backup files +rm -f $CARGO_MANIFEST.bak +rm -f $CHANGELOG_FILE.bak +rm -f $README_FILE.bak + +echo "manifest, changelog, and readme updated" +echo +echo "check other references:" +rg "$PACKAGE_NAME =" || true +rg "package = \"$PACKAGE_NAME\"" || true + +if [ $MACOS ]; then + printf "prepare $PACKAGE_NAME release $NEW_VERSION" | pbcopy +else + echo + echo "commit message:" + echo "prepare $PACKAGE_NAME release $NEW_VERSION" +fi + +SHORT_PACKAGE_NAME="$(echo $PACKAGE_NAME | sed 's/^actix-web-//' | sed 's/^actix-//')" +GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" +RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" + +echo +echo "GitHub release command:" +echo "gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" --prerelease" + +read -p "Submit draft GH release: (y/N) " GH_RELEASE +GH_RELEASE="${GH_RELEASE:-n}" + +if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then + gh release create "$GIT_TAG" --draft --title "$RELEASE_TITLE" --notes-file "$CHANGE_CHUNK_FILE" --prerelease +fi + +echo diff --git a/scripts/ci-test.sh b/scripts/ci-test.sh new file mode 100755 index 00000000..096eb760 --- /dev/null +++ b/scripts/ci-test.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# run tests matching what CI does for non-linux feature sets + +set -x + +cargo test --lib --tests -p=actix-router --all-features +cargo test --lib --tests -p=actix-http --all-features +cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls +cargo test --lib --tests -p=actix-web-codegen --all-features +cargo test --lib --tests -p=awc --all-features +cargo test --lib --tests -p=actix-http-test --all-features +cargo test --lib --tests -p=actix-test --all-features +cargo test --lib --tests -p=actix-files +cargo test --lib --tests -p=actix-multipart --all-features +cargo test --lib --tests -p=actix-web-actors --all-features diff --git a/src/error/response_error.rs b/src/error/response_error.rs index c3c54341..2c34be3c 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -49,7 +49,7 @@ downcast_dyn!(ResponseError); impl ResponseError for Box {} #[cfg(feature = "openssl")] -impl ResponseError for actix_tls::accept::openssl::SslError {} +impl ResponseError for actix_tls::accept::openssl::reexports::Error {} impl ResponseError for serde::de::value::Error { fn status_code(&self) -> StatusCode { diff --git a/src/server.rs b/src/server.rs index 0f3d7c59..1bf56655 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,9 +15,9 @@ use actix_service::{ }; #[cfg(feature = "openssl")] -use actix_tls::accept::openssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; +use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; #[cfg(feature = "rustls")] -use actix_tls::accept::rustls::ServerConfig as RustlsServerConfig; +use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; use crate::{config::AppConfig, Error}; @@ -108,11 +108,11 @@ where /// [Extensions] container so that request-local data can be passed to middleware and handlers. /// /// For example: - /// - `actix_tls::openssl::SslStream` when using openssl. - /// - `actix_tls::rustls::TlsStream` when using rustls. + /// - `actix_tls::accept::openssl::TlsStream` when using openssl. + /// - `actix_tls::accept::rustls::TlsStream` when using rustls. /// - `actix_web::rt::net::TcpStream` when no encryption is used. /// - /// See `on_connect` example for additional details. + /// See the `on_connect` example for additional details. pub fn on_connect(self, f: CB) -> HttpServer where CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static, From 697238fadca3b77c414eda013b1e812a932fad8c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 1 Dec 2021 00:26:07 +0000 Subject: [PATCH 87/99] prepare actix-multipart release 0.4.0-beta.9 --- actix-multipart/CHANGES.md | 8 ++++++-- actix-multipart/Cargo.toml | 2 +- actix-multipart/README.md | 4 ++-- scripts/{ci-test.sh => ci-test} | 0 4 files changed, 9 insertions(+), 5 deletions(-) rename scripts/{ci-test.sh => ci-test} (100%) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index deb99987..d9ded57a 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.9 - 2021-12-01 +* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] + +[#2463]: https://github.com/actix/actix-web/pull/2463 + + ## 0.4.0-beta.8 - 2021-11-22 * Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] * Added `MultipartError::NoContentDisposition` variant. [#2451] @@ -10,10 +16,8 @@ * Added `Field::name` method for getting the field name. [#2451] * `MultipartError` now marks variants with inner errors as the source. [#2451] * `MultipartError` is now marked as non-exhaustive. [#2451] -* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2451]: https://github.com/actix/actix-web/pull/2451 -[#2463]: https://github.com/actix/actix-web/pull/2463 ## 0.4.0-beta.7 - 2021-10-20 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 7554feeb..04a1d75e 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.8" +version = "0.4.0-beta.9" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 75379629..85c78c5f 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.8)](https://docs.rs/actix-multipart/0.4.0-beta.8) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.9)](https://docs.rs/actix-multipart/0.4.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.8/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.9/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/scripts/ci-test.sh b/scripts/ci-test similarity index 100% rename from scripts/ci-test.sh rename to scripts/ci-test From 0df275c478ac2fbdce508c6d06b00bde64e217a2 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 1 Dec 2021 19:42:02 +0000 Subject: [PATCH 88/99] update all IETF RFC links to new URL format --- actix-http/src/header/shared/charset.rs | 9 +- actix-http/src/header/shared/extended.rs | 16 ++-- actix-http/src/header/shared/quality_item.rs | 2 +- actix-http/src/header/utils.rs | 2 + actix-http/src/ws/proto.rs | 6 +- awc/src/ws.rs | 5 +- src/http/header/accept.rs | 8 +- src/http/header/accept_charset.rs | 7 +- src/http/header/accept_encoding.rs | 9 +- src/http/header/allow.rs | 14 +-- src/http/header/cache_control.rs | 6 +- src/http/header/content_disposition.rs | 91 +++++++++++--------- src/http/header/content_language.rs | 11 +-- src/http/header/content_range.rs | 15 ++-- src/http/header/content_type.rs | 11 +-- src/http/header/date.rs | 9 +- src/http/header/entity.rs | 10 ++- src/http/header/etag.rs | 10 +-- src/http/header/expires.rs | 8 +- src/http/header/if_match.rs | 11 +-- src/http/header/if_modified_since.rs | 9 +- src/http/header/if_none_match.rs | 11 +-- src/http/header/if_range.rs | 7 +- src/http/header/if_unmodified_since.rs | 10 +-- src/http/header/last_modified.rs | 16 ++-- src/http/header/range.rs | 33 +++---- src/service.rs | 1 - 27 files changed, 163 insertions(+), 184 deletions(-) diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs index 3cc0f3e2..109c02bd 100644 --- a/actix-http/src/header/shared/charset.rs +++ b/actix-http/src/header/shared/charset.rs @@ -1,9 +1,8 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; +use std::{fmt, str}; use self::Charset::*; -/// A Mime charset. +/// A MIME character set. /// /// The string representation is normalized to upper case. /// @@ -95,13 +94,13 @@ impl Charset { } } -impl Display for Charset { +impl fmt::Display for Charset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } -impl FromStr for Charset { +impl str::FromStr for Charset { type Err = crate::Error; fn from_str(s: &str) -> Result { diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 9fd4cdfb..3820b1db 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -24,8 +24,8 @@ pub struct ExtendedValue { pub value: Vec, } -/// Parses extended header parameter values (`ext-value`), as defined in -/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// Parses extended header parameter values (`ext-value`), as defined +/// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). /// /// Extended values are denoted by parameter names that end with `*`. /// @@ -34,7 +34,7 @@ pub struct ExtendedValue { /// ```text /// ext-value = charset "'" [ language ] "'" value-chars /// ; like RFC 2231's -/// ; (see [RFC2231], Section 7) +/// ; (see [RFC 2231 §7]) /// /// charset = "UTF-8" / "ISO-8859-1" / mime-charset /// @@ -43,22 +43,26 @@ pub struct ExtendedValue { /// / "!" / "#" / "$" / "%" / "&" /// / "+" / "-" / "^" / "_" / "`" /// / "{" / "}" / "~" -/// ; as in Section 2.3 of [RFC2978] +/// ; as in [RFC 2978 §2.3] /// ; except that the single quote is not included /// ; SHOULD be registered in the IANA charset registry /// -/// language = +/// language = /// /// value-chars = *( pct-encoded / attr-char ) /// /// pct-encoded = "%" HEXDIG HEXDIG -/// ; see [RFC3986], Section 2.1 +/// ; see [RFC 3986 §2.1] /// /// attr-char = ALPHA / DIGIT /// / "!" / "#" / "$" / "&" / "+" / "-" / "." /// / "^" / "_" / "`" / "|" / "~" /// ; token except ( "*" / "'" / "%" ) /// ``` +/// +/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7 +/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3 +/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 pub fn parse_extended_value( val: &str, ) -> Result { diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 431e9fb3..60a562df 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -224,7 +224,7 @@ mod tests { } } - impl FromStr for Encoding { + impl str::FromStr for Encoding { type Err = crate::error::ParseError; fn from_str(s: &str) -> Result { use Encoding::*; diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index c40d1cc9..f4fec933 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -13,8 +13,10 @@ where T: FromStr, { let mut result = Vec::new(); + for h in all { let s = h.to_str().map_err(|_| ParseError::Header)?; + result.extend( s.split(',') .filter_map(|x| match x.trim() { diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 8ec04a5c..75068e23 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -3,7 +3,9 @@ use std::{ fmt, }; -/// Operation codes as part of RFC6455. +/// Operation codes defined in [RFC 6455 §11.8]. +/// +/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum OpCode { /// Indicates a continuation frame of a fragmented message. @@ -105,7 +107,7 @@ pub enum CloseCode { Abnormal, /// Indicates that an endpoint is terminating the connection because it has received data within - /// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\] + /// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC 3629\] /// data within a text message). Invalid, diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 2fe36399..e2f1f86d 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -312,9 +312,8 @@ impl WebsocketsRequest { ); } - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) + // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded + // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). let sec_key: [u8; 16] = rand::random(); let key = base64::encode(&sec_key); diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 75366dfa..68676ba3 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -6,7 +6,8 @@ use super::{qitem, QualityItem}; use crate::http::header; crate::http::header::common_header! { - /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) + /// `Accept` header, defined + /// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) /// /// The `Accept` header field can be used by user agents to specify /// response media types that are acceptable. Accept header fields can @@ -15,7 +16,6 @@ crate::http::header::common_header! { /// in-line image /// /// # ABNF - /// /// ```text /// Accept = #( media-range [ accept-params ] ) /// @@ -27,7 +27,7 @@ crate::http::header::common_header! { /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] /// ``` /// - /// # Example values + /// # Example Values /// * `audio/*; q=0.2, audio/basic` /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` /// @@ -79,7 +79,7 @@ crate::http::header::common_header! { /// ``` (Accept, header::ACCEPT) => (QualityItem)+ - test_accept { + test_parse_and_format { // Tests from the RFC crate::http::header::common_header_test!( test1, diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index bb7d8651..fb21c5ac 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -2,7 +2,7 @@ use super::{Charset, QualityItem, ACCEPT_CHARSET}; crate::http::header::common_header! { /// `Accept-Charset` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) + /// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3) /// /// The `Accept-Charset` header field can be sent by a user agent to /// indicate what charsets are acceptable in textual response content. @@ -12,12 +12,11 @@ crate::http::header::common_header! { /// those charsets. /// /// # ABNF - /// /// ```text /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) /// ``` /// - /// # Example values + /// # Example Values /// * `iso-8859-5, unicode-1-1;q=0.8` /// /// # Examples @@ -55,7 +54,7 @@ crate::http::header::common_header! { /// ``` (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ - test_accept_charset { + test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index cfd29bf7..f7375a1e 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -1,8 +1,8 @@ use header::{Encoding, QualityItem}; header! { - /// `Accept-Encoding` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) + /// `Accept-Encoding` header, defined + /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// /// The `Accept-Encoding` header field can be used by user agents to /// indicate what response content-codings are @@ -11,13 +11,12 @@ header! { /// preferred. /// /// # ABNF - /// /// ```text /// Accept-Encoding = #( codings [ weight ] ) /// codings = content-coding / "identity" / "*" /// ``` /// - /// # Example values + /// # Example Values /// * `compress, gzip` /// * `` /// * `*` @@ -62,7 +61,7 @@ header! { /// ``` (AcceptEncoding, "Accept-Encoding") => (QualityItem)* - test_accept_encoding { + test_parse_and_format { // From the RFC crate::http::header::common_header_test!(test1, vec![b"compress, gzip"]); crate::http::header::common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); diff --git a/src/http/header/allow.rs b/src/http/header/allow.rs index 946f70e0..2546ce3a 100644 --- a/src/http/header/allow.rs +++ b/src/http/header/allow.rs @@ -1,27 +1,27 @@ -use crate::http::header; use actix_http::http::Method; +use crate::http::header; + crate::http::header::common_header! { - /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) + /// `Allow` header, defined + /// in [RFC 7231 §7.4.1](https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1) /// /// The `Allow` header field lists the set of methods advertised as - /// supported by the target resource. The purpose of this field is + /// supported by the target resource. The purpose of this field is /// strictly to inform the recipient of valid request methods associated /// with the resource. /// /// # ABNF - /// /// ```text /// Allow = #method /// ``` /// - /// # Example values + /// # Example Values /// * `GET, HEAD, PUT` /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` /// * `` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::{header::Allow, Method}; @@ -47,7 +47,7 @@ crate::http::header::common_header! { /// ``` (Allow, header::ALLOW) => (Method)* - test_allow { + test_parse_and_format { // From the RFC crate::http::header::common_header_test!( test1, diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs index 05903e3a..c5ac9e79 100644 --- a/src/http/header/cache_control.rs +++ b/src/http/header/cache_control.rs @@ -5,7 +5,8 @@ use super::{fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, use crate::http::header; -/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) +/// `Cache-Control` header, defined +/// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2). /// /// The `Cache-Control` header field is used to specify directives for /// caches along the request/response chain. Such cache directives are @@ -13,13 +14,12 @@ use crate::http::header; /// not imply that the same directive is to be given in the response. /// /// # ABNF -/// /// ```text /// Cache-Control = 1#cache-directive /// cache-directive = token [ "=" ( token / quoted-string ) ] /// ``` /// -/// # Example values +/// # Example Values /// /// * `no-cache` /// * `private, community="UCI"` diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 6d07a41b..439c995a 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -1,10 +1,14 @@ -//! # References +//! The `Content-Disposition` header and associated types. //! -//! "The Content-Disposition Header Field" -//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" -//! "Returning Values from Forms: multipart/form-data" -//! Browser conformance tests at: -//! IANA assignment: +//! # References +//! - "The Content-Disposition Header Field": +//! +//! - "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)": +//! +//! - "Returning Values from Forms: multipart/form-data": +//! +//! - Browser conformance tests at: +//! - IANA assignment: use once_cell::sync::Lazy; use regex::Regex; @@ -41,8 +45,9 @@ pub enum DispositionType { /// rather than process it normally (as per its media type). Attachment, - /// Used in *multipart/form-data* as defined in [RFC7578](https://tools.ietf.org/html/rfc7578) - /// to carry the field name and optional filename. + /// Used in *multipart/form-data* as defined in + /// [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) to carry the field name and + /// optional filename. FormData, /// Extension type. Should be handled by recipients the same way as Attachment. @@ -82,26 +87,29 @@ pub enum DispositionParam { /// A plain file name. /// - /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any - /// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where + /// It is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to contain + /// any non-ASCII characters when used in a *Content-Disposition* HTTP response header, where /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// in case there are Unicode characters in file names. Filename(String), /// An extended file name. It must not exist for `ContentType::Formdata` according to - /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). + /// [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2). FilenameExt(ExtendedValue), /// An unrecognized regular parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should - /// ignore unrecognizable parameters. + /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as + /// `reg-parameter`, in + /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as + /// `token "=" value`. Recipients should ignore unrecognizable parameters. Unknown(String, String), /// An unrecognized extended parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailing asterisk is not included. Recipients should ignore unrecognizable parameters. + /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as + /// `ext-parameter`, in + /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as + /// `ext-token "=" ext-value`. The single trailing asterisk is not included. Recipients should + /// ignore unrecognizable parameters. UnknownExt(String, ExtendedValue), } @@ -195,10 +203,10 @@ impl DispositionParam { } /// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) -/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as +/// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) +/// as (re)defined in [RFC 6266](https://tools.ietf.org/html/rfc6266), or as /// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). +/// as (re)defined in [RFC 7587](https://tools.ietf.org/html/rfc7578). /// /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as @@ -233,19 +241,17 @@ impl DispositionParam { /// ``` /// /// # Note +/// *filename* is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to +/// contain any non-ASCII characters when used in a *Content-Disposition* HTTP response header, +/// where filename* with charset UTF-8 may be used instead in case there are Unicode characters in +/// file names. Filename is [acceptable](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) +/// to be UTF-8 encoded directly in a *Content-Disposition* header for +/// *multipart/form-data*, though. /// -/// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any -/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where -/// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file -/// names. -/// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded -/// directly in a *Content-Disposition* header for *multipart/form-data*, though. -/// -/// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within +/// *filename* [must not](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) be used within /// *multipart/form-data*. /// -/// # Example -/// +/// # Examples /// ``` /// use actix_web::http::header::{ /// Charset, ContentDisposition, DispositionParam, DispositionType, @@ -291,10 +297,9 @@ impl DispositionParam { /// ``` /// /// # Security Note -/// /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). +/// information that may be present. See [RFC 2183](https://tools.ietf.org/html/rfc2183#section-2.3). // TODO: private fields and use smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { @@ -342,7 +347,7 @@ impl ContentDisposition { } else { // regular parameters let value = if left.starts_with('\"') { - // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 + // quoted-string: defined in RFC 6266 -> RFC 2616 Section 3.6 let mut escaping = false; let mut quoted_string = vec![]; let mut end = None; @@ -393,22 +398,22 @@ impl ContentDisposition { Ok(cd) } - /// Returns `true` if it is [`Inline`](DispositionType::Inline). + /// Returns `true` if type is [`Inline`](DispositionType::Inline). pub fn is_inline(&self) -> bool { matches!(self.disposition, DispositionType::Inline) } - /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). + /// Returns `true` if type is [`Attachment`](DispositionType::Attachment). pub fn is_attachment(&self) -> bool { matches!(self.disposition, DispositionType::Attachment) } - /// Returns `true` if it is [`FormData`](DispositionType::FormData). + /// Returns `true` if type is [`FormData`](DispositionType::FormData). pub fn is_form_data(&self) -> bool { matches!(self.disposition, DispositionType::FormData) } - /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. + /// Returns `true` if type is [`Ext`](DispositionType::Ext) and the `disp_type` matches. pub fn is_ext(&self, disp_type: impl AsRef) -> bool { matches!( self.disposition, @@ -487,7 +492,9 @@ impl fmt::Display for DispositionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and // backslash should be escaped in quoted-string (i.e. "foobar"). - // Ref: RFC6266 S4.1 -> RFC2616 S3.6 + // + // Ref: RFC 6266 §4.1 -> RFC 2616 §3.6 + // // filename-parm = "filename" "=" value // value = token | quoted-string // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) @@ -501,7 +508,7 @@ impl fmt::Display for DispositionParam { // CTL = // - // Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1 + // Ref: RFC 7578 S4.2 -> RFC 2183 S2 -> RFC 2045 S5.1 // parameter := attribute "=" value // attribute := token // ; Matching of attributes @@ -746,7 +753,7 @@ mod tests { #[test] fn from_raw_with_unicode() { - /* RFC7578 Section 4.2: + /* RFC 7578 Section 4.2: Some commonly deployed systems use multipart/form-data with file names directly encoded including octets outside the US-ASCII range. The encoding used for the file names is typically UTF-8, although HTML forms will use the charset associated with the form. @@ -819,9 +826,9 @@ mod tests { #[test] fn test_from_raw_unnecessary_percent_decode() { - // In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with + // In fact, RFC 7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with // non-ASCII characters MAY be percent-encoded. - // On the contrary, RFC6266 or other RFCs related to Content-Disposition response header + // On the contrary, RFC 6266 or other RFCs related to Content-Disposition response header // do not mention such percent-encoding. // So, it appears to be undecidable whether to percent-decode or not without // knowing the usage scenario (multipart/form-data v.s. HTTP response header) and diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs index 604ada83..0f428ad3 100644 --- a/src/http/header/content_language.rs +++ b/src/http/header/content_language.rs @@ -2,8 +2,8 @@ use super::{QualityItem, CONTENT_LANGUAGE}; use language_tags::LanguageTag; crate::http::header::common_header! { - /// `Content-Language` header, defined in - /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) + /// `Content-Language` header, defined + /// in [RFC 7231 §3.1.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.3.2) /// /// The `Content-Language` header field describes the natural language(s) /// of the intended audience for the representation. Note that this @@ -11,18 +11,15 @@ crate::http::header::common_header! { /// representation. /// /// # ABNF - /// /// ```text /// Content-Language = 1#language-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `da` /// * `mi, en` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem}; @@ -49,7 +46,7 @@ crate::http::header::common_header! { /// ``` (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ - test_content_language { + test_parse_and_format { crate::http::header::common_header_test!(test1, vec![b"da"]); crate::http::header::common_header_test!(test2, vec![b"mi, en"]); } diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index 3bdead2c..56b7f76b 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -1,15 +1,17 @@ -use std::fmt::{self, Display, Write}; -use std::str::FromStr; +use std::{ + fmt::{self, Display, Write}, + str::FromStr, +}; use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; use crate::error::ParseError; crate::http::header::common_header! { - /// `Content-Range` header, defined in - /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) + /// `Content-Range` header, defined + /// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] - test_content_range { + test_parse_and_format { crate::http::header::common_header_test!(test_bytes, vec![b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { @@ -69,10 +71,9 @@ crate::http::header::common_header! { } } -/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// Content-Range, described in [RFC 7233](https://tools.ietf.org/html/rfc7233#section-4.2) /// /// # ABNF -/// /// ```text /// Content-Range = byte-content-range /// / other-content-range diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index 23046000..624a5171 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -2,8 +2,8 @@ use super::CONTENT_TYPE; use mime::Mime; crate::http::header::common_header! { - /// `Content-Type` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) + /// `Content-Type` header, defined + /// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5) /// /// The `Content-Type` header field indicates the media type of the /// associated representation: either the representation enclosed in the @@ -18,18 +18,15 @@ crate::http::header::common_header! { /// this is an issue, it's possible to implement `Header` on a custom struct. /// /// # ABNF - /// /// ```text /// Content-Type = media-type /// ``` /// - /// # Example values - /// + /// # Example Values /// * `text/html; charset=utf-8` /// * `application/json` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::ContentType; @@ -51,7 +48,7 @@ crate::http::header::common_header! { /// ``` (ContentType, CONTENT_TYPE) => [Mime] - test_content_type { + test_parse_and_format { crate::http::header::common_header_test!( test1, vec![b"text/html"], diff --git a/src/http/header/date.rs b/src/http/header/date.rs index 4d171788..08c9b7ed 100644 --- a/src/http/header/date.rs +++ b/src/http/header/date.rs @@ -2,19 +2,18 @@ use super::{HttpDate, DATE}; use std::time::SystemTime; crate::http::header::common_header! { - /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) + /// `Date` header, defined + /// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) /// /// The `Date` header field represents the date and time at which the /// message was originated. /// /// # ABNF - /// /// ```text /// Date = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Tue, 15 Nov 1994 08:12:31 GMT` /// /// # Example @@ -31,7 +30,7 @@ crate::http::header::common_header! { /// ``` (Date, DATE) => [HttpDate] - test_date { + test_parse_and_format { crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } diff --git a/src/http/header/entity.rs b/src/http/header/entity.rs index 5073ed69..ff8e1728 100644 --- a/src/http/header/entity.rs +++ b/src/http/header/entity.rs @@ -1,5 +1,7 @@ -use std::fmt::{self, Display, Write}; -use std::str::FromStr; +use std::{ + fmt::{self, Display, Write}, + str::FromStr, +}; use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; @@ -15,7 +17,8 @@ fn check_slice_validity(slice: &str) -> bool { slice.bytes().all(entity_validate_char) } -/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) +/// An entity tag, defined +/// in [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) /// /// An entity tag consists of a string enclosed by two literal double quotes. /// Preceding the first double quote is an optional weakness indicator, @@ -23,7 +26,6 @@ fn check_slice_validity(slice: &str) -> bool { /// `W/"xyzzy"`. /// /// # ABNF -/// /// ```text /// entity-tag = [ weak ] opaque-tag /// weak = %x57.2F ; "W/", case-sensitive diff --git a/src/http/header/etag.rs b/src/http/header/etag.rs index aded7266..11206407 100644 --- a/src/http/header/etag.rs +++ b/src/http/header/etag.rs @@ -1,7 +1,8 @@ use super::{EntityTag, ETAG}; crate::http::header::common_header! { - /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) + /// `ETag` header, defined in + /// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) /// /// The `ETag` header field in a response provides the current entity-tag /// for the selected representation, as determined at the conclusion of @@ -14,19 +15,16 @@ crate::http::header::common_header! { /// prefixed by a weakness indicator. /// /// # ABNF - /// /// ```text /// ETag = entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `""` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{ETag, EntityTag}; @@ -48,7 +46,7 @@ crate::http::header::common_header! { /// ``` (ETag, ETAG) => [EntityTag] - test_etag { + test_parse_and_format { // From the RFC crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""], diff --git a/src/http/header/expires.rs b/src/http/header/expires.rs index e810fe26..7ff78be8 100644 --- a/src/http/header/expires.rs +++ b/src/http/header/expires.rs @@ -1,7 +1,8 @@ use super::{HttpDate, EXPIRES}; crate::http::header::common_header! { - /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) + /// `Expires` header, defined + /// in [RFC 7234 §5.3](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3) /// /// The `Expires` header field gives the date/time after which the /// response is considered stale. @@ -11,12 +12,11 @@ crate::http::header::common_header! { /// time. /// /// # ABNF - /// /// ```text /// Expires = HTTP-date /// ``` /// - /// # Example values + /// # Example Values /// * `Thu, 01 Dec 1994 16:00:00 GMT` /// /// # Example @@ -34,7 +34,7 @@ crate::http::header::common_header! { /// ``` (Expires, EXPIRES) => [HttpDate] - test_expires { + test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); } diff --git a/src/http/header/if_match.rs b/src/http/header/if_match.rs index 87a94a80..ac06fa87 100644 --- a/src/http/header/if_match.rs +++ b/src/http/header/if_match.rs @@ -1,8 +1,8 @@ use super::{EntityTag, IF_MATCH}; crate::http::header::common_header! { - /// `If-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) + /// `If-Match` header, defined + /// in [RFC 7232 §3.1](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1) /// /// The `If-Match` header field makes the request method conditional on /// the recipient origin server either having at least one current @@ -17,18 +17,15 @@ crate::http::header::common_header! { /// there have been any changes to the representation data. /// /// # ABNF - /// /// ```text /// If-Match = "*" / 1#entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * "xyzzy", "r2d2xxxx", "c3piozzzz" /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::IfMatch; @@ -52,7 +49,7 @@ crate::http::header::common_header! { /// ``` (IfMatch, IF_MATCH) => {Any / (EntityTag)+} - test_if_match { + test_parse_and_format { crate::http::header::common_header_test!( test1, vec![b"\"xyzzy\""], diff --git a/src/http/header/if_modified_since.rs b/src/http/header/if_modified_since.rs index 25400352..0d23be18 100644 --- a/src/http/header/if_modified_since.rs +++ b/src/http/header/if_modified_since.rs @@ -1,8 +1,8 @@ use super::{HttpDate, IF_MODIFIED_SINCE}; crate::http::header::common_header! { - /// `If-Modified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) + /// `If-Modified-Since` header, defined + /// in [RFC 7232 §3.3](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3) /// /// The `If-Modified-Since` header field makes a GET or HEAD request /// method conditional on the selected representation's modification date @@ -11,12 +11,11 @@ crate::http::header::common_header! { /// data has not changed. /// /// # ABNF - /// /// ```text /// If-Unmodified-Since = HTTP-date /// ``` /// - /// # Example values + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example @@ -34,7 +33,7 @@ crate::http::header::common_header! { /// ``` (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] - test_if_modified_since { + test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } diff --git a/src/http/header/if_none_match.rs b/src/http/header/if_none_match.rs index e1422bd3..80f87ed7 100644 --- a/src/http/header/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -1,8 +1,8 @@ use super::{EntityTag, IF_NONE_MATCH}; crate::http::header::common_header! { - /// `If-None-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) + /// `If-None-Match` header, defined + /// in [RFC 7232 §3.2](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2) /// /// The `If-None-Match` header field makes the request method conditional /// on a recipient cache or origin server either not having any current @@ -16,13 +16,11 @@ crate::http::header::common_header! { /// the representation data. /// /// # ABNF - /// /// ```text /// If-None-Match = "*" / 1#entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` @@ -30,7 +28,6 @@ crate::http::header::common_header! { /// * `*` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::IfNoneMatch; @@ -54,7 +51,7 @@ crate::http::header::common_header! { /// ``` (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} - test_if_none_match { + test_parse_and_format { crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]); crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]); crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs index cf69e726..9a51ab3a 100644 --- a/src/http/header/if_range.rs +++ b/src/http/header/if_range.rs @@ -8,7 +8,8 @@ use crate::error::ParseError; use crate::http::header; use crate::HttpMessage; -/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) +/// `If-Range` header, defined +/// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) /// /// If a client has a partial copy of a representation and wishes to have /// an up-to-date copy of the entire representation, it could use the @@ -24,18 +25,16 @@ use crate::HttpMessage; /// in Range; otherwise, send me the entire representation. /// /// # ABNF -/// /// ```text /// If-Range = entity-tag / HTTP-date /// ``` /// -/// # Example values +/// # Example Values /// /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// * `\"xyzzy\"` /// /// # Examples -/// /// ``` /// use actix_web::HttpResponse; /// use actix_web::http::header::{EntityTag, IfRange}; diff --git a/src/http/header/if_unmodified_since.rs b/src/http/header/if_unmodified_since.rs index 1cc7b304..d0498682 100644 --- a/src/http/header/if_unmodified_since.rs +++ b/src/http/header/if_unmodified_since.rs @@ -1,8 +1,8 @@ use super::{HttpDate, IF_UNMODIFIED_SINCE}; crate::http::header::common_header! { - /// `If-Unmodified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) + /// `If-Unmodified-Since` header, defined + /// in [RFC 7232 §3.4](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4) /// /// The `If-Unmodified-Since` header field makes the request method /// conditional on the selected representation's last modification date @@ -11,13 +11,11 @@ crate::http::header::common_header! { /// the user agent does not have an entity-tag for the representation. /// /// # ABNF - /// /// ```text /// If-Unmodified-Since = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example @@ -35,7 +33,7 @@ crate::http::header::common_header! { /// ``` (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] - test_if_unmodified_since { + test_parse_and_format { // Test case from RFC crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } diff --git a/src/http/header/last_modified.rs b/src/http/header/last_modified.rs index c43bf3ac..ce5c829c 100644 --- a/src/http/header/last_modified.rs +++ b/src/http/header/last_modified.rs @@ -1,8 +1,8 @@ use super::{HttpDate, LAST_MODIFIED}; crate::http::header::common_header! { - /// `Last-Modified` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) + /// `Last-Modified` header, defined + /// in [RFC 7232 §2.2](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2) /// /// The `Last-Modified` header field in a response provides a timestamp /// indicating the date and time at which the origin server believes the @@ -10,13 +10,11 @@ crate::http::header::common_header! { /// conclusion of handling the request. /// /// # ABNF - /// /// ```text /// Expires = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example @@ -34,8 +32,8 @@ crate::http::header::common_header! { /// ``` (LastModified, LAST_MODIFIED) => [HttpDate] - test_last_modified { - // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } + test_parse_and_format { + // Test case from RFC + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + } } diff --git a/src/http/header/range.rs b/src/http/header/range.rs index a9b40b40..4c2ceee6 100644 --- a/src/http/header/range.rs +++ b/src/http/header/range.rs @@ -4,15 +4,14 @@ use std::str::FromStr; use super::parsing::from_one_raw_str; use super::{Header, Raw}; -/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) +/// `Range` header, defined +/// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) /// -/// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more sub-ranges of the -/// selected representation data, rather than the entire selected +/// The "Range" header field on a GET request modifies the method semantics to request transfer of +/// only one or more sub-ranges of the selected representation data, rather than the entire selected /// representation data. /// /// # ABNF -/// /// ```text /// Range = byte-ranges-specifier / other-ranges-specifier /// other-ranges-specifier = other-range-unit "=" other-range-set @@ -27,8 +26,7 @@ use super::{Header, Raw}; /// last-byte-pos = 1*DIGIT /// ``` /// -/// # Example values -/// +/// # Example Values /// * `bytes=1000-` /// * `bytes=-2000` /// * `bytes=0-1,30-40` @@ -37,7 +35,6 @@ use super::{Header, Raw}; /// * `custom_unit=xxx-yyy` /// /// # Examples -/// /// ``` /// use hyper::header::{Headers, Range, ByteRangeSpec}; /// @@ -63,6 +60,7 @@ use super::{Header, Raw}; pub enum Range { /// Byte range Bytes(Vec), + /// Custom range, with unit not registered at IANA /// (`other-range-unit`: String , `other-range-set`: String) Unregistered(String, String), @@ -74,8 +72,10 @@ pub enum Range { pub enum ByteRangeSpec { /// Get all bytes between x and y ("x-y") FromTo(u64, u64), + /// Get all bytes starting from x ("x-") AllFrom(u64), + /// Get last x bytes ("-x") Last(u64), } @@ -238,9 +238,7 @@ impl FromStr for ByteRangeSpec { .or(Err(::Error::Header)) .map(ByteRangeSpec::AllFrom), (Some(start), Some(end)) => match (start.parse(), end.parse()) { - (Ok(start), Ok(end)) if start <= end => { - Ok(ByteRangeSpec::FromTo(start, end)) - } + (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)), _ => Err(::Error::Header), }, _ => Err(::Error::Header), @@ -248,16 +246,6 @@ impl FromStr for ByteRangeSpec { } } -fn from_comma_delimited(s: &str) -> Vec { - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }) - .filter_map(|x| x.parse().ok()) - .collect() -} - impl Header for Range { fn header_name() -> &'static str { static NAME: &'static str = "Range"; @@ -286,8 +274,7 @@ mod tests { assert_eq!(r2, r3); let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200), diff --git a/src/service.rs b/src/service.rs index 515d782d..8ba38df4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -561,7 +561,6 @@ where /// The max number of services can be grouped together is 12. /// /// # Examples -/// /// ``` /// use actix_web::{services, web, App}; /// From c4b20df56a8e769dade97bf0582a0462bd1d0b21 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 2 Dec 2021 03:45:04 +0000 Subject: [PATCH 89/99] convert all remaining IETF RFC links to new format --- actix-http/src/h1/decoder.rs | 2 +- actix-http/src/h1/encoder.rs | 7 ++++--- actix-http/src/h2/dispatcher.rs | 2 +- actix-http/src/header/mod.rs | 2 +- actix-http/src/header/shared/extended.rs | 6 +++--- actix-http/src/header/shared/quality_item.rs | 8 ++++---- actix-http/src/header/utils.rs | 2 +- actix-http/src/ws/proto.rs | 3 ++- actix-multipart/src/error.rs | 2 +- actix-multipart/src/server.rs | 4 ++-- awc/src/client/h1proto.rs | 3 +-- awc/src/client/h2proto.rs | 2 +- src/http/header/accept.rs | 4 ++-- src/http/header/accept_language.rs | 4 ++-- src/http/header/content_disposition.rs | 9 +++++---- src/http/header/content_range.rs | 3 ++- src/http/header/range.rs | 4 ++-- 17 files changed, 35 insertions(+), 32 deletions(-) diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 91a3af44..f25c35a7 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -174,7 +174,7 @@ pub(crate) trait MessageType: Sized { self.set_expect() } - // https://tools.ietf.org/html/rfc7230#section-3.3.3 + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 if chunked { // Chunked encoding Ok(PayloadLength::Payload(PayloadType::Payload( diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index cc26a200..60880cd7 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -71,15 +71,16 @@ pub(crate) trait MessageType: Sized { | StatusCode::PROCESSING | StatusCode::NO_CONTENT => { // skip content-length and transfer-encoding headers - // see https://tools.ietf.org/html/rfc7230#section-3.3.1 - // and https://tools.ietf.org/html/rfc7230#section-3.3.2 + // see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1 + // and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 skip_len = true; length = BodySize::None } StatusCode::NOT_MODIFIED => { // 304 responses should never have a body but should retain a manually set - // content-length header see https://tools.ietf.org/html/rfc7232#section-4.1 + // content-length header + // see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 skip_len = false; length = BodySize::None; } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 8b922b2c..8efd3e83 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -304,7 +304,7 @@ fn prepare_response( for (key, value) in head.headers.iter() { match *key { // TODO: consider skipping other headers according to: - // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // omit HTTP/1.x only headers CONNECTION | TRANSFER_ENCODING => continue, CONTENT_LENGTH if skip_len => continue, diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 125f7ef1..721f4ea7 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -66,7 +66,7 @@ impl From for HeaderMap { } /// This encode set is used for HTTP header values and is defined at -/// . +/// . pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 3820b1db..5ff5c8b3 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -1,17 +1,17 @@ +// Originally from hyper v0.11.27 src/header/parsing.rs + use std::{fmt, str::FromStr}; use language_tags::LanguageTag; use crate::header::{Charset, HTTP_VALUE}; -// From hyper v0.11.27 src/header/parsing.rs - /// The value part of an extended parameter consisting of three parts: /// - The REQUIRED character set name (`charset`). /// - The OPTIONAL language information (`language_tag`). /// - A character sequence representing the actual value (`value`), separated by single quotes. /// -/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). #[derive(Clone, Debug, PartialEq)] pub struct ExtendedValue { /// The character set that is used to encode the `value` to a string. diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 60a562df..ad1f6877 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -26,8 +26,8 @@ const MAX_FLOAT_QUALITY: f32 = 1.0; /// a value between 0 and 1000 e.g. `Quality(532)` matches the quality /// `q=0.532`. /// -/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) -/// gives more information on quality values in HTTP header fields. +/// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more +/// information on quality values in HTTP header fields. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Quality(u16); @@ -79,8 +79,8 @@ impl TryFrom for Quality { } } -/// Represents an item with a quality value as defined in -/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). +/// Represents an item with a quality value as defined +/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). #[derive(Clone, PartialEq, Debug)] pub struct QualityItem { /// The actual contents of the field. diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index f4fec933..94c5b6dc 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -59,7 +59,7 @@ where } /// Percent encode a sequence of bytes with a character set defined in -/// +/// #[inline] pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 75068e23..4227f221 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -222,7 +222,8 @@ impl> From<(CloseCode, T)> for CloseReason { } } -/// The WebSocket GUID as stated in the spec. See . +/// The WebSocket GUID as stated in the spec. +/// See . static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; /// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index de4594b8..7d0da35e 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -10,7 +10,7 @@ use derive_more::{Display, Error, From}; pub enum MultipartError { /// Content-Disposition header is not found or is not equal to "form-data". /// - /// According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a + /// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a /// Content-Disposition header must always be present and equal to "form-data". #[display(fmt = "No Content-Disposition `form-data` header")] NoContentDisposition, diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 319e7986..23397b7e 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -337,8 +337,8 @@ impl InnerMultipart { return Poll::Pending; }; - // According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a - // Content-Disposition header must always be present and set to "form-data". + // According to RFC 7578 §4.2, a Content-Disposition header must always be present and + // set to "form-data". let content_disposition = headers .get(&header::CONTENT_DISPOSITION) diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 7f3ba1b6..c442cd4c 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -66,8 +66,7 @@ where let mut framed = Framed::new(io, h1::ClientCodec::default()); // Check EXPECT header and enable expect handle flag accordingly. - // - // RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1 + // See https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1 let is_expect = if head.as_ref().headers.contains_key(EXPECT) { match body.size() { BodySize::None | BodySize::Sized(0) => { diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 2618e190..66fb790a 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -90,7 +90,7 @@ where for (key, value) in headers { match *key { // TODO: consider skipping other headers according to: - // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 // omit HTTP/1.x only headers CONNECTION | TRANSFER_ENCODING => continue, CONTENT_LENGTH if skip_len => continue, diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 68676ba3..3c4d5f59 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -153,7 +153,7 @@ impl Accept { /// Returns a sorted list of mime types from highest to lowest preference, accounting for /// [q-factor weighting] and specificity. /// - /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn mime_precedence(&self) -> Vec { let mut types = self.0.clone(); @@ -203,7 +203,7 @@ impl Accept { /// /// Returns `None` if contained list is empty. /// - /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn mime_preference(&self) -> Option { let types = self.mime_precedence(); types.first().cloned() diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index 1552f657..a7b60e36 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -3,8 +3,8 @@ use language_tags::LanguageTag; use super::{QualityItem, ACCEPT_LANGUAGE}; crate::http::header::common_header! { - /// `Accept-Language` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) + /// `Accept-Language` header, defined + /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) /// /// The `Accept-Language` header field can be used by user agents to /// indicate the set of natural languages that are preferred in the diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 439c995a..79fdb765 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -204,9 +204,9 @@ impl DispositionParam { /// A *Content-Disposition* header. It is compatible to be used either as /// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) -/// as (re)defined in [RFC 6266](https://tools.ietf.org/html/rfc6266), or as +/// as (re)defined in [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266), or as /// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC 7587](https://tools.ietf.org/html/rfc7578). +/// as (re)defined in [RFC 7587](https://datatracker.ietf.org/doc/html/rfc7578). /// /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as @@ -299,8 +299,9 @@ impl DispositionParam { /// # Security Note /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC 2183](https://tools.ietf.org/html/rfc2183#section-2.3). -// TODO: private fields and use smallvec +/// information that may be present. +/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). +// TODO: think about using private fields and smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index 56b7f76b..9966a258 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -71,7 +71,8 @@ crate::http::header::common_header! { } } -/// Content-Range, described in [RFC 7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// Content-Range header, defined +/// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) /// /// # ABNF /// ```text diff --git a/src/http/header/range.rs b/src/http/header/range.rs index 4c2ceee6..f57bac91 100644 --- a/src/http/header/range.rs +++ b/src/http/header/range.rs @@ -93,7 +93,7 @@ impl ByteRangeSpec { /// simply ignore the range header and serve the full entity using a `200 /// OK` status code. /// - /// This function closely follows [RFC 7233][1] section 2.1. + /// This function closely follows [RFC 7233 §2.1]. /// As such, it considers ranges to be satisfiable if they meet the /// following conditions: /// @@ -112,7 +112,7 @@ impl ByteRangeSpec { /// value of last-byte-pos with a value that is one less than the current /// length of the selected representation). /// - /// [1]: https://tools.ietf.org/html/rfc7233 + /// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233 pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { // If the full length is zero, there is no satisfiable end-inclusive range. if full_length == 0 { From 075d871e6392d5970908d95e1e043cf16aac1e3b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 2 Dec 2021 13:59:25 +0000 Subject: [PATCH 90/99] wrap `LanguageTags` type in new `AnyOrSome` type to support wildcards (#2480) --- CHANGES.md | 11 + actix-http/src/header/mod.rs | 2 +- actix-http/src/header/shared/quality_item.rs | 15 +- actix-http/src/header/utils.rs | 31 ++- src/http/header/accept.rs | 51 +++-- src/http/header/accept_language.rs | 210 ++++++++++++++++--- src/http/header/macros.rs | 138 +++++++----- src/http/header/mod.rs | 77 ++++--- src/http/header/preference.rs | 70 +++++++ 9 files changed, 469 insertions(+), 136 deletions(-) create mode 100644 src/http/header/preference.rs diff --git a/CHANGES.md b/CHANGES.md index 00bf85f2..36a56b82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] + +### Changed +* Rename `Accept::{mime_precedence => ranked}`. [#2480] +* Rename `Accept::{mime_preference => preference}`. [#2480] + +### Fixed +* Accept wildcard `*` items in `AcceptLanguage`. [#2480] + +[#2480]: https://github.com/actix/actix-web/pull/2480 ## 4.0.0-beta.13 - 2021-11-30 diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 721f4ea7..308cb012 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -55,7 +55,7 @@ pub trait Header: IntoHeaderValue { fn name() -> HeaderName; /// Parse a header - fn parse(msg: &T) -> Result; + fn parse(msg: &M) -> Result; } /// Convert `http::HeaderMap` to our `HeaderMap`. diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index ad1f6877..b9cca911 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,8 +1,7 @@ use std::{ cmp, convert::{TryFrom, TryInto}, - fmt, - str::{self, FromStr}, + fmt, str, }; use derive_more::{Display, Error}; @@ -83,16 +82,17 @@ impl TryFrom for Quality { /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). #[derive(Clone, PartialEq, Debug)] pub struct QualityItem { - /// The actual contents of the field. + /// The wrapped contents of the field. pub item: T, + /// The quality (client or server preference) for the value. pub quality: Quality, } impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. + /// Constructs a new `QualityItem` from an item and a quality value. + /// + /// The item can be of any type. The quality should be a value in the range [0, 1]. pub fn new(item: T, quality: Quality) -> QualityItem { QualityItem { item, quality } } @@ -116,7 +116,7 @@ impl fmt::Display for QualityItem { } } -impl FromStr for QualityItem { +impl str::FromStr for QualityItem { type Err = ParseError; fn from_str(qitem_str: &str) -> Result { @@ -128,6 +128,7 @@ impl FromStr for QualityItem { let mut raw_item = qitem_str; let mut quality = 1f32; + // TODO: MSRV(1.52): use rsplit_once let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect(); if parts.len() == 2 { diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index 94c5b6dc..2168202b 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -12,7 +12,8 @@ where I: Iterator + 'a, T: FromStr, { - let mut result = Vec::new(); + let size_guess = all.size_hint().1.unwrap_or(2); + let mut result = Vec::with_capacity(size_guess); for h in all { let s = h.to_str().map_err(|_| ParseError::Header)?; @@ -26,6 +27,7 @@ where .filter_map(|x| x.trim().parse().ok()), ) } + Ok(result) } @@ -34,10 +36,12 @@ where pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { if let Some(line) = val { let line = line.to_str().map_err(|_| ParseError::Header)?; + if !line.is_empty() { return T::from_str(line).or(Err(ParseError::Header)); } } + Err(ParseError::Header) } @@ -48,13 +52,16 @@ where T: fmt::Display, { let mut iter = parts.iter(); + if let Some(part) = iter.next() { fmt::Display::fmt(part, f)?; } + for part in iter { f.write_str(", ")?; fmt::Display::fmt(part, f)?; } + Ok(()) } @@ -65,3 +72,25 @@ pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Res let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); fmt::Display::fmt(&encoded, f) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn comma_delimited_parsing() { + let headers = vec![]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![0; 0]); + + let headers = vec![ + HeaderValue::from_static(""), + HeaderValue::from_static(","), + HeaderValue::from_static(" "), + HeaderValue::from_static("1 ,"), + HeaderValue::from_static(""), + ]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![1]); + } +} diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 3c4d5f59..bc794c02 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -77,7 +77,7 @@ crate::http::header::common_header! { /// ]) /// ); /// ``` - (Accept, header::ACCEPT) => (QualityItem)+ + (Accept, header::ACCEPT) => (QualityItem)* test_parse_and_format { // Tests from the RFC @@ -88,6 +88,7 @@ crate::http::header::common_header! { QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("audio/basic".parse().unwrap()), ]))); + crate::http::header::common_header_test!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], @@ -99,6 +100,7 @@ crate::http::header::common_header! { q(800)), qitem("text/x-c".parse().unwrap()), ]))); + // Custom tests crate::http::header::common_header_test!( test3, @@ -154,7 +156,11 @@ impl Accept { /// [q-factor weighting] and specificity. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 - pub fn mime_precedence(&self) -> Vec { + pub fn ranked(&self) -> Vec { + if self.is_empty() { + return vec![]; + } + let mut types = self.0.clone(); // use stable sort so items with equal q-factor and specificity retain listed order @@ -201,12 +207,29 @@ impl Accept { /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// - /// Returns `None` if contained list is empty. + /// As per the spec, will return [`Mime::STAR_STAR`] (indicating no preference) if the contained + /// list is empty. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 - pub fn mime_preference(&self) -> Option { - let types = self.mime_precedence(); - types.first().cloned() + pub fn preference(&self) -> Mime { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(mime::STAR_STAR) } } @@ -216,12 +239,12 @@ mod tests { use crate::http::header::q; #[test] - fn test_mime_precedence() { + fn ranking_precedence() { let test = Accept(vec![]); - assert!(test.mime_precedence().is_empty()); + assert!(test.ranked().is_empty()); let test = Accept(vec![qitem(mime::APPLICATION_JSON)]); - assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON)); + assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); let test = Accept(vec![ qitem(mime::TEXT_HTML), @@ -230,7 +253,7 @@ mod tests { QualityItem::new(mime::STAR_STAR, q(0.8)), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![ mime::TEXT_HTML, "application/xhtml+xml".parse().unwrap(), @@ -245,20 +268,20 @@ mod tests { qitem(mime::IMAGE_PNG), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR] ); } #[test] - fn test_mime_preference() { + fn preference_selection() { let test = Accept(vec![ qitem(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), ]); - assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML)); + assert_eq!(test.preference(), mime::TEXT_HTML); let test = Accept(vec![ QualityItem::new("video/*".parse().unwrap(), q(0.8)), @@ -267,6 +290,6 @@ mod tests { qitem(mime::IMAGE_SVG), QualityItem::new(mime::IMAGE_STAR, q(0.8)), ]); - assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG)); + assert_eq!(test.preference(), mime::IMAGE_PNG); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index a7b60e36..e96d1d13 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -1,66 +1,224 @@ use language_tags::LanguageTag; -use super::{QualityItem, ACCEPT_LANGUAGE}; +use super::{common_header, Preference, QualityItem}; +use crate::http::header; -crate::http::header::common_header! { +common_header! { /// `Accept-Language` header, defined /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. + /// The `Accept-Language` header field can be used by user agents to indicate the set of natural + /// languages that are preferred in the response. + /// + /// The `Accept-Language` header is defined in + /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language + /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). /// /// # ABNF - /// /// ```text /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = + /// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" + /// alphanum = ALPHA / DIGIT + /// weight = OWS ";" OWS "q=" qvalue + /// qvalue = ( "0" [ "." 0*3DIGIT ] ) + /// / ( "1" [ "." 0*3("0") ] ) /// ``` /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` + /// # Example Values + /// - `da, en-gb;q=0.8, en;q=0.7` + /// - `en-us;q=1.0, en;q=0.5, fr` + /// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5` /// /// # Examples - /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; + /// use actix_web::http::header::{AcceptLanguage, qitem}; /// /// let mut builder = HttpResponse::Ok(); - /// let langtag = LanguageTag::parse("en-US").unwrap(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem(langtag), + /// qitem("en-US".parse().unwrap()) /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, QualityItem, q, qitem}; + /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem(LanguageTag::parse("da").unwrap()), - /// QualityItem::new(LanguageTag::parse("en-GB").unwrap(), q(800)), - /// QualityItem::new(LanguageTag::parse("en").unwrap(), q(700)), + /// qitem("da".parse().unwrap()), + /// QualityItem::new("en-GB".parse().unwrap(), q(800)), + /// QualityItem::new("en".parse().unwrap(), q(700)), /// ]) /// ); /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ + (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* - test_accept_language { - // From the RFC - crate::http::header::common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - crate::http::header::common_header_test!( - test2, vec![b"en-US, en; q=0.5, fr"], + parse_and_fmt_tests { + common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![]))); + + common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![]))); + + common_header_test!( + example_from_rfc, + vec![b"da, en-gb;q=0.8, en;q=0.7"] + ); + + common_header_test!( + not_ordered_by_weight, + vec![b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ qitem("en-US".parse().unwrap()), QualityItem::new("en".parse().unwrap(), q(500)), qitem("fr".parse().unwrap()), - ]))); + ])) + ); + + common_header_test!( + has_wildcard, + vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], + Some(AcceptLanguage(vec![ + qitem("fr-CH".parse().unwrap()), + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("de".parse().unwrap(), q(700)), + QualityItem::new("*".parse().unwrap(), q(500)), + ])) + ); + } +} + +impl AcceptLanguage { + /// Returns a sorted list of languages from highest to lowest precedence, accounting + /// for [q-factor weighting]. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn ranked(&self) -> Vec> { + if self.0.is_empty() { + return vec![]; + } + + let mut types = self.0.clone(); + + // use stable sort so items with equal q-factor retain listed order + types.sort_by(|a, b| { + // sort by q-factor descending + b.quality.cmp(&a.quality) + }); + + types.into_iter().map(|qitem| qitem.item).collect() + } + + /// Extracts the most preferable language, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first language is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// As per the spec, returns [`Preference::Any`] if contained list is empty. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Preference { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(Preference::Any) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header::*; + + #[test] + fn ranking_precedence() { + let test = AcceptLanguage(vec![]); + assert!(test.ranked().is_empty()); + + let test = AcceptLanguage(vec![qitem("fr-CH".parse().unwrap())]); + assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); + + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr-CH".parse().unwrap(), + "fr".parse().unwrap(), + "en".parse().unwrap(), + "de".parse().unwrap(), + "*".parse().unwrap(), + ] + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr".parse().unwrap(), + "fr-CH".parse().unwrap(), + "en".parse().unwrap(), + "*".parse().unwrap(), + "de".parse().unwrap(), + ] + ); + } + + #[test] + fn preference_selection() { + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr-CH".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![]); + assert_eq!(test.preference(), Preference::Any); } } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index 419d4fb6..7fed7f28 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -1,33 +1,36 @@ +// TODO: replace with derive_more impl macro_rules! common_header_deref { ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { + impl ::core::ops::Deref for $from { type Target = $to; #[inline] - fn deref(&self) -> &$to { + fn deref(&self) -> &Self::Target { &self.0 } } - impl ::std::ops::DerefMut for $from { + impl ::core::ops::DerefMut for $from { #[inline] - fn deref_mut(&mut self) -> &mut $to { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } }; } +/// Sets up a test module with some useful imports for use with [`common_header_test!`]. macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] #[cfg(test)] mod $tm { + #![allow(unused_imports)] + use std::str; use actix_http::http::Method; use mime::*; use $crate::http::header::*; - use super::$id as HeaderField; + use super::{$id as HeaderField, *}; $($tf)* } } @@ -42,14 +45,19 @@ macro_rules! common_header_test { let raw = $raw; let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { req = req.insert_header((HeaderField::name(), item)).take(); } + let req = req.finish(); let value = HeaderField::parse(&req); + let result = format!("{}", value.unwrap()); let expected = String::from_utf8(raw[0].to_vec()).unwrap(); + let result_cmp: Vec = result .to_ascii_lowercase() .split(' ') @@ -60,10 +68,12 @@ macro_rules! common_header_test { .split(' ') .map(|x| x.to_owned()) .collect(); + assert_eq!(result_cmp.concat(), expected_cmp.concat()); } }; - ($id:ident, $raw:expr, $typed:expr) => { + + ($id:ident, $raw:expr, $exp:expr) => { #[test] fn $id() { use actix_http::test; @@ -75,26 +85,35 @@ macro_rules! common_header_test { } let req = req.finish(); let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { + let exp: Option = $exp; + + println!("req: {:?}", &req); + println!("val: {:?}", &val); + println!("exp: {:?}", &exp); + + // test parsing + assert_eq!(val.ok(), exp); + + // test formatting + if let Some(exp) = exp { let raw = &($raw)[..]; let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); + if let Some(s) = iter.next() { joined.push_str(s); + for s in iter { + joined.push_str(", "); + joined.push_str(s); + } } - assert_eq!(format!("{}", typed.unwrap()), joined); + assert_eq!(format!("{}", exp), joined); } } }; } macro_rules! common_header { + // TODO: these docs are wrong, there's no $n or $nn // $a:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header @@ -111,92 +130,100 @@ macro_rules! common_header { fn name() -> $crate::http::header::HeaderName { $name } + #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) + fn parse(msg: &M) -> Result { + let headers = msg.headers().get_all(Self::name()); + $crate::http::header::from_comma_delimited(headers).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; + // List header, one or more items ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { + fn parse(msg: &M) -> Result { $crate::http::header::from_comma_delimited( msg.headers().get_all(Self::name())).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { $crate::http::header::fmt_comma_delimited(f, &self.0[..]) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) } } }; + // Single value header ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub $value); + crate::http::header::common_header_deref!($id => $value); + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } + #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) + fn parse(msg: &M) -> Result { + let header = msg.headers().get(Self::name()); + $crate::http::header::from_one_raw_str(header).map($id) } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&self.0, f) } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; @@ -205,6 +232,7 @@ macro_rules! common_header { } } }; + // List header, one or more items with "*" option ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { $(#[$a])* @@ -215,42 +243,46 @@ macro_rules! common_header { /// Only the listed items are a match Items(Vec<$item>), } + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { $name } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - if let Some(true) = any { + #[inline] + fn parse(msg: &M) -> Result { + let is_any = msg + .headers() + .get(Self::name()) + .and_then(|hdr| hdr.to_str().ok()) + .map(|hdr| hdr.trim() == "*"); + + if let Some(true) = is_any { Ok($id::Any) } else { - Ok($id::Items( - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) + let headers = msg.headers().get_all(Self::name()); + Ok($id::Items($crate::http::header::from_comma_delimited(headers)?)) } } } - impl std::fmt::Display for $id { + + impl ::core::fmt::Display for $id { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { match *self { $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited( - f, &fields[..]) + $id::Items(ref fields) => + $crate::http::header::fmt_comma_delimited(f, &fields[..]) } } } + impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; + use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); let _ = write!(&mut writer, "{}", self); $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 79ba5772..45d5b8d1 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -1,15 +1,52 @@ //! A Collection of Header implementations for common HTTP Headers. //! -//! ## Mime -//! +//! ## Mime Types //! Several header fields use MIME values for their contents. Keeping with the strongly-typed theme, //! the [mime] crate is used in such headers as [`ContentType`] and [`Accept`]. -use bytes::{Bytes, BytesMut}; use std::fmt; -pub use self::accept_charset::AcceptCharset; +use bytes::{Bytes, BytesMut}; + +// re-export from actix-http +// - header name / value types +// - relevant traits for converting to header name / value +// - all const header names +// - header map +// - the few typed headers from actix-http +// - header parsing utils pub use actix_http::http::header::*; + +mod accept_charset; +// mod accept_encoding; +mod accept; +mod accept_language; +mod allow; +mod cache_control; +mod content_disposition; +mod content_language; +mod content_range; +mod content_type; +mod date; +mod encoding; +mod entity; +mod etag; +mod expires; +mod if_match; +mod if_modified_since; +mod if_none_match; +mod if_range; +mod if_unmodified_since; +mod last_modified; +mod macros; +mod preference; +// mod range; + +#[cfg(test)] +pub(crate) use macros::common_header_test; +pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; + +pub use self::accept_charset::AcceptCharset; //pub use self::accept_encoding::AcceptEncoding; pub use self::accept::Accept; pub use self::accept_language::AcceptLanguage; @@ -30,11 +67,10 @@ pub use self::if_none_match::IfNoneMatch; pub use self::if_range::IfRange; pub use self::if_unmodified_since::IfUnmodifiedSince; pub use self::last_modified::LastModified; +pub use self::preference::Preference; //pub use self::range::{Range, ByteRangeSpec}; -pub(crate) use actix_http::http::header::{ - fmt_comma_delimited, from_comma_delimited, from_one_raw_str, -}; +/// Format writer ([`fmt::Write`]) for a [`BytesMut`]. #[derive(Debug, Default)] struct Writer { buf: BytesMut, @@ -62,30 +98,3 @@ impl fmt::Write for Writer { fmt::write(self, args) } } - -mod accept_charset; -// mod accept_encoding; -mod accept; -mod accept_language; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_range; -mod content_type; -mod date; -mod encoding; -mod entity; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; - -mod macros; -#[cfg(test)] -pub(crate) use macros::common_header_test; -pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; diff --git a/src/http/header/preference.rs b/src/http/header/preference.rs new file mode 100644 index 00000000..979fc772 --- /dev/null +++ b/src/http/header/preference.rs @@ -0,0 +1,70 @@ +use std::{ + fmt::{self, Write as _}, + str, +}; + +/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the +/// underlying type does not support them. +/// +/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) +/// typed header but it does not parse `*` successfully. On the other hand, the `mime` crate, used +/// for [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not +/// used in those header types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub enum Preference { + /// A wildcard value. + Any, + + /// A valid `T`. + Specific(T), +} + +impl Preference { + /// Returns true if preference is the any/wildcard (`*`) value. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Returns true if preference is the specific item (`T`) variant. + pub fn is_specific(&self) -> bool { + matches!(self, Self::Specific(_)) + } + + /// Returns reference to value in `Specific` variant, if it is set. + pub fn item(&self) -> Option<&T> { + match self { + Preference::Specific(ref item) => Some(item), + Preference::Any => None, + } + } + + /// Consumes the container, returning the value in the `Specific` variant, if it is set. + pub fn into_item(self) -> Option { + match self { + Preference::Specific(item) => Some(item), + Preference::Any => None, + } + } +} + +impl fmt::Display for Preference { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Preference::Any => f.write_char('*'), + Preference::Specific(item) => fmt::Display::fmt(item, f), + } + } +} + +impl str::FromStr for Preference { + type Err = T::Err; + + #[inline] + fn from_str(s: &str) -> Result { + match s.trim() { + "*" => Ok(Self::Any), + other => other.parse().map(Preference::Specific), + } + } +} From 2a72bdae0991c49203b9359eade740e4de0881ef Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 2 Dec 2021 15:25:39 +0000 Subject: [PATCH 91/99] improve typed header macro (#2481) --- actix-http/src/header/shared/charset.rs | 2 +- actix-http/src/header/shared/extended.rs | 2 +- actix-http/src/header/shared/http_date.rs | 2 +- actix-http/src/header/shared/quality_item.rs | 4 +- actix-web-codegen/src/lib.rs | 4 +- src/http/header/accept.rs | 2 +- src/http/header/accept_charset.rs | 2 +- src/http/header/accept_encoding.rs | 8 ++- src/http/header/accept_language.rs | 4 +- src/http/header/allow.rs | 2 +- src/http/header/any_or_some.rs | 70 ++++++++++++++++++ src/http/header/cache_control.rs | 10 +-- src/http/header/content_disposition.rs | 2 +- src/http/header/content_language.rs | 7 +- src/http/header/content_range.rs | 4 +- src/http/header/content_type.rs | 4 +- src/http/header/date.rs | 2 +- src/http/header/entity.rs | 2 +- src/http/header/etag.rs | 2 +- src/http/header/expires.rs | 2 +- src/http/header/if_match.rs | 6 +- src/http/header/if_modified_since.rs | 2 +- src/http/header/if_none_match.rs | 2 +- src/http/header/if_range.rs | 7 +- src/http/header/if_unmodified_since.rs | 2 +- src/http/header/last_modified.rs | 2 +- src/http/header/macros.rs | 74 ++++++-------------- src/http/header/mod.rs | 2 +- src/http/header/range.rs | 13 ++-- 29 files changed, 147 insertions(+), 100 deletions(-) create mode 100644 src/http/header/any_or_some.rs diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs index 109c02bd..1e77e1be 100644 --- a/actix-http/src/header/shared/charset.rs +++ b/actix-http/src/header/shared/charset.rs @@ -7,7 +7,7 @@ use self::Charset::*; /// The string representation is normalized to upper case. /// /// See . -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[allow(non_camel_case_types)] pub enum Charset { /// US ASCII diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 5ff5c8b3..b2cf1d75 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -31,7 +31,7 @@ pub struct ExtendedValue { /// /// ## ABNF /// -/// ```text +/// ```plain /// ext-value = charset "'" [ language ] "'" value-chars /// ; like RFC 2231's /// ; (see [RFC 2231 §7]) diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs index 3441f90a..8dbdf4a6 100644 --- a/actix-http/src/header/shared/http_date.rs +++ b/actix-http/src/header/shared/http_date.rs @@ -8,7 +8,7 @@ use crate::{ helpers::MutWriter, }; -/// A timestamp with HTTP formatting and parsing. +/// A timestamp with HTTP-style formatting and parsing. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct HttpDate(SystemTime); diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index b9cca911..9b170f01 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -27,7 +27,7 @@ const MAX_FLOAT_QUALITY: f32 = 1.0; /// /// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more /// information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Quality(u16); impl Quality { @@ -80,7 +80,7 @@ impl TryFrom for Quality { /// Represents an item with a quality value as defined /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). -#[derive(Clone, PartialEq, Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct QualityItem { /// The wrapped contents of the field. pub item: T, diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 85faf6bc..cebf9e5f 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -66,7 +66,7 @@ mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// /// # Syntax -/// ```text +/// ```plain /// #[route("path", method="HTTP_METHOD"[, attributes])] /// ``` /// @@ -112,7 +112,7 @@ concat!(" Creates route handler with `actix_web::guard::", stringify!($variant), "`. # Syntax -```text +```plain #[", stringify!($method), r#"("path"[, attributes])] ``` diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index bc794c02..a0c98547 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -16,7 +16,7 @@ crate::http::header::common_header! { /// in-line image /// /// # ABNF - /// ```text + /// ```plain /// Accept = #( media-range [ accept-params ] ) /// /// media-range = ( "*/*" diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index fb21c5ac..5577ab60 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -12,7 +12,7 @@ crate::http::header::common_header! { /// those charsets. /// /// # ABNF - /// ```text + /// ```plain /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) /// ``` /// diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index f7375a1e..0440153a 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -1,3 +1,5 @@ +// TODO: reinstate module + use header::{Encoding, QualityItem}; header! { @@ -11,7 +13,7 @@ header! { /// preferred. /// /// # ABNF - /// ```text + /// ```plain /// Accept-Encoding = #( codings [ weight ] ) /// codings = content-coding / "identity" / "*" /// ``` @@ -59,15 +61,17 @@ header! { /// ]) /// ); /// ``` - (AcceptEncoding, "Accept-Encoding") => (QualityItem)* + (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem)* test_parse_and_format { // From the RFC crate::http::header::common_header_test!(test1, vec![b"compress, gzip"]); crate::http::header::common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); crate::http::header::common_header_test!(test3, vec![b"*"]); + // Note: Removed quality 1 from gzip crate::http::header::common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + // Note: Removed quality 1 from gzip crate::http::header::common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index e96d1d13..fb1637eb 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -15,7 +15,7 @@ common_header! { /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). /// /// # ABNF - /// ```text + /// ```plain /// Accept-Language = 1#( language-range [ weight ] ) /// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" /// alphanum = ALPHA / DIGIT @@ -57,7 +57,7 @@ common_header! { /// ``` (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* - parse_and_fmt_tests { + test_parse_and_format { common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![]))); common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![]))); diff --git a/src/http/header/allow.rs b/src/http/header/allow.rs index 2546ce3a..c8cc153e 100644 --- a/src/http/header/allow.rs +++ b/src/http/header/allow.rs @@ -12,7 +12,7 @@ crate::http::header::common_header! { /// with the resource. /// /// # ABNF - /// ```text + /// ```plain /// Allow = #method /// ``` /// diff --git a/src/http/header/any_or_some.rs b/src/http/header/any_or_some.rs new file mode 100644 index 00000000..e5a37e49 --- /dev/null +++ b/src/http/header/any_or_some.rs @@ -0,0 +1,70 @@ +use std::{ + fmt::{self, Write as _}, + str, +}; + +/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the +/// underlying type does not support them. +/// +/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) +/// typed header but it does parse `*` successfully. On the other hand, the `mime` crate, used for +/// [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not +/// used in those header types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub enum AnyOrSome { + /// A wildcard value. + Any, + + /// A valid `T`. + Item(T), +} + +impl AnyOrSome { + /// Returns true if item is wildcard (`*`) variant. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Returns true if item is a valid item (`T`) variant. + pub fn is_item(&self) -> bool { + matches!(self, Self::Item(_)) + } + + /// Returns reference to value in `Item` variant, if it is set. + pub fn item(&self) -> Option<&T> { + match self { + AnyOrSome::Item(ref item) => Some(item), + AnyOrSome::Any => None, + } + } + + /// Consumes the container, returning the value in the `Item` variant, if it is set. + pub fn into_item(self) -> Option { + match self { + AnyOrSome::Item(item) => Some(item), + AnyOrSome::Any => None, + } + } +} + +impl fmt::Display for AnyOrSome { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyOrSome::Any => f.write_char('*'), + AnyOrSome::Item(item) => fmt::Display::fmt(item, f), + } + } +} + +impl str::FromStr for AnyOrSome { + type Err = T::Err; + + #[inline] + fn from_str(s: &str) -> Result { + match s.trim() { + "*" => Ok(Self::Any), + other => other.parse().map(AnyOrSome::Item), + } + } +} diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs index c5ac9e79..27cf30ce 100644 --- a/src/http/header/cache_control.rs +++ b/src/http/header/cache_control.rs @@ -1,6 +1,8 @@ use std::fmt::{self, Write}; use std::str::FromStr; +use derive_more::{Deref, DerefMut}; + use super::{fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer}; use crate::http::header; @@ -14,7 +16,7 @@ use crate::http::header; /// not imply that the same directive is to be given in the response. /// /// # ABNF -/// ```text +/// ```plain /// Cache-Control = 1#cache-directive /// cache-directive = token [ "=" ( token / quoted-string ) ] /// ``` @@ -46,11 +48,9 @@ use crate::http::header; /// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), /// ])); /// ``` -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)] pub struct CacheControl(pub Vec); -crate::http::header::common_header_deref!(CacheControl => Vec); - // TODO: this could just be the crate::http::header::common_header! macro impl Header for CacheControl { fn name() -> header::HeaderName { @@ -88,7 +88,7 @@ impl IntoHeaderValue for CacheControl { } /// `CacheControl` contains a list of these directives. -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum CacheDirective { /// "no-cache" NoCache, diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 79fdb765..945a58f7 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -220,7 +220,7 @@ impl DispositionParam { /// itself, *Content-Disposition* has no effect. /// /// # ABNF -/// ```text +/// ```plain /// content-disposition = "Content-Disposition" ":" /// disposition-type *( ";" disposition-parm ) /// diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs index 0f428ad3..39ca8da5 100644 --- a/src/http/header/content_language.rs +++ b/src/http/header/content_language.rs @@ -1,7 +1,8 @@ -use super::{QualityItem, CONTENT_LANGUAGE}; use language_tags::LanguageTag; -crate::http::header::common_header! { +use super::{common_header, QualityItem, CONTENT_LANGUAGE}; + +common_header! { /// `Content-Language` header, defined /// in [RFC 7231 §3.1.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.3.2) /// @@ -11,7 +12,7 @@ crate::http::header::common_header! { /// representation. /// /// # ABNF - /// ```text + /// ```plain /// Content-Language = 1#language-tag /// ``` /// diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index 9966a258..90b3f7fe 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -75,7 +75,7 @@ crate::http::header::common_header! { /// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) /// /// # ABNF -/// ```text +/// ```plain /// Content-Range = byte-content-range /// / other-content-range /// @@ -91,7 +91,7 @@ crate::http::header::common_header! { /// other-content-range = other-range-unit SP other-range-resp /// other-range-resp = *CHAR /// ``` -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ContentRangeSpec { /// Byte range Bytes { diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index 624a5171..1fc75d0e 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -18,7 +18,7 @@ crate::http::header::common_header! { /// this is an issue, it's possible to implement `Header` on a custom struct. /// /// # ABNF - /// ```text + /// ```plain /// Content-Type = media-type /// ``` /// @@ -110,5 +110,3 @@ impl ContentType { ContentType(mime::APPLICATION_OCTET_STREAM) } } - -impl Eq for ContentType {} diff --git a/src/http/header/date.rs b/src/http/header/date.rs index 08c9b7ed..4063deab 100644 --- a/src/http/header/date.rs +++ b/src/http/header/date.rs @@ -9,7 +9,7 @@ crate::http::header::common_header! { /// message was originated. /// /// # ABNF - /// ```text + /// ```plain /// Date = HTTP-date /// ``` /// diff --git a/src/http/header/entity.rs b/src/http/header/entity.rs index ff8e1728..50b40b7b 100644 --- a/src/http/header/entity.rs +++ b/src/http/header/entity.rs @@ -26,7 +26,7 @@ fn check_slice_validity(slice: &str) -> bool { /// `W/"xyzzy"`. /// /// # ABNF -/// ```text +/// ```plain /// entity-tag = [ weak ] opaque-tag /// weak = %x57.2F ; "W/", case-sensitive /// opaque-tag = DQUOTE *etagc DQUOTE diff --git a/src/http/header/etag.rs b/src/http/header/etag.rs index 11206407..4724c917 100644 --- a/src/http/header/etag.rs +++ b/src/http/header/etag.rs @@ -15,7 +15,7 @@ crate::http::header::common_header! { /// prefixed by a weakness indicator. /// /// # ABNF - /// ```text + /// ```plain /// ETag = entity-tag /// ``` /// diff --git a/src/http/header/expires.rs b/src/http/header/expires.rs index 7ff78be8..5b6c65c5 100644 --- a/src/http/header/expires.rs +++ b/src/http/header/expires.rs @@ -12,7 +12,7 @@ crate::http::header::common_header! { /// time. /// /// # ABNF - /// ```text + /// ```plain /// Expires = HTTP-date /// ``` /// diff --git a/src/http/header/if_match.rs b/src/http/header/if_match.rs index ac06fa87..a565b912 100644 --- a/src/http/header/if_match.rs +++ b/src/http/header/if_match.rs @@ -1,6 +1,6 @@ -use super::{EntityTag, IF_MATCH}; +use super::{common_header, EntityTag, IF_MATCH}; -crate::http::header::common_header! { +common_header! { /// `If-Match` header, defined /// in [RFC 7232 §3.1](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1) /// @@ -17,7 +17,7 @@ crate::http::header::common_header! { /// there have been any changes to the representation data. /// /// # ABNF - /// ```text + /// ```plain /// If-Match = "*" / 1#entity-tag /// ``` /// diff --git a/src/http/header/if_modified_since.rs b/src/http/header/if_modified_since.rs index 0d23be18..14d6c355 100644 --- a/src/http/header/if_modified_since.rs +++ b/src/http/header/if_modified_since.rs @@ -11,7 +11,7 @@ crate::http::header::common_header! { /// data has not changed. /// /// # ABNF - /// ```text + /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// diff --git a/src/http/header/if_none_match.rs b/src/http/header/if_none_match.rs index 80f87ed7..fb1895fc 100644 --- a/src/http/header/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -16,7 +16,7 @@ crate::http::header::common_header! { /// the representation data. /// /// # ABNF - /// ```text + /// ```plain /// If-None-Match = "*" / 1#entity-tag /// ``` /// diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs index 9a51ab3a..5af9255f 100644 --- a/src/http/header/if_range.rs +++ b/src/http/header/if_range.rs @@ -25,7 +25,7 @@ use crate::HttpMessage; /// in Range; otherwise, send me the entire representation. /// /// # ABNF -/// ```text +/// ```plain /// If-Range = entity-tag / HTTP-date /// ``` /// @@ -107,10 +107,11 @@ impl IntoHeaderValue for IfRange { } #[cfg(test)] -mod test_if_range { +mod test_parse_and_format { + use std::str; + use super::IfRange as HeaderField; use crate::http::header::*; - use std::str; crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); crate::http::header::common_header_test!(test2, vec![b"\"abc\""]); diff --git a/src/http/header/if_unmodified_since.rs b/src/http/header/if_unmodified_since.rs index d0498682..0df6d7ba 100644 --- a/src/http/header/if_unmodified_since.rs +++ b/src/http/header/if_unmodified_since.rs @@ -11,7 +11,7 @@ crate::http::header::common_header! { /// the user agent does not have an entity-tag for the representation. /// /// # ABNF - /// ```text + /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// diff --git a/src/http/header/last_modified.rs b/src/http/header/last_modified.rs index ce5c829c..e15443ed 100644 --- a/src/http/header/last_modified.rs +++ b/src/http/header/last_modified.rs @@ -10,7 +10,7 @@ crate::http::header::common_header! { /// conclusion of handling the request. /// /// # ABNF - /// ```text + /// ```plain /// Expires = HTTP-date /// ``` /// diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index 7fed7f28..d91d1d28 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -1,25 +1,3 @@ -// TODO: replace with derive_more impl -macro_rules! common_header_deref { - ($from:ty => $to:ty) => { - impl ::core::ops::Deref for $from { - type Target = $to; - - #[inline] - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl ::core::ops::DerefMut for $from { - #[inline] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - }; -} - -/// Sets up a test module with some useful imports for use with [`common_header_test!`]. macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { #[cfg(test)] @@ -87,10 +65,6 @@ macro_rules! common_header_test { let val = HeaderField::parse(&req); let exp: Option = $exp; - println!("req: {:?}", &req); - println!("val: {:?}", &val); - println!("exp: {:?}", &exp); - // test parsing assert_eq!(val.ok(), exp); @@ -114,17 +88,17 @@ macro_rules! common_header_test { macro_rules! common_header { // TODO: these docs are wrong, there's no $n or $nn - // $a:meta: Attributes associated with the header item (usually docs) + // $attrs:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header // $nn:expr: Nice name of the header // List header, zero or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)*) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub Vec<$item>); - crate::http::header::common_header_deref!($id => Vec<$item>); + impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -158,13 +132,11 @@ macro_rules! common_header { }; // List header, one or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)+) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub Vec<$item>); - crate::http::header::common_header_deref!($id => Vec<$item>); - impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -197,13 +169,11 @@ macro_rules! common_header { }; // Single value header - ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] + ($(#[$attrs:meta])*($id:ident, $name:expr) => [$value:ty]) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] pub struct $id(pub $value); - crate::http::header::common_header_deref!($id => $value); - impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -234,8 +204,8 @@ macro_rules! common_header { }; // List header, one or more items with "*" option - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { - $(#[$a])* + ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { + $(#[$attrs])* #[derive(Clone, Debug, PartialEq)] pub enum $id { /// Any value is a match @@ -291,32 +261,32 @@ macro_rules! common_header { }; // optional test module - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { - $(#[$a])* + $(#[$attrs])* ($id, $name) => ($item)* } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; - ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { + ($(#[$attrs:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { - $(#[$a])* + $(#[$attrs])* ($id, $n) => ($item)+ } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; - ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { + ($(#[$attrs:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { - $(#[$a])* ($id, $name) => [$item] + $(#[$attrs])* ($id, $name) => [$item] } crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { + ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { crate::http::header::common_header! { - $(#[$a])* + $(#[$attrs])* ($id, $name) => {Any / ($item)+} } @@ -324,7 +294,7 @@ macro_rules! common_header { }; } -pub(crate) use {common_header, common_header_deref, common_header_test_module}; +pub(crate) use {common_header, common_header_test_module}; #[cfg(test)] pub(crate) use common_header_test; diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 45d5b8d1..750f0e5b 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -44,7 +44,7 @@ mod preference; #[cfg(test)] pub(crate) use macros::common_header_test; -pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; +pub(crate) use macros::{common_header, common_header_test_module}; pub use self::accept_charset::AcceptCharset; //pub use self::accept_encoding::AcceptEncoding; diff --git a/src/http/header/range.rs b/src/http/header/range.rs index f57bac91..11006fff 100644 --- a/src/http/header/range.rs +++ b/src/http/header/range.rs @@ -1,8 +1,11 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; +// TODO: reinstate module -use super::parsing::from_one_raw_str; -use super::{Header, Raw}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +use super::{parsing::from_one_raw_str, Header, Raw}; /// `Range` header, defined /// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) @@ -12,7 +15,7 @@ use super::{Header, Raw}; /// representation data. /// /// # ABNF -/// ```text +/// ```plain /// Range = byte-ranges-specifier / other-ranges-specifier /// other-ranges-specifier = other-range-unit "=" other-range-set /// other-range-set = 1*VCHAR From deece8d519512a83a98c7b8b1cdcd664fc0ee03a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 2 Dec 2021 17:04:40 +0000 Subject: [PATCH 92/99] re-instate accept-encoding typed header (#2482) --- CHANGES.md | 3 + actix-http/src/header/shared/quality_item.rs | 2 +- actix-http/src/header/utils.rs | 7 + scripts/ci-test | 2 + src/http/header/accept.rs | 5 +- src/http/header/accept_encoding.rs | 23 +- src/http/header/accept_language.rs | 1 + src/http/header/cache_control.rs | 290 +++++++------------ src/http/header/encoding.rs | 13 +- src/http/header/macros.rs | 51 +++- src/http/header/mod.rs | 8 +- 11 files changed, 190 insertions(+), 215 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 36a56b82..c754d4dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## Unreleased - 2021-xx-xx ### Added * Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] +* `AcceptEncoding` typed header. [#2482] ### Changed * Rename `Accept::{mime_precedence => ranked}`. [#2480] @@ -10,8 +11,10 @@ ### Fixed * Accept wildcard `*` items in `AcceptLanguage`. [#2480] +* Typed headers containing lists that require one or more items now enforce this minimum. [#2482] [#2480]: https://github.com/actix/actix-web/pull/2480 +[#2482]: https://github.com/actix/actix-web/pull/2482 ## 4.0.0-beta.13 - 2021-11-30 diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 9b170f01..a109b44e 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -27,7 +27,7 @@ const MAX_FLOAT_QUALITY: f32 = 1.0; /// /// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more /// information on quality values in HTTP header fields. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Quality(u16); impl Quality { diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index 2168202b..a23f5b75 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -83,6 +83,13 @@ mod tests { let res: Vec = from_comma_delimited(headers.iter()).unwrap(); assert_eq!(res, vec![0; 0]); + let headers = vec![ + HeaderValue::from_static("1, 2"), + HeaderValue::from_static("3,4"), + ]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![1, 2, 3, 4]); + let headers = vec![ HeaderValue::from_static(""), HeaderValue::from_static(","), diff --git a/scripts/ci-test b/scripts/ci-test index 096eb760..98e13927 100755 --- a/scripts/ci-test +++ b/scripts/ci-test @@ -14,3 +14,5 @@ cargo test --lib --tests -p=actix-test --all-features cargo test --lib --tests -p=actix-files cargo test --lib --tests -p=actix-multipart --all-features cargo test --lib --tests -p=actix-web-actors --all-features + +cargo test --workspace --doc diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index a0c98547..fe291c01 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -118,8 +118,9 @@ crate::http::header::common_header! { #[test] fn test_fuzzing1() { - use actix_http::test::TestRequest; - let req = TestRequest::default().insert_header((crate::http::header::ACCEPT, "chunk#;e")).finish(); + let req = test::TestRequest::default() + .insert_header((header::ACCEPT, "chunk#;e")) + .finish(); let header = Accept::parse(&req); assert!(header.is_ok()); } diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index 0440153a..85cd0a4f 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -1,8 +1,9 @@ -// TODO: reinstate module +use actix_http::header::QualityItem; -use header::{Encoding, QualityItem}; +use super::{common_header, Encoding}; +use crate::http::header; -header! { +common_header! { /// `Accept-Encoding` header, defined /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// @@ -30,7 +31,7 @@ header! { /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; /// - /// let mut builder = HttpResponse::new(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) /// ); @@ -39,7 +40,7 @@ header! { /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; /// - /// let mut builder = HttpResponse::new(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ /// qitem(Encoding::Chunked), @@ -52,7 +53,7 @@ header! { /// use actix_web::HttpResponse; /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q, qitem}; /// - /// let mut builder = HttpResponse::new(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ /// qitem(Encoding::Chunked), @@ -65,14 +66,14 @@ header! { test_parse_and_format { // From the RFC - crate::http::header::common_header_test!(test1, vec![b"compress, gzip"]); - crate::http::header::common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - crate::http::header::common_header_test!(test3, vec![b"*"]); + common_header_test!(test1, vec![b"compress, gzip"]); + common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + common_header_test!(test3, vec![b"*"]); // Note: Removed quality 1 from gzip - crate::http::header::common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); // Note: Removed quality 1 from gzip - crate::http::header::common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index fb1637eb..229f95ef 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -67,6 +67,7 @@ common_header! { vec![b"da, en-gb;q=0.8, en;q=0.7"] ); + common_header_test!( not_ordered_by_weight, vec![b"en-US, en; q=0.5, fr"], diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs index 27cf30ce..490d3655 100644 --- a/src/http/header/cache_control.rs +++ b/src/http/header/cache_control.rs @@ -1,92 +1,97 @@ -use std::fmt::{self, Write}; -use std::str::FromStr; - -use derive_more::{Deref, DerefMut}; - -use super::{fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer}; +use std::{fmt, str}; +use super::common_header; use crate::http::header; -/// `Cache-Control` header, defined -/// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2). -/// -/// The `Cache-Control` header field is used to specify directives for -/// caches along the request/response chain. Such cache directives are -/// unidirectional in that the presence of a directive in a request does -/// not imply that the same directive is to be given in the response. -/// -/// # ABNF -/// ```plain -/// Cache-Control = 1#cache-directive -/// cache-directive = token [ "=" ( token / quoted-string ) ] -/// ``` -/// -/// # Example Values -/// -/// * `no-cache` -/// * `private, community="UCI"` -/// * `max-age=30` -/// -/// # Examples -/// ``` -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); -/// ``` -/// -/// ``` -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.insert_header(CacheControl(vec![ -/// CacheDirective::NoCache, -/// CacheDirective::Private, -/// CacheDirective::MaxAge(360u32), -/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), -/// ])); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)] -pub struct CacheControl(pub Vec); +common_header! { + /// `Cache-Control` header, defined + /// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2). + /// + /// The `Cache-Control` header field is used to specify directives for + /// caches along the request/response chain. Such cache directives are + /// unidirectional in that the presence of a directive in a request does + /// not imply that the same directive is to be given in the response. + /// + /// # ABNF + /// ```text + /// Cache-Control = 1#cache-directive + /// cache-directive = token [ "=" ( token / quoted-string ) ] + /// ``` + /// + /// # Example Values + /// * `no-cache` + /// * `private, community="UCI"` + /// * `max-age=30` + /// + /// # Examples + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{CacheControl, CacheDirective}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); + /// ``` + /// + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{CacheControl, CacheDirective}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header(CacheControl(vec![ + /// CacheDirective::NoCache, + /// CacheDirective::Private, + /// CacheDirective::MaxAge(360u32), + /// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), + /// ])); + /// ``` + (CacheControl, header::CACHE_CONTROL) => (CacheDirective)+ -// TODO: this could just be the crate::http::header::common_header! macro -impl Header for CacheControl { - fn name() -> header::HeaderName { - header::CACHE_CONTROL - } + test_parse_and_format { + common_header_test!(no_headers, vec![b""; 0], None); + common_header_test!(empty_header, vec![b""; 1], None); + common_header_test!(bad_syntax, vec![b"foo="], None); - #[inline] - fn parse(msg: &T) -> Result - where - T: crate::HttpMessage, - { - let directives = from_comma_delimited(msg.headers().get_all(&Self::name()))?; - if !directives.is_empty() { - Ok(CacheControl(directives)) - } else { - Err(crate::error::ParseError::Header) + common_header_test!( + multiple_headers, + vec![&b"no-cache"[..], &b"private"[..]], + Some(CacheControl(vec![ + CacheDirective::NoCache, + CacheDirective::Private, + ])) + ); + + common_header_test!( + argument, + vec![b"max-age=100, private"], + Some(CacheControl(vec![ + CacheDirective::MaxAge(100), + CacheDirective::Private, + ])) + ); + + common_header_test!( + extension, + vec![b"foo, bar=baz"], + Some(CacheControl(vec![ + CacheDirective::Extension("foo".to_owned(), None), + CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), + ])) + ); + + #[test] + fn parse_quote_form() { + let req = test::TestRequest::default() + .insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) + .finish(); + + assert_eq!( + Header::parse(&req).ok(), + Some(CacheControl(vec![CacheDirective::MaxAge(200)])) + ) } } } -impl fmt::Display for CacheControl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt_comma_delimited(f, &self.0[..]) - } -} - -impl IntoHeaderValue for CacheControl { - type Error = header::InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_maybe_shared(writer.take()) - } -} - /// `CacheControl` contains a list of these directives. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CacheDirective { @@ -126,38 +131,40 @@ pub enum CacheDirective { impl fmt::Display for CacheDirective { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use self::CacheDirective::*; - fmt::Display::fmt( - match *self { - NoCache => "no-cache", - NoStore => "no-store", - NoTransform => "no-transform", - OnlyIfCached => "only-if-cached", - MaxAge(secs) => return write!(f, "max-age={}", secs), - MaxStale(secs) => return write!(f, "max-stale={}", secs), - MinFresh(secs) => return write!(f, "min-fresh={}", secs), + let dir_str = match self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", - MustRevalidate => "must-revalidate", - Public => "public", - Private => "private", - ProxyRevalidate => "proxy-revalidate", - SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), - Extension(ref name, None) => &name[..], - Extension(ref name, Some(ref arg)) => { - return write!(f, "{}={}", name, arg); - } - }, - f, - ) + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(name, None) => name.as_str(), + Extension(name, Some(arg)) => return write!(f, "{}={}", name, arg), + }; + + f.write_str(dir_str) } } -impl FromStr for CacheDirective { - type Err = Option<::Err>; - fn from_str(s: &str) -> Result::Err>> { +impl str::FromStr for CacheDirective { + type Err = Option<::Err>; + + fn from_str(s: &str) -> Result { use self::CacheDirective::*; + match s { + "" => Err(None), + "no-cache" => Ok(NoCache), "no-store" => Ok(NoStore), "no-transform" => Ok(NoTransform), @@ -166,7 +173,7 @@ impl FromStr for CacheDirective { "public" => Ok(Public), "private" => Ok(Private), "proxy-revalidate" => Ok(ProxyRevalidate), - "" => Err(None), + _ => match s.find('=') { Some(idx) if idx + 1 < s.len() => { match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { @@ -183,76 +190,3 @@ impl FromStr for CacheDirective { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::http::header::Header; - use actix_http::test::TestRequest; - - #[test] - fn test_parse_multiple_headers() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "no-cache, private")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::NoCache, - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_argument() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "max-age=100, private")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::MaxAge(100), - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_quote_form() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![CacheDirective::MaxAge(200)])) - ) - } - - #[test] - fn test_parse_extension() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "foo, bar=baz")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::Extension("foo".to_owned(), None), - CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), - ])) - ) - } - - #[test] - fn test_parse_bad_syntax() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "foo=")) - .finish(); - let cache: Result = Header::parse(&req); - assert_eq!(cache.ok(), None) - } -} diff --git a/src/http/header/encoding.rs b/src/http/header/encoding.rs index ce31c100..a61edda6 100644 --- a/src/http/header/encoding.rs +++ b/src/http/header/encoding.rs @@ -4,26 +4,33 @@ pub use self::Encoding::{ Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd, }; -/// A value to represent an encoding used in `Transfer-Encoding` -/// or `Accept-Encoding` header. -#[derive(Clone, PartialEq, Debug)] +/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header. +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { /// The `chunked` encoding. Chunked, + /// The `br` encoding. Brotli, + /// The `gzip` encoding. Gzip, + /// The `deflate` encoding. Deflate, + /// The `compress` encoding. Compress, + /// The `identity` encoding. Identity, + /// The `trailers` encoding. Trailers, + /// The `zstd` encoding. Zstd, + /// Some other encoding that is less common, can be any String. EncodingExt(String), } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index d91d1d28..3f530658 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -4,11 +4,14 @@ macro_rules! common_header_test_module { mod $tm { #![allow(unused_imports)] - use std::str; - use actix_http::http::Method; - use mime::*; - use $crate::http::header::*; + use ::core::str; + + use ::actix_http::{http::Method, test}; + use ::mime::*; + + use $crate::http::header::{self, *}; use super::{$id as HeaderField, *}; + $($tf)* } } @@ -19,22 +22,22 @@ macro_rules! common_header_test { ($id:ident, $raw:expr) => { #[test] fn $id() { - use actix_http::test; + use ::actix_http::test; let raw = $raw; - let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); + let headers = raw.iter().map(|x| x.to_vec()).collect::>(); let mut req = test::TestRequest::default(); - for item in a { - req = req.insert_header((HeaderField::name(), item)).take(); + for item in headers { + req = req.append_header((HeaderField::name(), item)).take(); } let req = req.finish(); let value = HeaderField::parse(&req); let result = format!("{}", value.unwrap()); - let expected = String::from_utf8(raw[0].to_vec()).unwrap(); + let expected = ::std::string::String::from_utf8(raw[0].to_vec()).unwrap(); let result_cmp: Vec = result .to_ascii_lowercase() @@ -56,14 +59,17 @@ macro_rules! common_header_test { fn $id() { use actix_http::test; - let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); + let headers = $raw.iter().map(|x| x.to_vec()).collect::>(); let mut req = test::TestRequest::default(); - for item in a { - req.insert_header((HeaderField::name(), item)); + + for item in headers { + req.append_header((HeaderField::name(), item)); } + let req = req.finish(); let val = HeaderField::parse(&req); - let exp: Option = $exp; + + let exp: ::core::option::Option = $exp; // test parsing assert_eq!(val.ok(), exp); @@ -122,6 +128,7 @@ macro_rules! common_header { impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; + #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); @@ -142,10 +149,19 @@ macro_rules! common_header { fn name() -> $crate::http::header::HeaderName { $name } + #[inline] - fn parse(msg: &M) -> Result { - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) + fn parse(msg: &M) -> Result{ + let headers = msg.headers().get_all(Self::name()); + + $crate::http::header::from_comma_delimited(headers) + .and_then(|items| { + if items.is_empty() { + Err($crate::error::ParseError::Header) + } else { + Ok($id(items)) + } + }) } } @@ -159,6 +175,7 @@ macro_rules! common_header { impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; + #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); @@ -197,6 +214,7 @@ macro_rules! common_header { impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; + #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { self.0.try_into_value() } @@ -251,6 +269,7 @@ macro_rules! common_header { impl $crate::http::header::IntoHeaderValue for $id { type Error = $crate::http::header::InvalidHeaderValue; + #[inline] fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { use ::core::fmt::Write; let mut writer = $crate::http::header::Writer::new(); diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 750f0e5b..98548dad 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -17,9 +17,9 @@ use bytes::{Bytes, BytesMut}; // - header parsing utils pub use actix_http::http::header::*; -mod accept_charset; -// mod accept_encoding; mod accept; +mod accept_charset; +mod accept_encoding; mod accept_language; mod allow; mod cache_control; @@ -46,9 +46,9 @@ mod preference; pub(crate) use macros::common_header_test; pub(crate) use macros::{common_header, common_header_test_module}; -pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; pub use self::accept::Accept; +pub use self::accept_charset::AcceptCharset; +pub use self::accept_encoding::AcceptEncoding; pub use self::accept_language::AcceptLanguage; pub use self::allow::Allow; pub use self::cache_control::{CacheControl, CacheDirective}; From a2d5c5a0580d3e0d9e1cbb59396ec24159573384 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Fri, 3 Dec 2021 02:16:34 +0800 Subject: [PATCH 93/99] Use cilent time out for h2 handshake timeout. (#2483) --- actix-http/CHANGES.md | 4 + actix-http/src/h2/dispatcher.rs | 13 ++- actix-http/src/h2/mod.rs | 55 ++++++++- actix-http/src/h2/service.rs | 13 +-- actix-http/src/service.rs | 11 +- actix-http/tests/test_h2_ping_pong.rs | 77 ------------- actix-http/tests/test_h2_timer.rs | 153 ++++++++++++++++++++++++++ 7 files changed, 231 insertions(+), 95 deletions(-) delete mode 100644 actix-http/tests/test_h2_ping_pong.rs create mode 100644 actix-http/tests/test_h2_timer.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 797cde99..1eaccfb2 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,7 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] +* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +[#2483]: https://github.com/actix/actix-web/pull/2483 ## 3.0.0-beta.14 - 2021-11-30 ### Changed diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 8efd3e83..607997eb 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::time::Sleep; +use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; @@ -55,9 +55,16 @@ where on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, + timer: Option>>, ) -> Self { - let ping_pong = config.keep_alive_timer().map(|timer| H2PingPong { - timer: Box::pin(timer), + let ping_pong = config.keep_alive().map(|dur| H2PingPong { + timer: timer + .map(|mut timer| { + // reset timer if it's received from new function. + timer.as_mut().reset(config.now() + dur); + timer + }) + .unwrap_or_else(|| Box::pin(sleep(dur))), on_flight: false, ping_pong: connection.ping_pong().unwrap(), }); diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index 7eff44ac..25d53403 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -1,20 +1,30 @@ //! HTTP/2 protocol. use std::{ + future::Future, pin::Pin, task::{Context, Poll}, }; +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_rt::time::Sleep; use bytes::Bytes; use futures_core::{ready, Stream}; -use h2::RecvStream; +use h2::{ + server::{handshake, Connection, Handshake}, + RecvStream, +}; mod dispatcher; mod service; pub use self::dispatcher::Dispatcher; pub use self::service::H2Service; -use crate::error::PayloadError; + +use crate::{ + config::ServiceConfig, + error::{DispatchError, PayloadError}, +}; /// HTTP/2 peer stream. pub struct Payload { @@ -50,3 +60,44 @@ impl Stream for Payload { } } } + +pub(crate) fn handshake_with_timeout( + io: T, + config: &ServiceConfig, +) -> HandshakeWithTimeout +where + T: AsyncRead + AsyncWrite + Unpin, +{ + HandshakeWithTimeout { + handshake: handshake(io), + timer: config.client_timer().map(Box::pin), + } +} + +pub(crate) struct HandshakeWithTimeout { + handshake: Handshake, + timer: Option>>, +} + +impl Future for HandshakeWithTimeout +where + T: AsyncRead + AsyncWrite + Unpin, +{ + type Output = Result<(Connection, Option>>), DispatchError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + match Pin::new(&mut this.handshake).poll(cx)? { + // return the timer on success handshake. It can be re-used for h2 ping-pong. + Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))), + Poll::Pending => match this.timer.as_mut() { + Some(timer) => { + ready!(timer.as_mut().poll(cx)); + Poll::Ready(Err(DispatchError::SlowRequestTimeout)) + } + None => Poll::Pending, + }, + } + } +} diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 79874023..0ad17ec0 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -15,9 +15,7 @@ use actix_service::{ ServiceFactoryExt as _, }; use actix_utils::future::ready; -use bytes::Bytes; use futures_core::{future::LocalBoxFuture, ready}; -use h2::server::{handshake as h2_handshake, Handshake as H2Handshake}; use log::error; use crate::{ @@ -28,7 +26,7 @@ use crate::{ ConnectCallback, OnConnectData, Request, Response, }; -use super::dispatcher::Dispatcher; +use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout}; /// `ServiceFactory` implementation for HTTP/2 transport pub struct H2Service { @@ -297,7 +295,7 @@ where Some(self.cfg.clone()), addr, on_connect_data, - h2_handshake(io), + handshake_with_timeout(io, &self.cfg), ), } } @@ -314,7 +312,7 @@ where Option, Option, OnConnectData, - H2Handshake, + HandshakeWithTimeout, ), } @@ -352,7 +350,7 @@ where ref mut on_connect_data, ref mut handshake, ) => match ready!(Pin::new(handshake).poll(cx)) { - Ok(conn) => { + Ok((conn, timer)) => { let on_connect_data = std::mem::take(on_connect_data); self.state = State::Incoming(Dispatcher::new( srv.take().unwrap(), @@ -360,12 +358,13 @@ where on_connect_data, config.take().unwrap(), *peer_addr, + timer, )); self.poll(cx) } Err(err) => { trace!("H2 handshake error: {}", err); - Poll::Ready(Err(err.into())) + Poll::Ready(Err(err)) } }, } diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index a47dda73..fb0cccb3 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -9,13 +9,11 @@ use std::{ task::{Context, Poll}, }; -use ::h2::server::{handshake as h2_handshake, Handshake as H2Handshake}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; use actix_service::{ fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; -use bytes::Bytes; use futures_core::{future::LocalBoxFuture, ready}; use pin_project::pin_project; @@ -522,7 +520,7 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { state: State::H2Handshake(Some(( - h2_handshake(io), + h2::handshake_with_timeout(io, &self.cfg), self.cfg.clone(), self.flow.clone(), on_connect_data, @@ -567,7 +565,7 @@ where H2(#[pin] h2::Dispatcher), H2Handshake( Option<( - H2Handshake, + h2::HandshakeWithTimeout, ServiceConfig, Rc>, OnConnectData, @@ -625,7 +623,7 @@ where StateProj::H2(disp) => disp.poll(cx), StateProj::H2Handshake(data) => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { - Ok(conn) => { + Ok((conn, timer)) => { let (_, cfg, srv, on_connect_data, peer_addr) = data.take().unwrap(); self.as_mut().project().state.set(State::H2( @@ -635,13 +633,14 @@ where on_connect_data, cfg, peer_addr, + timer, ), )); self.poll(cx) } Err(err) => { trace!("H2 handshake error: {}", err); - Poll::Ready(Err(err.into())) + Poll::Ready(Err(err)) } } } diff --git a/actix-http/tests/test_h2_ping_pong.rs b/actix-http/tests/test_h2_ping_pong.rs deleted file mode 100644 index 30ce9aa5..00000000 --- a/actix-http/tests/test_h2_ping_pong.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::io; - -use actix_http::{error::Error, HttpService, Response}; -use actix_server::Server; - -#[actix_rt::test] -async fn h2_ping_pong() -> io::Result<()> { - let (tx, rx) = std::sync::mpsc::sync_channel(1); - - let lst = std::net::TcpListener::bind("127.0.0.1:0")?; - - let addr = lst.local_addr().unwrap(); - - let join = std::thread::spawn(move || { - actix_rt::System::new().block_on(async move { - let srv = Server::build() - .disable_signals() - .workers(1) - .listen("h2_ping_pong", lst, || { - HttpService::build() - .keep_alive(3) - .h2(|_| async { Ok::<_, Error>(Response::ok()) }) - .tcp() - })? - .run(); - - tx.send(srv.handle()).unwrap(); - - srv.await - }) - }); - - let handle = rx.recv().unwrap(); - - let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); - - // use a separate thread for h2 client so it can be blocked. - std::thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async move { - let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); - - let (mut tx, conn) = h2::client::handshake(stream).await.unwrap(); - - tokio::spawn(async move { conn.await.unwrap() }); - - let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap(); - let res = res.await.unwrap(); - - assert_eq!(res.status().as_u16(), 200); - - sync_tx.send(()).unwrap(); - - // intentionally block the client thread so it can not answer ping pong. - std::thread::sleep(std::time::Duration::from_secs(1000)); - }) - }); - - rx.recv().unwrap(); - - let now = std::time::Instant::now(); - - // stop server gracefully. this step would take up to 30 seconds. - handle.stop(true).await; - - // join server thread. only when connection are all gone this step would finish. - join.join().unwrap()?; - - // check the time used for join server thread so it's known that the server shutdown - // is from keep alive and not server graceful shutdown timeout. - assert!(now.elapsed() < std::time::Duration::from_secs(30)); - - Ok(()) -} diff --git a/actix-http/tests/test_h2_timer.rs b/actix-http/tests/test_h2_timer.rs new file mode 100644 index 00000000..2b9c26e4 --- /dev/null +++ b/actix-http/tests/test_h2_timer.rs @@ -0,0 +1,153 @@ +use std::io; + +use actix_http::{error::Error, HttpService, Response}; +use actix_server::Server; +use tokio::io::AsyncWriteExt; + +#[actix_rt::test] +async fn h2_ping_pong() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let srv = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(3) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(srv.handle()).unwrap(); + + srv.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for h2 client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + let (mut tx, conn) = h2::client::handshake(stream).await.unwrap(); + + tokio::spawn(async move { conn.await.unwrap() }); + + let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap(); + let res = res.await.unwrap(); + + assert_eq!(res.status().as_u16(), 200); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it can not answer ping pong. + std::thread::sleep(std::time::Duration::from_secs(1000)); + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from keep alive and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} + +#[actix_rt::test] +async fn h2_handshake_timeout() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let srv = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(30) + // set first request timeout to 5 seconds. + // this is the timeout used for http2 handshake. + .client_timeout(5000) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(srv.handle()).unwrap(); + + srv.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for tcp client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + // do not send the last new line intentionally. + // This should hang the server handshake + let malicious_buf = b"PRI * HTTP/2.0\r\n\r\nSM\r\n"; + stream.write_all(malicious_buf).await.unwrap(); + stream.flush().await.unwrap(); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it sit idle and not do handshake. + std::thread::sleep(std::time::Duration::from_secs(1000)); + + drop(stream) + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from handshake timeout and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} From c7c02ef99d6ef2c600355b1c889eb4f11ffcb88a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 4 Dec 2021 19:40:47 +0000 Subject: [PATCH 94/99] body ergonomics v3 (#2468) --- CHANGES.md | 4 + actix-files/src/chunked.rs | 3 +- actix-files/src/named.rs | 21 +- actix-http/CHANGES.md | 24 +- actix-http/examples/echo2.rs | 8 +- actix-http/src/body/body.rs | 333 --------------------- actix-http/src/body/body_stream.rs | 23 +- actix-http/src/body/boxed.rs | 80 +++++ actix-http/src/body/either.rs | 83 ++++++ actix-http/src/body/message_body.rs | 443 ++++++++++++++++++++-------- actix-http/src/body/mod.rs | 268 +---------------- actix-http/src/body/none.rs | 43 +++ actix-http/src/body/size.rs | 2 +- actix-http/src/body/sized_stream.rs | 2 + actix-http/src/body/utils.rs | 78 +++++ actix-http/src/builder.rs | 20 +- actix-http/src/encoding/encoder.rs | 144 +++++---- actix-http/src/encoding/mod.rs | 3 + actix-http/src/error.rs | 29 +- actix-http/src/h1/dispatcher.rs | 59 ++-- actix-http/src/h1/service.rs | 44 ++- actix-http/src/h1/utils.rs | 38 ++- actix-http/src/h2/dispatcher.rs | 14 +- actix-http/src/h2/service.rs | 28 +- actix-http/src/message.rs | 9 +- actix-http/src/response.rs | 175 ++++++----- actix-http/src/response_builder.rs | 67 ++--- actix-http/src/service.rs | 204 ++++++------- actix-http/src/ws/dispatcher.rs | 26 +- actix-http/src/ws/mod.rs | 33 ++- actix-http/tests/test_client.rs | 4 +- actix-http/tests/test_openssl.rs | 12 +- actix-http/tests/test_rustls.rs | 12 +- actix-http/tests/test_server.rs | 43 +-- actix-http/tests/test_ws.rs | 6 +- actix-test/src/lib.rs | 7 +- actix-web-actors/Cargo.toml | 2 +- actix-web-actors/src/ws.rs | 16 +- actix-web-actors/tests/test_ws.rs | 2 +- awc/Cargo.toml | 1 + awc/src/any_body.rs | 266 +++++++++++++++++ awc/src/client/connection.rs | 8 +- awc/src/client/error.rs | 20 +- awc/src/client/h1proto.rs | 10 +- awc/src/client/h2proto.rs | 16 +- awc/src/connect.rs | 13 +- awc/src/error.rs | 2 + awc/src/frozen.rs | 10 +- awc/src/lib.rs | 3 + awc/src/middleware/redirect.rs | 17 +- awc/src/request.rs | 13 +- awc/src/sender.rs | 24 +- benches/responder.rs | 17 +- src/app.rs | 56 ++-- src/app_service.rs | 28 +- src/data.rs | 2 +- src/dev.rs | 53 +++- src/error/error.rs | 6 +- src/error/internal.rs | 19 +- src/error/macros.rs | 2 +- src/error/response_error.rs | 30 +- src/handler.rs | 43 ++- src/http/header/accept.rs | 2 +- src/lib.rs | 2 + src/middleware/compat.rs | 6 +- src/middleware/compress.rs | 34 +-- src/middleware/logger.rs | 2 +- src/resource.rs | 29 +- src/responder.rs | 299 +++++++++++-------- src/response/builder.rs | 53 ++-- src/response/response.rs | 34 ++- src/route.rs | 23 +- src/scope.rs | 80 ++--- src/server.rs | 10 +- src/service.rs | 42 ++- src/test.rs | 24 +- src/types/either.rs | 10 +- src/types/form.rs | 43 ++- src/types/json.rs | 30 +- src/types/path.rs | 2 +- src/types/payload.rs | 6 +- src/types/query.rs | 2 +- src/web.rs | 8 +- tests/test_server.rs | 7 +- 84 files changed, 2134 insertions(+), 1685 deletions(-) delete mode 100644 actix-http/src/body/body.rs create mode 100644 actix-http/src/body/boxed.rs create mode 100644 actix-http/src/body/either.rs create mode 100644 actix-http/src/body/none.rs create mode 100644 actix-http/src/body/utils.rs create mode 100644 awc/src/any_body.rs diff --git a/CHANGES.md b/CHANGES.md index c754d4dd..2dc45c3e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### Added * Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] * `AcceptEncoding` typed header. [#2482] +* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] +* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] ### Changed * Rename `Accept::{mime_precedence => ranked}`. [#2480] @@ -11,8 +13,10 @@ ### Fixed * Accept wildcard `*` items in `AcceptLanguage`. [#2480] +* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] * Typed headers containing lists that require one or more items now enforce this minimum. [#2482] +[#2468]: https://github.com/actix/actix-web/pull/2468 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index fbb46e41..68221ccc 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -6,8 +6,7 @@ use std::{ task::{Context, Poll}, }; -use actix_web::error::Error; -use bytes::Bytes; +use actix_web::{error::Error, web::Bytes}; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 547048bb..89775c6b 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -10,12 +10,12 @@ use std::{ #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use actix_http::body::AnyBody; use actix_service::{Service, ServiceFactory}; use actix_web::{ + body::{self, BoxBody, SizedStream}, dev::{ AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, - ServiceResponse, SizedStream, + ServiceResponse, }, http::{ header::{ @@ -113,6 +113,8 @@ pub(crate) use std::fs::File; #[cfg(feature = "experimental-io-uring")] pub(crate) use tokio_uring::fs::File; +use super::chunked; + impl NamedFile { /// Creates an instance from a previously opened file. /// @@ -394,7 +396,7 @@ impl NamedFile { } /// Creates an `HttpResponse` with file as a streaming body. - pub fn into_response(self, req: &HttpRequest) -> HttpResponse { + pub fn into_response(self, req: &HttpRequest) -> HttpResponse { if self.status_code != StatusCode::OK { let mut res = HttpResponse::build(self.status_code); @@ -416,7 +418,7 @@ impl NamedFile { res.encoding(current_encoding); } - let reader = super::chunked::new_chunked_read(self.md.len(), 0, self.file); + let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); return res.streaming(reader); } @@ -527,10 +529,13 @@ impl NamedFile { if precondition_failed { return resp.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).body(AnyBody::None); + return resp + .status(StatusCode::NOT_MODIFIED) + .body(body::None::new()) + .map_into_boxed_body(); } - let reader = super::chunked::new_chunked_read(length, offset, self.file); + let reader = chunked::new_chunked_read(length, offset, self.file); if offset != 0 || length != self.md.len() { resp.status(StatusCode::PARTIAL_CONTENT); @@ -595,7 +600,9 @@ impl DerefMut for NamedFile { } impl Responder for NamedFile { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) } } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 1eaccfb2..23c15296 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,9 +3,31 @@ ## Unreleased - 2021-xx-xx ### Added * Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] -* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +* `Response::map_into_boxed_body`. [#2468] +* `body::EitherBody` enum. [#2468] +* `body::None` struct. [#2468] +* Impl `MessageBody` for `bytestring::ByteString`. [#2468] +* `impl Clone for ws::HandshakeError`. [#2468] + +### Changed +* Rename `body::BoxBody::{from_body => new}`. [#2468] +* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] +* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] +* Error types using in service builders now require `Into>`. [#2468] +* `From` implementations on error types now return a `Response`. [#2468] +* `ResponseBuilder::body(B)` now returns `Response>`. [#2468] +* `ResponseBuilder::finish()` now returns `Response>`. [#2468] + +### Removed +* `ResponseBuilder::streaming`. [#2468] +* `impl Future` for `ResponseBuilder`. [#2468] +* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] +* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] [#2483]: https://github.com/actix/actix-web/pull/2483 +[#2468]: https://github.com/actix/actix-web/pull/2468 + ## 3.0.0-beta.14 - 2021-11-30 ### Changed diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index 6e5ddec7..6092c01c 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,12 +1,14 @@ use std::io; -use actix_http::{body::AnyBody, http::HeaderValue, http::StatusCode}; -use actix_http::{Error, HttpService, Request, Response}; +use actix_http::{ + body::MessageBody, http::HeaderValue, http::StatusCode, Error, HttpService, Request, + Response, +}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; -async fn handle_request(mut req: Request) -> Result, Error> { +async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); while let Some(item) = req.payload().next().await { body.extend_from_slice(&item?) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs deleted file mode 100644 index e8861024..00000000 --- a/actix-http/src/body/body.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - borrow::Cow, - error::Error as StdError, - fmt, mem, - pin::Pin, - task::{Context, Poll}, -}; - -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; -use pin_project::pin_project; - -use crate::error::Error; - -use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; - -#[deprecated(since = "4.0.0", note = "Renamed to `AnyBody`.")] -pub type Body = AnyBody; - -/// Represents various types of HTTP message body. -#[pin_project(project = AnyBodyProj)] -#[derive(Clone)] -pub enum AnyBody { - /// Empty response. `Content-Length` header is not set. - None, - - /// Complete, in-memory response body. - Bytes(Bytes), - - /// Generic / Other message body. - Body(#[pin] B), -} - -impl AnyBody { - /// Constructs a "body" representing an empty response. - pub fn none() -> Self { - Self::None - } - - /// Constructs a new, 0-length body. - pub fn empty() -> Self { - Self::Bytes(Bytes::new()) - } - - /// Create boxed body from generic message body. - pub fn new_boxed(body: B) -> Self - where - B: MessageBody + 'static, - B::Error: Into>, - { - Self::Body(BoxBody::from_body(body)) - } - - /// Constructs new `AnyBody` instance from a slice of bytes by copying it. - /// - /// If your bytes container is owned, it may be cheaper to use a `From` impl. - pub fn copy_from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } - - #[doc(hidden)] - #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] - pub fn from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } -} - -impl AnyBody -where - B: MessageBody + 'static, - B::Error: Into>, -{ - /// Create body from generic message body. - pub fn new(body: B) -> Self { - Self::Body(body) - } - - pub fn into_boxed(self) -> AnyBody { - match self { - Self::None => AnyBody::None, - Self::Bytes(bytes) => AnyBody::Bytes(bytes), - Self::Body(body) => AnyBody::new_boxed(body), - } - } -} - -impl MessageBody for AnyBody -where - B: MessageBody, - B::Error: Into> + 'static, -{ - type Error = Error; - - fn size(&self) -> BodySize { - match self { - AnyBody::None => BodySize::None, - AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Body(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - match self.project() { - AnyBodyProj::None => Poll::Ready(None), - AnyBodyProj::Bytes(bin) => { - let len = bin.len(); - if len == 0 { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(bin)))) - } - } - - AnyBodyProj::Body(body) => body - .poll_next(cx) - .map_err(|err| Error::new_body().with_cause(err)), - } - } -} - -impl PartialEq for AnyBody { - fn eq(&self, other: &AnyBody) -> bool { - match *self { - AnyBody::None => matches!(*other, AnyBody::None), - AnyBody::Bytes(ref b) => match *other { - AnyBody::Bytes(ref b2) => b == b2, - _ => false, - }, - AnyBody::Body(_) => false, - } - } -} - -impl fmt::Debug for AnyBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Bytes(ref bytes) => write!(f, "AnyBody::Bytes({:?})", bytes), - AnyBody::Body(ref stream) => write!(f, "AnyBody::Message({:?})", stream), - } - } -} - -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Self { - Self::Bytes(Bytes::from_static(string.as_ref())) - } -} - -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Self { - Self::Bytes(Bytes::from_static(bytes)) - } -} - -impl From> for AnyBody { - fn from(vec: Vec) -> Self { - Self::Bytes(Bytes::from(vec)) - } -} - -impl From for AnyBody { - fn from(string: String) -> Self { - Self::Bytes(Bytes::from(string)) - } -} - -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Self { - Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) - } -} - -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Self { - match string { - Cow::Owned(s) => Self::from(s), - Cow::Borrowed(s) => { - Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) - } - } - } -} - -impl From for AnyBody { - fn from(bytes: Bytes) -> Self { - Self::Bytes(bytes) - } -} - -impl From for AnyBody { - fn from(bytes: BytesMut) -> Self { - Self::Bytes(bytes.freeze()) - } -} - -impl From> for AnyBody> -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -impl From> for AnyBody> -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into> + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -/// A boxed message body with boxed errors. -pub struct BoxBody(Pin>>>); - -impl BoxBody { - /// Boxes a `MessageBody` and any errors it generates. - pub fn from_body(body: B) -> Self - where - B: MessageBody + 'static, - B::Error: Into>, - { - let body = MessageBodyMapErr::new(body, Into::into); - Self(Box::pin(body)) - } - - /// Returns a mutable pinned reference to the inner message body type. - pub fn as_pin_mut( - &mut self, - ) -> Pin<&mut (dyn MessageBody>)> { - self.0.as_mut() - } -} - -impl fmt::Debug for BoxBody { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("BoxAnyBody(dyn MessageBody)") - } -} - -impl MessageBody for BoxBody { - type Error = Error; - - fn size(&self) -> BodySize { - self.0.size() - } - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - self.0 - .as_mut() - .poll_next(cx) - .map_err(|err| Error::new_body().with_cause(err)) - } -} - -#[cfg(test)] -mod tests { - use std::marker::PhantomPinned; - - use static_assertions::{assert_impl_all, assert_not_impl_all}; - - use super::*; - use crate::body::to_bytes; - - struct PinType(PhantomPinned); - - impl MessageBody for PinType { - type Error = crate::Error; - - fn size(&self) -> BodySize { - unimplemented!() - } - - fn poll_next( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>> { - unimplemented!() - } - } - - assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); - assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); - assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); - assert_impl_all!(AnyBody: MessageBody); - - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); - assert_not_impl_all!(BoxBody: Send, Sync, Unpin); - assert_not_impl_all!(AnyBody: Send, Sync, Unpin); - - #[actix_rt::test] - async fn nested_boxed_body() { - let body = AnyBody::copy_from_slice(&[1, 2, 3]); - let boxed_body = BoxBody::from_body(BoxBody::from_body(body)); - - assert_eq!( - to_bytes(boxed_body).await.unwrap(), - Bytes::from(vec![1, 2, 3]), - ); - } -} diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index 31de9b48..1da7a848 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -20,6 +20,8 @@ pin_project! { } } +// TODO: from_infallible method + impl BodyStream where S: Stream>, @@ -75,6 +77,7 @@ mod tests { use derive_more::{Display, Error}; use futures_core::ready; use futures_util::{stream, FutureExt as _}; + use pin_project_lite::pin_project; use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; @@ -166,12 +169,14 @@ mod tests { BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); assert!(matches!(to_bytes(body).await, Err(StreamErr))); - #[pin_project::pin_project(project = TimeDelayStreamProj)] - #[derive(Debug)] - enum TimeDelayStream { - Start, - Sleep(Pin>), - Done, + pin_project! { + #[derive(Debug)] + #[project = TimeDelayStreamProj] + enum TimeDelayStream { + Start, + Sleep { delay: Pin> }, + Done, + } } impl Stream for TimeDelayStream { @@ -184,12 +189,14 @@ mod tests { match self.as_mut().get_mut() { TimeDelayStream::Start => { let sleep = sleep(Duration::from_millis(1)); - self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep))); + self.as_mut().set(TimeDelayStream::Sleep { + delay: Box::pin(sleep), + }); cx.waker().wake_by_ref(); Poll::Pending } - TimeDelayStream::Sleep(ref mut delay) => { + TimeDelayStream::Sleep { ref mut delay } => { ready!(delay.poll_unpin(cx)); self.set(TimeDelayStream::Done); cx.waker().wake_by_ref(); diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs new file mode 100644 index 00000000..9442bd1d --- /dev/null +++ b/actix-http/src/body/boxed.rs @@ -0,0 +1,80 @@ +use std::{ + error::Error as StdError, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody, MessageBodyMapErr}; +use crate::Error; + +/// A boxed message body with boxed errors. +pub struct BoxBody(Pin>>>); + +impl BoxBody { + /// Boxes a `MessageBody` and any errors it generates. + pub fn new(body: B) -> Self + where + B: MessageBody + 'static, + { + let body = MessageBodyMapErr::new(body, Into::into); + Self(Box::pin(body)) + } + + /// Returns a mutable pinned reference to the inner message body type. + pub fn as_pin_mut( + &mut self, + ) -> Pin<&mut (dyn MessageBody>)> { + self.0.as_mut() + } +} + +impl fmt::Debug for BoxBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BoxBody(dyn MessageBody)") + } +} + +impl MessageBody for BoxBody { + type Error = Error; + + fn size(&self) -> BodySize { + self.0.size() + } + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.0 + .as_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)) + } +} + +#[cfg(test)] +mod tests { + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); + + assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + + #[actix_rt::test] + async fn nested_boxed_body() { + let body = Bytes::from_static(&[1, 2, 3]); + let boxed_body = BoxBody::new(BoxBody::new(body)); + + assert_eq!( + to_bytes(boxed_body).await.unwrap(), + Bytes::from(vec![1, 2, 3]), + ); + } +} diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs new file mode 100644 index 00000000..6169ee62 --- /dev/null +++ b/actix-http/src/body/either.rs @@ -0,0 +1,83 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use pin_project_lite::pin_project; + +use super::{BodySize, BoxBody, MessageBody}; +use crate::Error; + +pin_project! { + #[project = EitherBodyProj] + #[derive(Debug, Clone)] + pub enum EitherBody { + /// A body of type `L`. + Left { #[pin] body: L }, + + /// A body of type `R`. + Right { #[pin] body: R }, + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant and boxed right variant. + pub fn new(body: L) -> Self { + Self::Left { body } + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant. + pub fn left(body: L) -> Self { + Self::Left { body } + } + + /// Creates new `EitherBody` using right variant. + pub fn right(body: R) -> Self { + Self::Right { body } + } +} + +impl MessageBody for EitherBody +where + L: MessageBody + 'static, + R: MessageBody + 'static, +{ + type Error = Error; + + fn size(&self) -> BodySize { + match self { + EitherBody::Left { body } => body.size(), + EitherBody::Right { body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + EitherBodyProj::Left { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + EitherBodyProj::Right { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn type_parameter_inference() { + let _body: EitherBody<(), _> = EitherBody::new(()); + + let _body: EitherBody<_, ()> = EitherBody::left(()); + let _body: EitherBody<(), _> = EitherBody::right(()); + } +} diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 62a7e9b1..053b6f28 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -2,6 +2,7 @@ use std::{ convert::Infallible, + error::Error as StdError, mem, pin::Pin, task::{Context, Poll}, @@ -13,9 +14,12 @@ use pin_project_lite::pin_project; use super::BodySize; -/// An interface for response bodies. +/// An interface types that can converted to bytes and used as response bodies. +// TODO: examples pub trait MessageBody { - type Error; + // TODO: consider this bound to only fmt::Display since the error type is not really used + // and there is an impl for Into> on String + type Error: Into>; /// Body size hint. fn size(&self) -> BodySize; @@ -27,152 +31,218 @@ pub trait MessageBody { ) -> Poll>>; } -impl MessageBody for () { - type Error = Infallible; +mod foreign_impls { + use super::*; - fn size(&self) -> BodySize { - BodySize::Sized(0) - } + impl MessageBody for Infallible { + type Error = Infallible; - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - Poll::Ready(None) - } -} + #[inline] + fn size(&self) -> BodySize { + match *self {} + } -impl MessageBody for Box -where - B: MessageBody + Unpin, -{ - type Error = B::Error; - - fn size(&self) -> BodySize { - self.as_ref().size() - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Pin::new(self.get_mut().as_mut()).poll_next(cx) - } -} - -impl MessageBody for Pin> -where - B: MessageBody, -{ - type Error = B::Error; - - fn size(&self) -> BodySize { - self.as_ref().size() - } - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - self.as_mut().poll_next(cx) - } -} - -impl MessageBody for Bytes { - type Error = Infallible; - - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut())))) + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + match *self {} } } -} -impl MessageBody for BytesMut { - type Error = Infallible; + impl MessageBody for () { + type Error = Infallible; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + BodySize::Sized(0) + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) } } -} -impl MessageBody for &'static str { - type Error = Infallible; + impl MessageBody for Box + where + B: MessageBody + Unpin, + { + type Error = B::Error; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from_static( - mem::take(self.get_mut()).as_ref(), - )))) + #[inline] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(self.get_mut().as_mut()).poll_next(cx) } } -} -impl MessageBody for Vec { - type Error = Infallible; + impl MessageBody for Pin> + where + B: MessageBody, + { + type Error = B::Error; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut()))))) + #[inline] + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.as_mut().poll_next(cx) } } -} -impl MessageBody for String { - type Error = Infallible; + impl MessageBody for &'static [u8] { + type Error = Infallible; - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + let bytes = Bytes::from_static(bytes); + Poll::Ready(Some(Ok(bytes))) + } + } } - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from( - mem::take(self.get_mut()).into_bytes(), - )))) + impl MessageBody for Bytes { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for BytesMut { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()).freeze(); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for Vec { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(bytes)))) + } + } + } + + impl MessageBody for &'static str { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + let bytes = Bytes::from_static(string.as_bytes()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for String { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(string)))) + } + } + } + + impl MessageBody for bytestring::ByteString { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(string.into_bytes()))) } } } @@ -202,6 +272,7 @@ impl MessageBody for MessageBodyMapErr where B: MessageBody, F: FnOnce(B::Error) -> E, + E: Into>, { type Error = E; @@ -226,3 +297,129 @@ where } } } + +#[cfg(test)] +mod tests { + use actix_rt::pin; + use actix_utils::future::poll_fn; + use bytes::{Bytes, BytesMut}; + + use super::*; + + macro_rules! assert_poll_next { + ($pin:expr, $exp:expr) => { + assert_eq!( + poll_fn(|cx| $pin.as_mut().poll_next(cx)) + .await + .unwrap() // unwrap option + .unwrap(), // unwrap result + $exp + ); + }; + } + + macro_rules! assert_poll_next_none { + ($pin:expr) => { + assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none()); + }; + } + + #[actix_rt::test] + async fn boxing_equivalence() { + assert_eq!(().size(), BodySize::Sized(0)); + assert_eq!(().size(), Box::new(()).size()); + assert_eq!(().size(), Box::pin(()).size()); + + let pl = Box::new(()); + pin!(pl); + assert_poll_next_none!(pl); + + let mut pl = Box::pin(()); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_unit() { + let pl = (); + assert_eq!(pl.size(), BodySize::Sized(0)); + pin!(pl); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_static_str() { + assert_eq!("".size(), BodySize::Sized(0)); + assert_eq!("test".size(), BodySize::Sized(4)); + + let pl = "test"; + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_static_bytes() { + assert_eq!(b"".as_ref().size(), BodySize::Sized(0)); + assert_eq!(b"test".as_ref().size(), BodySize::Sized(4)); + + let pl = b"test".as_ref(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_vec() { + assert_eq!(vec![0; 0].size(), BodySize::Sized(0)); + assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + + let pl = Vec::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes() { + assert_eq!(Bytes::new().size(), BodySize::Sized(0)); + assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4)); + + let pl = Bytes::from_static(b"test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes_mut() { + assert_eq!(BytesMut::new().size(), BodySize::Sized(0)); + assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4)); + + let pl = BytesMut::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_string() { + assert_eq!(String::new().size(), BodySize::Sized(0)); + assert_eq!("test".to_owned().size(), BodySize::Sized(4)); + + let pl = "test".to_owned(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + // down-casting used to be done with a method on MessageBody trait + // test is kept to demonstrate equivalence of Any trait + #[actix_rt::test] + async fn test_body_casting() { + let mut body = String::from("hello cast"); + // let mut resp_body: &mut dyn MessageBody = &mut body; + let resp_body: &mut dyn std::any::Any = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push('!'); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } +} diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index df6c6b08..af7c4626 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -1,272 +1,20 @@ //! Traits and structures to aid consuming and writing HTTP payloads. -use std::task::Poll; - -use actix_rt::pin; -use actix_utils::future::poll_fn; -use bytes::{Bytes, BytesMut}; -use futures_core::ready; - -#[allow(clippy::module_inception)] -mod body; mod body_stream; +mod boxed; +mod either; mod message_body; +mod none; mod size; mod sized_stream; +mod utils; -#[allow(deprecated)] -pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; +pub use self::boxed::BoxBody; +pub use self::either::EitherBody; pub use self::message_body::MessageBody; pub(crate) use self::message_body::MessageBodyMapErr; +pub use self::none::None; pub use self::size::BodySize; pub use self::sized_stream::SizedStream; - -/// Collects the body produced by a `MessageBody` implementation into `Bytes`. -/// -/// Any errors produced by the body stream are returned immediately. -/// -/// # Examples -/// ``` -/// use actix_http::body::{AnyBody, to_bytes}; -/// use bytes::Bytes; -/// -/// # async fn test_to_bytes() { -/// let body = AnyBody::none(); -/// let bytes = to_bytes(body).await.unwrap(); -/// assert!(bytes.is_empty()); -/// -/// let body = AnyBody::copy_from_slice(b"123"); -/// let bytes = to_bytes(body).await.unwrap(); -/// assert_eq!(bytes, b"123"[..]); -/// # } -/// ``` -pub async fn to_bytes(body: B) -> Result { - let cap = match body.size() { - BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), - BodySize::Sized(size) => size as usize, - // good enough first guess for chunk size - BodySize::Stream => 32_768, - }; - - let mut buf = BytesMut::with_capacity(cap); - - pin!(body); - - poll_fn(|cx| loop { - let body = body.as_mut(); - - match ready!(body.poll_next(cx)) { - Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), - None => return Poll::Ready(Ok(())), - Some(Err(err)) => return Poll::Ready(Err(err)), - } - }) - .await?; - - Ok(buf.freeze()) -} - -#[cfg(test)] -mod tests { - use std::pin::Pin; - - use actix_rt::pin; - use actix_utils::future::poll_fn; - use bytes::{Bytes, BytesMut}; - - use super::{to_bytes, AnyBody as TestAnyBody, BodySize, MessageBody as _}; - - impl AnyBody { - pub(crate) fn get_ref(&self) -> &[u8] { - match *self { - AnyBody::Bytes(ref bin) => bin, - _ => panic!(), - } - } - } - - /// AnyBody alias because rustc does not (can not?) infer the default type parameter. - type AnyBody = TestAnyBody; - - #[actix_rt::test] - async fn test_static_str() { - assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); - assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from("test").get_ref(), b"test"); - - assert_eq!("test".size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| Pin::new(&mut "test").poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_static_bytes() { - assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); - assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).size(), - BodySize::Sized(4) - ); - assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), - b"test" - ); - let sb = Bytes::from(&b"test"[..]); - pin!(sb); - - assert_eq!(sb.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_vec() { - assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); - let test_vec = Vec::from("test"); - pin!(test_vec); - - assert_eq!(test_vec.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| test_vec.as_mut().poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes() { - let b = Bytes::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes_mut() { - let b = BytesMut::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_string() { - let b = "test".to_owned(); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(&b).get_ref(), b"test"); - pin!(b); - - assert_eq!(b.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_unit() { - assert_eq!(().size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) - .await - .is_none()); - } - - #[actix_rt::test] - async fn test_box_and_pin() { - let val = Box::new(()); - pin!(val); - assert_eq!(val.size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); - - let mut val = Box::pin(()); - assert_eq!(val.size(), BodySize::Sized(0)); - assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); - } - - #[actix_rt::test] - async fn test_body_eq() { - assert!( - AnyBody::Bytes(Bytes::from_static(b"1")) - == AnyBody::Bytes(Bytes::from_static(b"1")) - ); - assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None); - } - - #[actix_rt::test] - async fn test_body_debug() { - assert!(format!("{:?}", AnyBody::None).contains("Body::None")); - assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); - } - - #[actix_rt::test] - async fn test_serde_json() { - use serde_json::{json, Value}; - assert_eq!( - AnyBody::from( - serde_json::to_vec(&Value::String("test".to_owned())).unwrap() - ) - .size(), - BodySize::Sized(6) - ); - assert_eq!( - AnyBody::from( - serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() - ) - .size(), - BodySize::Sized(25) - ); - } - - // down-casting used to be done with a method on MessageBody trait - // test is kept to demonstrate equivalence of Any trait - #[actix_rt::test] - async fn test_body_casting() { - let mut body = String::from("hello cast"); - // let mut resp_body: &mut dyn MessageBody = &mut body; - let resp_body: &mut dyn std::any::Any = &mut body; - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast"); - let body = &mut resp_body.downcast_mut::().unwrap(); - body.push('!'); - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast!"); - let not_body = resp_body.downcast_ref::<()>(); - assert!(not_body.is_none()); - } - - #[actix_rt::test] - async fn test_to_bytes() { - let body = AnyBody::empty(); - let bytes = to_bytes(body).await.unwrap(); - assert!(bytes.is_empty()); - - let body = AnyBody::copy_from_slice(b"123"); - let bytes = to_bytes(body).await.unwrap(); - assert_eq!(bytes, b"123"[..]); - } -} +pub use self::utils::to_bytes; diff --git a/actix-http/src/body/none.rs b/actix-http/src/body/none.rs new file mode 100644 index 00000000..0fc7c8c9 --- /dev/null +++ b/actix-http/src/body/none.rs @@ -0,0 +1,43 @@ +use std::{ + convert::Infallible, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody}; + +/// Body type for responses that forbid payloads. +/// +/// Distinct from an empty response which would contain a Content-Length header. +/// +/// For an "empty" body, use `()` or `Bytes::new()`. +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct None; + +impl None { + /// Constructs new "none" body. + #[inline] + pub fn new() -> Self { + None + } +} + +impl MessageBody for None { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::None + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(Option::None) + } +} diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs index e238eada..d64af9d4 100644 --- a/actix-http/src/body/size.rs +++ b/actix-http/src/body/size.rs @@ -18,7 +18,7 @@ pub enum BodySize { } impl BodySize { - /// Returns true if size hint indicates no or empty body. + /// Returns true if size hint indicates omitted or empty body. /// /// Streams will return false because it cannot be known without reading the stream. /// diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index b92de44c..c8606897 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -32,6 +32,8 @@ where } } +// TODO: from_infallible method + impl MessageBody for SizedStream where S: Stream>, diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs new file mode 100644 index 00000000..a421ffd7 --- /dev/null +++ b/actix-http/src/body/utils.rs @@ -0,0 +1,78 @@ +use std::task::Poll; + +use actix_rt::pin; +use actix_utils::future::poll_fn; +use bytes::{Bytes, BytesMut}; +use futures_core::ready; + +use super::{BodySize, MessageBody}; + +/// Collects the body produced by a `MessageBody` implementation into `Bytes`. +/// +/// Any errors produced by the body stream are returned immediately. +/// +/// # Examples +/// ``` +/// use actix_http::body::{self, to_bytes}; +/// use bytes::Bytes; +/// +/// # async fn test_to_bytes() { +/// let body = body::None::new(); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert!(bytes.is_empty()); +/// +/// let body = Bytes::from_static(b"123"); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert_eq!(bytes, b"123"[..]); +/// # } +/// ``` +pub async fn to_bytes(body: B) -> Result { + let cap = match body.size() { + BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), + BodySize::Sized(size) => size as usize, + // good enough first guess for chunk size + BodySize::Stream => 32_768, + }; + + let mut buf = BytesMut::with_capacity(cap); + + pin!(body); + + poll_fn(|cx| loop { + let body = body.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf.freeze()) +} + +#[cfg(test)] +mod test { + use futures_util::{stream, StreamExt as _}; + + use super::*; + use crate::{body::BodyStream, Error}; + + #[actix_rt::test] + async fn test_to_bytes() { + let bytes = to_bytes(()).await.unwrap(); + assert!(bytes.is_empty()); + + let body = Bytes::from_static(b"123"); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123"[..]); + + let stream = + stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) + .map(Ok::<_, Error>); + let body = BodyStream::new(stream); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123abc"[..]); + } +} diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 4e68dc92..ca821f1d 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,10 +1,10 @@ -use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc}; +use std::{fmt, marker::PhantomData, net, rc::Rc}; use actix_codec::Framed; use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::{KeepAlive, ServiceConfig}, h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h2::H2Service, @@ -31,7 +31,7 @@ pub struct HttpServiceBuilder { impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, { @@ -54,11 +54,11 @@ where impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, X: ServiceFactory, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Error: fmt::Display, @@ -120,7 +120,7 @@ where where F: IntoServiceFactory, X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpServiceBuilder { @@ -178,7 +178,7 @@ where where B: MessageBody, F: IntoServiceFactory, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, { @@ -200,12 +200,11 @@ where pub fn h2(self, service: F) -> H2Service where F: IntoServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, @@ -223,12 +222,11 @@ where pub fn finish(self, service: F) -> HttpService where F: IntoServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { let cfg = ServiceConfig::new( self.keep_alive, diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 62100ff1..49e5663d 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -12,7 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle}; use bytes::Bytes; use derive_more::Display; use futures_core::ready; -use pin_project::pin_project; +use pin_project_lite::pin_project; #[cfg(feature = "compress-brotli")] use brotli2::write::BrotliEncoder; @@ -23,8 +23,10 @@ use flate2::write::{GzEncoder, ZlibEncoder}; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; +use super::Writer; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, MessageBody}, + error::BlockingError, http::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, @@ -32,84 +34,92 @@ use crate::{ ResponseHead, }; -use super::Writer; -use crate::error::BlockingError; - const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; -#[pin_project] -pub struct Encoder { - eof: bool, - #[pin] - body: EncoderBody, - encoder: Option, - fut: Option>>, +pin_project! { + pub struct Encoder { + #[pin] + body: EncoderBody, + encoder: Option, + fut: Option>>, + eof: bool, + } } impl Encoder { + fn none() -> Self { + Encoder { + body: EncoderBody::None, + encoder: None, + fut: None, + eof: true, + } + } + pub fn response( encoding: ContentEncoding, head: &mut ResponseHead, - body: AnyBody, - ) -> AnyBody> { + body: B, + ) -> Self { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT || encoding == ContentEncoding::Identity || encoding == ContentEncoding::Auto); - let body = match body { - AnyBody::None => return AnyBody::None, - AnyBody::Bytes(buf) => { - if can_encode { - EncoderBody::Bytes(buf) - } else { - return AnyBody::Bytes(buf); - } - } - AnyBody::Body(body) => EncoderBody::Stream(body), - }; + match body.size() { + // no need to compress an empty body + BodySize::None => return Self::none(), + + // we cannot assume that Sized is not a stream + BodySize::Sized(_) | BodySize::Stream => {} + } + + // TODO potentially some optimisation for single-chunk responses here by trying to read the + // payload eagerly, stopping after 2 polls if the first is a chunk and the second is None if can_encode { - // Modify response body only if encoder is not None + // Modify response body only if encoder is set if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); head.no_chunking(false); - return AnyBody::Body(Encoder { - body, - eof: false, - fut: None, + return Encoder { + body: EncoderBody::Stream { body }, encoder: Some(enc), - }); + fut: None, + eof: false, + }; } } - AnyBody::Body(Encoder { - body, - eof: false, - fut: None, + Encoder { + body: EncoderBody::Stream { body }, encoder: None, - }) + fut: None, + eof: false, + } } } -#[pin_project(project = EncoderBodyProj)] -enum EncoderBody { - Bytes(Bytes), - Stream(#[pin] B), +pin_project! { + #[project = EncoderBodyProj] + enum EncoderBody { + None, + Stream { #[pin] body: B }, + } } impl MessageBody for EncoderBody where B: MessageBody, { - type Error = EncoderError; + type Error = EncoderError; fn size(&self) -> BodySize { match self { - EncoderBody::Bytes(ref b) => b.size(), - EncoderBody::Stream(ref b) => b.size(), + EncoderBody::None => BodySize::None, + EncoderBody::Stream { body } => body.size(), } } @@ -118,14 +128,11 @@ where cx: &mut Context<'_>, ) -> Poll>> { match self.project() { - EncoderBodyProj::Bytes(b) => { - if b.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(std::mem::take(b)))) - } - } - EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), + EncoderBodyProj::None => Poll::Ready(None), + + EncoderBodyProj::Stream { body } => body + .poll_next(cx) + .map_err(|err| EncoderError::Body(err.into())), } } } @@ -134,7 +141,7 @@ impl MessageBody for Encoder where B: MessageBody, { - type Error = EncoderError; + type Error = EncoderError; fn size(&self) -> BodySize { if self.encoder.is_none() { @@ -197,6 +204,7 @@ where None => { if let Some(encoder) = this.encoder.take() { let chunk = encoder.finish().map_err(EncoderError::Io)?; + if chunk.is_empty() { return Poll::Ready(None); } else { @@ -222,12 +230,15 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { enum ContentEncoder { #[cfg(feature = "compress-gzip")] Deflate(ZlibEncoder), + #[cfg(feature = "compress-gzip")] Gzip(GzEncoder), + #[cfg(feature = "compress-brotli")] Br(BrotliEncoder), - // We need explicit 'static lifetime here because ZstdEncoder need lifetime - // argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static` + + // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we + // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. #[cfg(feature = "compress-zstd")] Zstd(ZstdEncoder<'static, Writer>), } @@ -240,20 +251,24 @@ impl ContentEncoder { Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + #[cfg(feature = "compress-zstd")] ContentEncoding::Zstd => { let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; Some(ContentEncoder::Zstd(encoder)) } + _ => None, } } @@ -263,10 +278,13 @@ impl ContentEncoder { match *self { #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), } @@ -279,16 +297,19 @@ impl ContentEncoder { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), @@ -307,6 +328,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -315,6 +337,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -323,6 +346,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), @@ -337,9 +361,9 @@ impl ContentEncoder { #[derive(Debug, Display)] #[non_exhaustive] -pub enum EncoderError { +pub enum EncoderError { #[display(fmt = "body")] - Body(E), + Body(Box), #[display(fmt = "blocking")] Blocking(BlockingError), @@ -348,18 +372,18 @@ pub enum EncoderError { Io(io::Error), } -impl StdError for EncoderError { +impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - EncoderError::Body(err) => Some(err), + EncoderError::Body(err) => Some(&**err), EncoderError::Blocking(err) => Some(err), EncoderError::Io(err) => Some(err), } } } -impl From> for crate::Error { - fn from(err: EncoderError) -> Self { +impl From for crate::Error { + fn from(err: EncoderError) -> Self { crate::Error::new_encoder().with_cause(err) } } diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs index cb271c63..d51dd66c 100644 --- a/actix-http/src/encoding/mod.rs +++ b/actix-http/src/encoding/mod.rs @@ -10,6 +10,9 @@ mod encoder; pub use self::decoder::Decoder; pub use self::encoder::Encoder; +/// Special-purpose writer for streaming (de-)compression. +/// +/// Pre-allocates 8KiB of capacity. pub(self) struct Writer { buf: BytesMut, } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 970c0c56..231e90e5 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -5,7 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err use derive_more::{Display, Error, From}; use http::{uri::InvalidUri, StatusCode}; -use crate::{body::AnyBody, ws, Response}; +use crate::{body::BoxBody, ws, Response}; pub use http::Error as HttpError; @@ -66,14 +66,15 @@ impl Error { } } -impl From for Response> { +impl From for Response { fn from(err: Error) -> Self { + // TODO: more appropriate error status codes, usage assessment needed let status_code = match err.inner.kind { Kind::Parse => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, }; - Response::new(status_code).set_body(AnyBody::from(err.to_string())) + Response::new(status_code).set_body(BoxBody::new(err.to_string())) } } @@ -132,12 +133,6 @@ impl From for Error { } } -impl From for Error { - fn from(err: ws::ProtocolError) -> Self { - Self::new_ws().with_cause(err) - } -} - impl From for Error { fn from(err: HttpError) -> Self { Self::new_http().with_cause(err) @@ -150,6 +145,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: ws::ProtocolError) -> Self { + Self::new_ws().with_cause(err) + } +} + /// A set of errors that can occur during parsing HTTP streams. #[derive(Debug, Display, Error)] #[non_exhaustive] @@ -240,7 +241,7 @@ impl From for Error { } } -impl From for Response { +impl From for Response { fn from(err: ParseError) -> Self { Error::from(err).into() } @@ -337,7 +338,7 @@ pub enum DispatchError { /// Service error // FIXME: display and error type #[display(fmt = "Service Error")] - Service(#[error(not(source))] Response), + Service(#[error(not(source))] Response), /// Body error // FIXME: display and error type @@ -421,11 +422,11 @@ mod tests { #[test] fn test_into_response() { - let resp: Response = ParseError::Incomplete.into(); + let resp: Response = ParseError::Incomplete.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: Response = Error::new_http().with_cause(err).into(); + let resp: Response = Error::new_http().with_cause(err).into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -450,7 +451,7 @@ mod tests { fn test_error_http_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); let err = Error::new_io().with_cause(orig); - let resp: Response = err.into(); + let resp: Response = err.into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 163d84f5..6695d1bf 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,6 +1,5 @@ use std::{ collections::VecDeque, - error::Error as StdError, fmt, future::Future, io, mem, net, @@ -19,7 +18,7 @@ use log::{error, trace}; use pin_project::pin_project; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, error::{DispatchError, ParseError, PayloadError}, service::HttpFlow, @@ -51,13 +50,12 @@ bitflags! { pub struct Dispatcher where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -73,13 +71,12 @@ where enum DispatcherState where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -92,13 +89,12 @@ where struct InnerDispatcher where S: Service, - S::Error: Into>, + S::Error: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -137,13 +133,12 @@ where X: Service, B: MessageBody, - B::Error: Into>, { None, ExpectCall(#[pin] X::Future), ServiceCall(#[pin] S::Future), SendPayload(#[pin] B), - SendErrorPayload(#[pin] AnyBody), + SendErrorPayload(#[pin] BoxBody), } impl State @@ -153,7 +148,6 @@ where X: Service, B: MessageBody, - B::Error: Into>, { fn is_empty(&self) -> bool { matches!(self, State::None) @@ -171,14 +165,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -232,14 +225,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -335,7 +327,7 @@ where fn send_error_response( mut self: Pin<&mut Self>, message: Response<()>, - body: AnyBody, + body: BoxBody, ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { @@ -380,7 +372,7 @@ where // send_response would update InnerDispatcher state to SendPayload or // None(If response body is empty). // continue loop to poll it. - self.as_mut().send_error_response(res, AnyBody::empty())?; + self.as_mut().send_error_response(res, BoxBody::new(()))?; } // return with upgrade request and poll it exclusively. @@ -400,7 +392,7 @@ where // send service call error as response Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -497,7 +489,7 @@ where // send expect error as response Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.as_mut().send_error_response(res, body)?; } @@ -546,7 +538,7 @@ where // to notify the dispatcher a new state is set and the outer loop // should be continue. Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); return self.send_error_response(res, body); } @@ -566,7 +558,7 @@ where Poll::Pending => Ok(()), // see the comment on ExpectCall state branch's Ready(Err(err)). Poll::Ready(Err(err)) => { - let res: Response = err.into(); + let res: Response = err.into(); let (res, body) = res.replace_body(()); self.send_error_response(res, body) } @@ -772,7 +764,7 @@ where trace!("Slow request timeout"); let _ = self.as_mut().send_error_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - AnyBody::empty(), + BoxBody::new(()), ); this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); @@ -909,14 +901,13 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -1067,17 +1058,19 @@ mod tests { } } - fn ok_service() -> impl Service, Error = Error> + fn ok_service( + ) -> impl Service, Error = Error> { fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) } fn echo_path_service( - ) -> impl Service, Error = Error> { + ) -> impl Service, Error = Error> + { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( - Response::ok().set_body(AnyBody::copy_from_slice(path)), + Response::ok().set_body(Bytes::copy_from_slice(path)), )) }) } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 8a50417d..70e83901 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, fmt, marker::PhantomData, net, @@ -16,7 +15,7 @@ use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::ServiceConfig, error::DispatchError, service::HttpServiceHandler, @@ -38,7 +37,7 @@ pub struct H1Service { impl H1Service where S: ServiceFactory, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, @@ -63,21 +62,20 @@ impl H1Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -114,16 +112,15 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -132,7 +129,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create OpenSSL based service. @@ -177,16 +174,15 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -195,7 +191,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create Rustls based service. @@ -226,7 +222,7 @@ mod rustls { impl H1Service where S: ServiceFactory, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, @@ -234,7 +230,7 @@ where pub fn expect(self, expect: X1) -> H1Service where X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { H1Service { @@ -277,21 +273,20 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -347,17 +342,16 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Response: Into>, B: MessageBody, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 2547f449..905585a3 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -1,22 +1,30 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use pin_project_lite::pin_project; -use crate::body::{BodySize, MessageBody}; -use crate::error::Error; -use crate::h1::{Codec, Message}; -use crate::response::Response; +use crate::{ + body::{BodySize, MessageBody}, + error::Error, + h1::{Codec, Message}, + response::Response, +}; -/// Send HTTP/1 response -#[pin_project::pin_project] -pub struct SendResponse { - res: Option, BodySize)>>, - #[pin] - body: Option, - #[pin] - framed: Option>, +pin_project! { + /// Send HTTP/1 response + pub struct SendResponse { + res: Option, BodySize)>>, + + #[pin] + body: Option, + + #[pin] + framed: Option>, + } } impl SendResponse diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 607997eb..6d2f4579 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -24,7 +24,7 @@ use log::{error, trace}; use pin_project_lite::pin_project; use crate::{ - body::{AnyBody, BodySize, MessageBody}, + body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, service::HttpFlow, OnConnectData, Payload, Request, Response, ResponseHead, @@ -51,7 +51,7 @@ where { pub(crate) fn new( flow: Rc>, - mut connection: Connection, + mut conn: Connection, on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, @@ -66,14 +66,14 @@ where }) .unwrap_or_else(|| Box::pin(sleep(dur))), on_flight: false, - ping_pong: connection.ping_pong().unwrap(), + ping_pong: conn.ping_pong().unwrap(), }); Self { flow, config, peer_addr, - connection, + connection: conn, on_connect_data, ping_pong, _phantom: PhantomData, @@ -92,12 +92,11 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into>, + S::Error: Into>, S::Future: 'static, S::Response: Into>, B: MessageBody, - B::Error: Into>, { type Output = Result<(), crate::error::DispatchError>; @@ -132,7 +131,7 @@ where let res = match fut.await { Ok(res) => handle_response(res.into(), tx, config).await, Err(err) => { - let res: Response = err.into(); + let res: Response = err.into(); handle_response(res, tx, config).await } }; @@ -207,7 +206,6 @@ async fn handle_response( ) -> Result<(), DispatchError> where B: MessageBody, - B::Error: Into>, { let (res, body) = res.replace_body(()); diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 0ad17ec0..8a9061b9 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, future::Future, marker::PhantomData, net, @@ -19,7 +18,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use log::error; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, config::ServiceConfig, error::DispatchError, service::HttpFlow, @@ -39,12 +38,11 @@ pub struct H2Service { impl H2Service where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create new `H2Service` instance with config. pub(crate) fn with_config>( @@ -70,12 +68,11 @@ impl H2Service where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create plain TCP based service pub fn tcp( @@ -114,12 +111,11 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create OpenSSL based service. pub fn openssl( @@ -162,12 +158,11 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create Rustls based service. pub fn rustls( @@ -204,12 +199,11 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -244,7 +238,7 @@ where impl H2ServiceHandler where S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -267,11 +261,10 @@ impl Service<(T, Option)> for H2ServiceHandler, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, { type Response = (); type Error = DispatchError; @@ -320,7 +313,7 @@ pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -332,11 +325,10 @@ impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody, - B::Error: Into>, { type Output = Result<(), DispatchError>; diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index e0bed063..c8e1ce6d 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -46,8 +46,8 @@ pub trait Head: Default + 'static { #[derive(Debug)] pub struct RequestHead { - pub uri: Uri, pub method: Method, + pub uri: Uri, pub version: Version, pub headers: HeaderMap, pub extensions: RefCell, @@ -58,13 +58,13 @@ pub struct RequestHead { impl Default for RequestHead { fn default() -> RequestHead { RequestHead { - uri: Uri::default(), method: Method::default(), + uri: Uri::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - flags: Flags::empty(), - peer_addr: None, extensions: RefCell::new(Extensions::new()), + peer_addr: None, + flags: Flags::empty(), } } } @@ -192,6 +192,7 @@ impl RequestHead { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum RequestHeadType { Owned(RequestHead), Rc(Rc, Option), diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 47f1c37e..ad41094a 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -6,14 +6,15 @@ use std::{ }; use bytes::{Bytes, BytesMut}; +use bytestring::ByteString; use crate::{ - body::{AnyBody, MessageBody}, - error::Error, + body::{BoxBody, MessageBody}, extensions::Extensions, + header::{self, IntoHeaderValue}, http::{HeaderMap, StatusCode}, message::{BoxedResponseHead, ResponseHead}, - ResponseBuilder, + Error, ResponseBuilder, }; /// An HTTP response. @@ -22,13 +23,13 @@ pub struct Response { pub(crate) body: B, } -impl Response { +impl Response { /// Constructs a new response with default body. #[inline] pub fn new(status: StatusCode) -> Self { Response { head: BoxedResponseHead::new(status), - body: AnyBody::empty(), + body: BoxBody::new(()), } } @@ -189,6 +190,14 @@ impl Response { } } + #[inline] + pub fn map_into_boxed_body(self) -> Response + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } + /// Returns body, consuming this response. pub fn into_body(self) -> B { self.body @@ -223,81 +232,99 @@ impl Default for Response { } } -impl>, E: Into> From> - for Response +impl>, E: Into> From> + for Response { fn from(res: Result) -> Self { match res { Ok(val) => val.into(), - Err(err) => err.into().into(), + Err(err) => Response::from(err.into()), } } } -impl From for Response { +impl From for Response { fn from(mut builder: ResponseBuilder) -> Self { - builder.finish() + builder.finish().map_into_boxed_body() } } -impl From for Response { +impl From for Response { fn from(val: std::convert::Infallible) -> Self { match val {} } } -impl From<&'static str> for Response { +impl From<&'static str> for Response<&'static str> { fn from(val: &'static str) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From<&'static [u8]> for Response { +impl From<&'static [u8]> for Response<&'static [u8]> { fn from(val: &'static [u8]) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl<'a> From<&'a String> for Response { - fn from(val: &'a String) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) +impl From<&String> for Response { + fn from(val: &String) -> Self { + let mut res = Response::with_body(StatusCode::OK, val.clone()); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: Bytes) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { - Response::build(StatusCode::OK) - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res + } +} + +impl From for Response { + fn from(val: ByteString) -> Self { + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } #[cfg(test)] mod tests { use super::*; - use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; + use crate::{ + body::to_bytes, + http::header::{HeaderValue, CONTENT_TYPE, COOKIE}, + }; #[test] fn test_debug() { @@ -309,73 +336,73 @@ mod tests { assert!(dbg.contains("Response")); } - #[test] - fn test_into_response() { - let resp: Response = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); + #[actix_rt::test] + async fn test_into_response() { + let res = Response::from("test"); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = b"test".as_ref().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b"test".as_ref()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = "test".to_owned().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = (&"test".to_owned()).into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = BytesMut::from("test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); } } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index c5fcb625..0537112d 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -2,19 +2,11 @@ use std::{ cell::{Ref, RefMut}, - error::Error as StdError, - fmt, - future::Future, - pin::Pin, - str, - task::{Context, Poll}, + fmt, str, }; -use bytes::Bytes; -use futures_core::Stream; - use crate::{ - body::{AnyBody, BodyStream}, + body::{EitherBody, MessageBody}, error::{Error, HttpError}, header::{self, IntoHeaderPair, IntoHeaderValue}, message::{BoxedResponseHead, ConnectionType, ResponseHead}, @@ -235,10 +227,14 @@ impl ResponseBuilder { /// Generate response with a wrapped body. /// /// This `ResponseBuilder` will be left in a useless state. - #[inline] - pub fn body>(&mut self, body: B) -> Response { - self.message_body(body.into()) - .unwrap_or_else(Response::from) + pub fn body(&mut self, body: B) -> Response> + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_body(|_, body| EitherBody::left(body)), + Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)), + } } /// Generate response with a body. @@ -253,24 +249,12 @@ impl ResponseBuilder { Ok(Response { head, body }) } - /// Generate response with a streaming body. - /// - /// This `ResponseBuilder` will be left in a useless state. - #[inline] - pub fn streaming(&mut self, stream: S) -> Response - where - S: Stream> + 'static, - E: Into> + 'static, - { - self.body(AnyBody::new_boxed(BodyStream::new(stream))) - } - /// Generate response with an empty body. /// /// This `ResponseBuilder` will be left in a useless state. #[inline] - pub fn finish(&mut self) -> Response { - self.body(AnyBody::empty()) + pub fn finish(&mut self) -> Response> { + self.body(()) } /// Create an owned `ResponseBuilder`, leaving the original in a useless state. @@ -327,14 +311,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { } } -impl Future for ResponseBuilder { - type Output = Result, Error>; - - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { - Poll::Ready(Ok(self.finish())) - } -} - impl fmt::Debug for ResponseBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let head = self.head.as_ref().unwrap(); @@ -356,8 +332,9 @@ impl fmt::Debug for ResponseBuilder { #[cfg(test)] mod tests { + use bytes::Bytes; + use super::*; - use crate::body::AnyBody; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; #[test] @@ -383,20 +360,28 @@ mod tests { #[test] fn test_force_close() { let resp = Response::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive()) + assert!(!resp.keep_alive()); } #[test] fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(AnyBody::empty()); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") + .body(Bytes::new()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); + + let resp = Response::build(StatusCode::OK) + .content_type(mime::APPLICATION_JAVASCRIPT_UTF_8) + .body(Bytes::new()); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + "application/javascript; charset=utf-8" + ); } #[test] fn test_into_builder() { - let mut resp: Response = "test".into(); + let mut resp: Response<_> = "test".into(); assert_eq!(resp.status(), StatusCode::OK); resp.headers_mut().insert( diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index fb0cccb3..7af34ba0 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, fmt, future::Future, marker::PhantomData, @@ -15,10 +14,10 @@ use actix_service::{ fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; use futures_core::{future::LocalBoxFuture, ready}; -use pin_project::pin_project; +use pin_project_lite::pin_project; use crate::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, MessageBody}, builder::HttpServiceBuilder, config::{KeepAlive, ServiceConfig}, error::DispatchError, @@ -38,7 +37,7 @@ pub struct HttpService { impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -53,12 +52,11 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { /// Create new `HttpService` instance. pub fn new>(service: F) -> Self { @@ -93,7 +91,7 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -107,7 +105,7 @@ where pub fn expect(self, expect: X1) -> HttpService where X1: ServiceFactory, - X1::Error: Into>, + X1::Error: Into>, X1::InitError: fmt::Debug, { HttpService { @@ -151,17 +149,16 @@ impl HttpService where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -170,7 +167,7 @@ where Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -208,17 +205,16 @@ mod openssl { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -227,7 +223,7 @@ mod openssl { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create OpenSSL based service. @@ -281,17 +277,16 @@ mod rustls { where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory< @@ -300,7 +295,7 @@ mod rustls { Response = (), >, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create Rustls based service. @@ -348,22 +343,21 @@ where S: ServiceFactory, S::Future: 'static, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, X: ServiceFactory, X::Future: 'static, - X::Error: Into>, + X::Error: Into>, X::InitError: fmt::Debug, U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Future: 'static, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -426,11 +420,11 @@ where impl HttpServiceHandler where S: Service, - S::Error: Into>, + S::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed)>, - U::Error: Into>, + U::Error: Into>, { pub(super) fn new( cfg: ServiceConfig, @@ -450,7 +444,7 @@ where pub(super) fn _poll_ready( &self, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; @@ -486,18 +480,17 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into>, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; @@ -519,23 +512,27 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { - state: State::H2Handshake(Some(( - h2::handshake_with_timeout(io, &self.cfg), - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - ))), + state: State::H2Handshake { + handshake: Some(( + h2::handshake_with_timeout(io, &self.cfg), + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + )), + }, }, Protocol::Http1 => HttpServiceHandlerResponse { - state: State::H1(h1::Dispatcher::new( - io, - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - )), + state: State::H1 { + dispatcher: h1::Dispatcher::new( + io, + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + ), + }, }, proto => unimplemented!("Unsupported HTTP version: {:?}.", proto), @@ -543,58 +540,65 @@ where } } -#[pin_project(project = StateProj)] -enum State -where - T: AsyncRead + AsyncWrite + Unpin, +pin_project! { + #[project = StateProj] + enum State + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, - S: Service, - S::Future: 'static, - S::Error: Into>, + S: Service, + S::Future: 'static, + S::Error: Into>, - B: MessageBody, - B::Error: Into>, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - H1(#[pin] h1::Dispatcher), - H2(#[pin] h2::Dispatcher), - H2Handshake( - Option<( - h2::HandshakeWithTimeout, - ServiceConfig, - Rc>, - OnConnectData, - Option, - )>, - ), + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + H1 { #[pin] dispatcher: h1::Dispatcher }, + H2 { #[pin] dispatcher: h2::Dispatcher }, + H2Handshake { + handshake: Option<( + h2::HandshakeWithTimeout, + ServiceConfig, + Rc>, + OnConnectData, + Option, + )>, + }, + } } -#[pin_project] -pub struct HttpServiceHandlerResponse -where - T: AsyncRead + AsyncWrite + Unpin, +pin_project! { + pub struct HttpServiceHandlerResponse + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, - S: Service, - S::Error: Into> + 'static, - S::Future: 'static, - S::Response: Into> + 'static, + S: Service, + S::Error: Into>, + S::Error: 'static, + S::Future: 'static, + S::Response: Into>, + S::Response: 'static, - B: MessageBody, - B::Error: Into>, + B: MessageBody, - X: Service, - X::Error: Into>, + X: Service, + X::Error: Into>, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - #[pin] - state: State, + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + #[pin] + state: State, + } } impl Future for HttpServiceHandlerResponse @@ -602,15 +606,14 @@ where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into> + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, - B::Error: Into>, X: Service, - X::Error: Into>, + X::Error: Into>, U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, @@ -619,23 +622,24 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project().state.project() { - StateProj::H1(disp) => disp.poll(cx), - StateProj::H2(disp) => disp.poll(cx), - StateProj::H2Handshake(data) => { + StateProj::H1 { dispatcher } => dispatcher.poll(cx), + StateProj::H2 { dispatcher } => dispatcher.poll(cx), + StateProj::H2Handshake { handshake: data } => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { Ok((conn, timer)) => { - let (_, cfg, srv, on_connect_data, peer_addr) = + let (_, config, flow, on_connect_data, peer_addr) = data.take().unwrap(); - self.as_mut().project().state.set(State::H2( - h2::Dispatcher::new( - srv, + + self.as_mut().project().state.set(State::H2 { + dispatcher: h2::Dispatcher::new( + flow, conn, on_connect_data, - cfg, + config, peer_addr, timer, ), - )); + }); self.poll(cx) } Err(err) => { diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index f49cbe5d..a3f766e9 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -4,17 +4,21 @@ use std::task::{Context, Poll}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; +use pin_project_lite::pin_project; use super::{Codec, Frame, Message}; -#[pin_project::pin_project] -pub struct Dispatcher -where - S: Service + 'static, - T: AsyncRead + AsyncWrite, -{ - #[pin] - inner: inner::Dispatcher, +pin_project! { + pub struct Dispatcher + where + S: Service, + S: 'static, + T: AsyncRead, + T: AsyncWrite, + { + #[pin] + inner: inner::Dispatcher, + } } impl Dispatcher @@ -72,7 +76,7 @@ mod inner { use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; - use crate::{body::AnyBody, Response}; + use crate::{body::BoxBody, Response}; /// Framed transport errors pub enum DispatcherError @@ -136,7 +140,7 @@ mod inner { } } - impl From> for Response + impl From> for Response where E: fmt::Debug + fmt::Display, U: Encoder + Decoder, @@ -144,7 +148,7 @@ mod inner { ::Error: fmt::Debug, { fn from(err: DispatcherError) -> Self { - Response::internal_server_error().set_body(AnyBody::from(err.to_string())) + Response::internal_server_error().set_body(BoxBody::new(err.to_string())) } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 70e0e62a..cb1aa673 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -8,9 +8,9 @@ use std::io; use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; +use crate::body::BoxBody; use crate::{ - body::AnyBody, header::HeaderValue, message::RequestHead, response::Response, - ResponseBuilder, + header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder, }; mod codec; @@ -69,7 +69,7 @@ pub enum ProtocolError { } /// WebSocket handshake errors -#[derive(Debug, PartialEq, Display, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. #[display(fmt = "Method not allowed.")] @@ -96,8 +96,8 @@ pub enum HandshakeError { BadWebsocketKey, } -impl From<&HandshakeError> for Response { - fn from(err: &HandshakeError) -> Self { +impl From for Response { + fn from(err: HandshakeError) -> Self { match err { HandshakeError::GetMethodRequired => { let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); @@ -139,9 +139,9 @@ impl From<&HandshakeError> for Response { } } -impl From for Response { - fn from(err: HandshakeError) -> Self { - (&err).into() +impl From<&HandshakeError> for Response { + fn from(err: &HandshakeError) -> Self { + (*err).into() } } @@ -220,9 +220,10 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { #[cfg(test)] mod tests { + use crate::{header, Method}; + use super::*; - use crate::{body::AnyBody, test::TestRequest}; - use http::{header, Method}; + use crate::test::TestRequest; #[test] fn test_handshake() { @@ -336,17 +337,17 @@ mod tests { #[test] fn test_ws_error_http_response() { - let resp: Response = HandshakeError::GetMethodRequired.into(); + let resp: Response = HandshakeError::GetMethodRequired.into(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); + let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoConnectionUpgrade.into(); + let resp: Response = HandshakeError::NoConnectionUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoVersionHeader.into(); + let resp: Response = HandshakeError::NoVersionHeader.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::UnsupportedVersion.into(); + let resp: Response = HandshakeError::UnsupportedVersion.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::BadWebsocketKey.into(); + let resp: Response = HandshakeError::BadWebsocketKey.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 414266d8..4c923873 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,7 +1,7 @@ use std::convert::Infallible; use actix_http::{ - body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, + body::BoxBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, }; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; @@ -99,7 +99,7 @@ async fn test_with_query_parameter() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl From for Response { +impl From for Response { fn from(_: ExpectFailed) -> Self { Response::new(StatusCode::EXPECTATION_FAILED) } diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index e7dd7817..86ee17c7 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -5,7 +5,7 @@ extern crate tls_openssl as openssl; use std::{convert::Infallible, io}; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderValue}, @@ -348,7 +348,7 @@ async fn test_h2_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .openssl(tls_config()) @@ -399,9 +399,11 @@ async fn test_h2_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(err: BadRequest) -> Self { - Response::build(StatusCode::BAD_REQUEST).body(err.to_string()) + Response::build(StatusCode::BAD_REQUEST) + .body(err.to_string()) + .map_into_boxed_body() } } @@ -409,7 +411,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .openssl(tls_config()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index b5289bf7..87375277 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{BodyStream, BoxBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, @@ -416,7 +416,7 @@ async fn test_h2_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .rustls(tls_config()) @@ -467,9 +467,9 @@ async fn test_h2_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(_: BadRequest) -> Self { - Response::bad_request().set_body(AnyBody::from("error")) + Response::bad_request().set_body(BoxBody::new("error")) } } @@ -477,7 +477,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; @@ -494,7 +494,7 @@ async fn test_h2_service_error() { async fn test_h1_service_error() { let mut srv = test_server(move || { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 11bc8e93..adf2a28c 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -6,7 +6,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, SizedStream}, + body::{self, BodyStream, BoxBody, SizedStream}, header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, }; @@ -69,7 +69,7 @@ async fn test_h1_2() { #[display(fmt = "expect failed")] struct ExpectFailed; -impl From for Response { +impl From for Response { fn from(_: ExpectFailed) -> Self { Response::new(StatusCode::EXPECTATION_FAILED) } @@ -622,7 +622,7 @@ async fn test_h1_body_chunked_explicit() { ok::<_, Infallible>( Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .tcp() @@ -656,7 +656,9 @@ async fn test_h1_body_chunked_implicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body)) + ok::<_, Infallible>( + Response::build(StatusCode::OK).body(BodyStream::new(body)), + ) }) .tcp() }) @@ -714,9 +716,9 @@ async fn test_h1_response_http_error_handling() { #[display(fmt = "error")] struct BadRequest; -impl From for Response { +impl From for Response { fn from(_: BadRequest) -> Self { - Response::bad_request().set_body(AnyBody::from("error")) + Response::bad_request().set_body(BoxBody::new("error")) } } @@ -724,7 +726,7 @@ impl From for Response { async fn test_h1_service_error() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .tcp() }) .await; @@ -773,36 +775,35 @@ async fn test_not_modified_spec_h1() { let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { - let res: Response = match req.path() { + let res: Response = match req.path() { // with no content-length "/none" => { - Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None) + Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) + .map_into_boxed_body() } // with no content-length - "/body" => Response::with_body( - StatusCode::NOT_MODIFIED, - AnyBody::from("1234"), - ), + "/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") + .map_into_boxed_body(), // with manual content-length header and specific None body "/cl-none" => { - let mut res = - Response::with_body(StatusCode::NOT_MODIFIED, AnyBody::None); + let mut res = Response::with_body( + StatusCode::NOT_MODIFIED, + body::None::new(), + ); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("24")); - res + res.map_into_boxed_body() } // with manual content-length header and ignore-able body "/cl-body" => { - let mut res = Response::with_body( - StatusCode::NOT_MODIFIED, - AnyBody::from("1234"), - ); + let mut res = + Response::with_body(StatusCode::NOT_MODIFIED, "1234"); res.headers_mut() .insert(CL.clone(), header::HeaderValue::from_static("4")); - res + res.map_into_boxed_body() } _ => panic!("unknown route"), diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 6d0de231..c9138201 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -6,7 +6,7 @@ use std::{ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{ - body::{AnyBody, BodySize}, + body::{BodySize, BoxBody}, h1, ws::{self, CloseCode, Frame, Item, Message}, Error, HttpService, Request, Response, @@ -50,14 +50,14 @@ enum WsServiceError { Dispatcher, } -impl From for Response { +impl From for Response { fn from(err: WsServiceError) -> Self { match err { WsServiceError::Http(err) => err.into(), WsServiceError::Ws(err) => err.into(), WsServiceError::Io(_err) => unreachable!(), WsServiceError::Dispatcher => Response::internal_server_error() - .set_body(AnyBody::from(format!("{}", err))), + .set_body(BoxBody::new(format!("{}", err))), } } } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index b80918ec..1decd6e9 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -31,7 +31,7 @@ extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] extern crate tls_rustls as rustls; -use std::{error::Error as StdError, fmt, net, thread, time::Duration}; +use std::{fmt, net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; @@ -41,7 +41,8 @@ use actix_http::{ }; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_web::{ - dev::{AppConfig, MessageBody, Server, ServerHandle, Service}, + body::MessageBody, + dev::{AppConfig, Server, ServerHandle, Service}, rt::{self, System}, web, Error, }; @@ -88,7 +89,6 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { start_with(TestServerConfig::default(), factory) } @@ -128,7 +128,6 @@ where S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, - B::Error: Into>, { // for sending handles and server info back from the spawned thread let (started_tx, started_rx) = std::sync::mpsc::channel(); diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index e7b8cd0f..28b5b29e 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -22,7 +22,7 @@ actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } -pin-project = "1.0.0" +pin-project-lite = "0.2" tokio = { version = "1", features = ["sync"] } [dev-dependencies] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index f0a53d4e..b6323c2e 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -30,6 +30,7 @@ use actix_web::{ use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::Stream; +use pin_project_lite::pin_project; use tokio::sync::oneshot::Sender; /// Perform WebSocket handshake and start actor. @@ -462,13 +463,14 @@ where } } -#[pin_project::pin_project] -struct WsStream { - #[pin] - stream: S, - decoder: Codec, - buf: BytesMut, - closed: bool, +pin_project! { + struct WsStream { + #[pin] + stream: S, + decoder: Codec, + buf: BytesMut, + closed: bool, + } } impl WsStream diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 0a8e50b3..e481b283 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -78,7 +78,7 @@ async fn test_with_credentials() { match srv.ws().await { Ok(_) => panic!("WebSocket client without credentials should panic"), Err(awc::error::WsClientError::InvalidResponseStatus(status)) => { - assert_eq!(status, StatusCode::UNAUTHORIZED) + assert_eq!(status, StatusCode::UNAUTHORIZED); } Err(e) => panic!("Invalid error from WebSocket client: {}", e), } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 851e5cd4..fc60f5ed 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -105,6 +105,7 @@ brotli2 = "0.3.2" env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } +static_assertions = "1.1" rcgen = "0.8" rustls-pemfile = "0.2" diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs new file mode 100644 index 00000000..cb9038ff --- /dev/null +++ b/awc/src/any_body.rs @@ -0,0 +1,266 @@ +use std::{ + borrow::Cow, + fmt, mem, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::{Bytes, BytesMut}; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; + +use crate::BoxError; + +pin_project! { + /// Represents various types of HTTP message body. + #[derive(Clone)] + #[project = AnyBodyProj] + pub enum AnyBody { + /// Empty response. `Content-Length` header is not set. + None, + + /// Complete, in-memory response body. + Bytes { body: Bytes }, + + /// Generic / Other message body. + Body { #[pin] body: B }, + } +} + +impl AnyBody { + /// Constructs a "body" representing an empty response. + pub fn none() -> Self { + Self::None + } + + /// Constructs a new, 0-length body. + pub fn empty() -> Self { + Self::Bytes { body: Bytes::new() } + } + + /// Create boxed body from generic message body. + pub fn new_boxed(body: B) -> Self + where + B: MessageBody + 'static, + { + Self::Body { + body: BoxBody::new(body), + } + } + + /// Constructs new `AnyBody` instance from a slice of bytes by copying it. + /// + /// If your bytes container is owned, it may be cheaper to use a `From` impl. + pub fn copy_from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] + pub fn from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } +} + +impl AnyBody { + /// Create body from generic message body. + pub fn new(body: B) -> Self { + Self::Body { body } + } +} + +impl AnyBody +where + B: MessageBody + 'static, +{ + pub fn into_boxed(self) -> AnyBody { + match self { + Self::None => AnyBody::None, + Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Body { body } => AnyBody::new_boxed(body), + } + } +} + +impl MessageBody for AnyBody +where + B: MessageBody, +{ + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + match self { + AnyBody::None => BodySize::None, + AnyBody::Bytes { ref body } => BodySize::Sized(body.len() as u64), + AnyBody::Body { ref body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + AnyBodyProj::None => Poll::Ready(None), + AnyBodyProj::Bytes { body } => { + let len = body.len(); + if len == 0 { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(mem::take(body)))) + } + } + + AnyBodyProj::Body { body } => body.poll_next(cx).map_err(|err| err.into()), + } + } +} + +impl PartialEq for AnyBody { + fn eq(&self, other: &AnyBody) -> bool { + match self { + AnyBody::None => matches!(*other, AnyBody::None), + AnyBody::Bytes { body } => match other { + AnyBody::Bytes { body: b2 } => body == b2, + _ => false, + }, + AnyBody::Body { .. } => false, + } + } +} + +impl fmt::Debug for AnyBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AnyBody::None => write!(f, "AnyBody::None"), + AnyBody::Bytes { ref body } => write!(f, "AnyBody::Bytes({:?})", body), + AnyBody::Body { ref body } => write!(f, "AnyBody::Message({:?})", body), + } + } +} + +impl From<&'static str> for AnyBody { + fn from(string: &'static str) -> Self { + Self::Bytes { + body: Bytes::from_static(string.as_ref()), + } + } +} + +impl From<&'static [u8]> for AnyBody { + fn from(bytes: &'static [u8]) -> Self { + Self::Bytes { + body: Bytes::from_static(bytes), + } + } +} + +impl From> for AnyBody { + fn from(vec: Vec) -> Self { + Self::Bytes { + body: Bytes::from(vec), + } + } +} + +impl From for AnyBody { + fn from(string: String) -> Self { + Self::Bytes { + body: Bytes::from(string), + } + } +} + +impl From<&'_ String> for AnyBody { + fn from(string: &String) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), + } + } +} + +impl From> for AnyBody { + fn from(string: Cow<'_, str>) -> Self { + match string { + Cow::Owned(s) => Self::from(s), + Cow::Borrowed(s) => Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), + }, + } + } +} + +impl From for AnyBody { + fn from(bytes: Bytes) -> Self { + Self::Bytes { body: bytes } + } +} + +impl From for AnyBody { + fn from(bytes: BytesMut) -> Self { + Self::Bytes { + body: bytes.freeze(), + } + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: SizedStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: BodyStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomPinned; + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + + struct PinType(PhantomPinned); + + impl MessageBody for PinType { + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + unimplemented!() + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + unimplemented!() + } + } + + assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(AnyBody: MessageBody); + + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); +} diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 6bbc9ad0..0e1f0bfe 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -12,9 +12,9 @@ use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use actix_http::{ - body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead, -}; +use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; + +use crate::BoxError; use super::error::SendRequestError; use super::pool::Acquired; @@ -254,7 +254,7 @@ where where H: Into + 'static, RB: MessageBody + 'static, - RB::Error: Into, + RB::Error: Into, { Box::pin(async move { match self { diff --git a/awc/src/client/error.rs b/awc/src/client/error.rs index 68ffb6fb..d351b5d5 100644 --- a/awc/src/client/error.rs +++ b/awc/src/client/error.rs @@ -1,13 +1,13 @@ -use std::{error::Error as StdError, fmt, io}; +use std::{fmt, io}; use derive_more::{Display, From}; -use actix_http::{ - error::{Error, ParseError}, - http::Error as HttpError, -}; +use actix_http::{error::ParseError, http::Error as HttpError}; + #[cfg(feature = "openssl")] -use actix_tls::accept::openssl::reexports::Error as OpenSslError; +use actix_tls::accept::openssl::reexports::Error as OpensslError; + +use crate::BoxError; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] @@ -20,7 +20,7 @@ pub enum ConnectError { /// SSL error #[cfg(feature = "openssl")] #[display(fmt = "{}", _0)] - SslError(OpenSslError), + SslError(OpensslError), /// Failed to resolve the hostname #[display(fmt = "Failed resolving hostname: {}", _0)] @@ -118,11 +118,11 @@ pub enum SendRequestError { TunnelNotSupported, /// Error sending request body - Body(Error), + Body(BoxError), /// Other errors that can occur after submitting a request. #[display(fmt = "{:?}: {}", _1, _0)] - Custom(Box, Box), + Custom(BoxError, Box), } impl std::error::Error for SendRequestError {} @@ -141,7 +141,7 @@ pub enum FreezeRequestError { /// Other errors that can occur after submitting a request. #[display(fmt = "{:?}: {}", _1, _0)] - Custom(Box, Box), + Custom(BoxError, Box), } impl std::error::Error for FreezeRequestError {} diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index c442cd4c..b26a97ee 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -13,7 +13,7 @@ use actix_http::{ header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, StatusCode, }, - Error, Payload, RequestHeadType, ResponseHead, + Payload, RequestHeadType, ResponseHead, }; use actix_utils::future::poll_fn; use bytes::buf::BufMut; @@ -22,6 +22,8 @@ use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; +use crate::BoxError; + use super::connection::{ConnectionIo, H1Connection}; use super::error::{ConnectError, SendRequestError}; @@ -33,7 +35,7 @@ pub(crate) async fn send_request( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { // set request host header if !head.as_ref().headers.contains_key(HOST) @@ -155,7 +157,7 @@ pub(crate) async fn send_body( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { actix_rt::pin!(body); @@ -166,7 +168,7 @@ where Some(Ok(chunk)) => { framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?; } - Some(Err(err)) => return Err(err.into().into()), + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), None => { eof = true; framed.as_mut().write(h1::Message::Chunk(None))?; diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 66fb790a..9ced5776 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -13,9 +13,11 @@ use log::trace; use actix_http::{ body::{BodySize, MessageBody}, header::HeaderMap, - Error, Payload, RequestHeadType, ResponseHead, + Payload, RequestHeadType, ResponseHead, }; +use crate::BoxError; + use super::{ config::ConnectorConfig, connection::{ConnectionIo, H2Connection}, @@ -30,7 +32,7 @@ pub(crate) async fn send_request( where Io: ConnectionIo, B: MessageBody, - B::Error: Into, + B::Error: Into, { trace!("Sending client request: {:?} {:?}", head, body.size()); @@ -133,10 +135,12 @@ where async fn send_body(body: B, mut send: SendStream) -> Result<(), SendRequestError> where B: MessageBody, - B::Error: Into, + B::Error: Into, { let mut buf = None; + actix_rt::pin!(body); + loop { if buf.is_none() { match poll_fn(|cx| body.as_mut().poll_next(cx)).await { @@ -144,10 +148,10 @@ where send.reserve_capacity(b.len()); buf = Some(b); } - Some(Err(e)) => return Err(e.into().into()), + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), None => { - if let Err(e) = send.send_data(Bytes::new(), true) { - return Err(e.into()); + if let Err(err) = send.send_data(Bytes::new(), true) { + return Err(err.into()); } send.reserve_capacity(0); return Ok(()); diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 05f2a649..19870b06 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -7,16 +7,17 @@ use std::{ }; use actix_codec::Framed; -use actix_http::{ - body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, -}; +use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead}; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; -use crate::client::{ - Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, +use crate::{ + any_body::AnyBody, + client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, + }, + response::ClientResponse, }; -use crate::response::ClientResponse; pub type BoxConnectorService = Rc< dyn Service< diff --git a/awc/src/error.rs b/awc/src/error.rs index d415efe9..726e1a50 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -11,6 +11,8 @@ use serde_json::error::Error as JsonError; pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; +// TODO: address display, error, and from impls + /// Websocket client error #[derive(Debug, Display, From)] pub enum WsClientError { diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 46a00b00..47239735 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,18 +1,18 @@ -use std::{convert::TryFrom, error::Error as StdError, net, rc::Rc, time::Duration}; +use std::{convert::TryFrom, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::AnyBody, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, RequestHead, }; use crate::{ + any_body::AnyBody, sender::{RequestSender, SendClientRequest}, - ClientConfig, + BoxError, ClientConfig, }; /// `FrozenClientRequest` struct represents cloneable client request. @@ -82,7 +82,7 @@ impl FrozenClientRequest { pub fn send_stream(&self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( self.addr, @@ -207,7 +207,7 @@ impl FrozenSendBuilder { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index fc99419e..2f418312 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -104,6 +104,7 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +mod any_body; mod builder; mod client; mod connect; @@ -139,6 +140,8 @@ use actix_service::Service; use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; +pub(crate) type BoxError = Box; + /// An asynchronous HTTP and WebSocket client. /// /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 12a71f7c..89cff22c 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -8,7 +8,6 @@ use std::{ }; use actix_http::{ - body::AnyBody, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -17,10 +16,12 @@ use bytes::Bytes; use futures_core::ready; use super::Transform; - -use crate::client::{InvalidUrl, SendRequestError}; -use crate::connect::{ConnectRequest, ConnectResponse}; -use crate::ClientResponse; +use crate::{ + any_body::AnyBody, + client::{InvalidUrl, SendRequestError}, + connect::{ConnectRequest, ConnectResponse}, + ClientResponse, +}; pub struct Redirect { max_redirect_times: u8, @@ -95,7 +96,7 @@ where }; let body_opt = match body { - AnyBody::Bytes(ref b) => Some(b.clone()), + AnyBody::Bytes { ref body } => Some(body.clone()), _ => None, }; @@ -192,7 +193,9 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => AnyBody::Bytes(bytes.clone()), + Some(ref bytes) => AnyBody::Bytes { + body: bytes.clone(), + }, // TODO: should this be AnyBody::Empty or AnyBody::None. _ => AnyBody::empty(), } diff --git a/awc/src/request.rs b/awc/src/request.rs index f364b43c..d26b703f 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,11 +1,10 @@ -use std::{convert::TryFrom, error::Error as StdError, fmt, net, rc::Rc, time::Duration}; +use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::AnyBody, http::{ header::{self, IntoHeaderPair}, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, @@ -13,15 +12,17 @@ use actix_http::{ RequestHead, }; -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; use crate::{ + any_body::AnyBody, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, - ClientConfig, + BoxError, ClientConfig, }; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; + /// An HTTP Client request builder /// /// This type can be used to construct an instance of `ClientRequest` through a @@ -404,7 +405,7 @@ impl ClientRequest { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 7e1bcd64..51fce191 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -1,5 +1,4 @@ use std::{ - error::Error as StdError, future::Future, net, pin::Pin, @@ -9,12 +8,12 @@ use std::{ }; use actix_http::{ - body::{AnyBody, BodyStream}, + body::BodyStream, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, }, - Error, RequestHead, RequestHeadType, + RequestHead, RequestHeadType, }; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; @@ -26,8 +25,9 @@ use serde::Serialize; use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; use crate::{ + any_body::AnyBody, error::{FreezeRequestError, InvalidUrl, SendRequestError}, - ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, + BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, }; #[derive(Debug, From)] @@ -162,12 +162,6 @@ impl From for SendClientRequest { } } -impl From for SendClientRequest { - fn from(e: Error) -> Self { - SendClientRequest::Err(Some(e.into())) - } -} - impl From for SendClientRequest { fn from(e: HttpError) -> Self { SendClientRequest::Err(Some(e.into())) @@ -236,7 +230,9 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -265,7 +261,9 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -279,7 +277,7 @@ impl RequestSender { ) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into> + 'static, + E: Into + 'static, { self.send_body( addr, diff --git a/benches/responder.rs b/benches/responder.rs index 5d0b98d5..20aae335 100644 --- a/benches/responder.rs +++ b/benches/responder.rs @@ -1,9 +1,10 @@ use std::{future::Future, time::Instant}; +use actix_http::body::BoxBody; use actix_utils::future::{ready, Ready}; -use actix_web::http::StatusCode; -use actix_web::test::TestRequest; -use actix_web::{error, Error, HttpRequest, HttpResponse, Responder}; +use actix_web::{ + error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder, +}; use criterion::{criterion_group, criterion_main, Criterion}; use futures_util::future::{join_all, Either}; @@ -50,7 +51,9 @@ where } impl Responder for StringResponder { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) @@ -58,9 +61,11 @@ impl Responder for StringResponder { } impl Responder for OptionResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self.0 { - Some(t) => t.respond_to(req), + Some(t) => t.respond_to(req).map_into_boxed_body(), None => HttpResponse::from_error(error::ErrorInternalServerError("err")), } } diff --git a/src/app.rs b/src/app.rs index a291a959..36063ec7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,37 +1,35 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::marker::PhantomData; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::body::{AnyBody, MessageBody}; -use actix_http::{Extensions, Request}; -use actix_service::boxed::{self, BoxServiceFactory}; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, Request, +}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, + apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, + Transform, }; use futures_util::future::FutureExt as _; -use crate::app_service::{AppEntry, AppInit, AppRoutingFactory}; -use crate::config::ServiceConfig; -use crate::data::{Data, DataFactory, FnDataFactory}; -use crate::dev::ResourceDef; -use crate::error::Error; -use crate::resource::Resource; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, - ServiceResponse, +use crate::{ + app_service::{AppEntry, AppInit, AppRoutingFactory}, + config::ServiceConfig, + data::{Data, DataFactory, FnDataFactory}, + dev::ResourceDef, + error::Error, + resource::Resource, + route::Route, + service::{ + AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, + ServiceRequest, ServiceResponse, + }, }; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; - /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App { endpoint: T, services: Vec>, - default: Option>, + default: Option>, factory_ref: Rc>>, data_factories: Vec, external: Vec, @@ -39,7 +37,7 @@ pub struct App { _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -287,7 +285,7 @@ where /// ); /// } /// ``` - pub fn default_service(mut self, f: F) -> Self + pub fn default_service(mut self, svc: F) -> Self where F: IntoServiceFactory, U: ServiceFactory< @@ -298,10 +296,12 @@ where > + 'static, U::InitError: fmt::Debug, { - // create and configure default resource - self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err( - |e| log::error!("Can not construct default service: {:?}", e), - )))); + let svc = svc + .into_factory() + .map(|res| res.map_into_boxed_body()) + .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); + + self.default = Some(Rc::new(boxed::factory(svc))); self } diff --git a/src/app_service.rs b/src/app_service.rs index cf34b302..bca8f262 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -2,10 +2,7 @@ use std::{cell::RefCell, mem, rc::Rc}; use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Router, Url}; -use actix_service::{ - boxed::{self, BoxService, BoxServiceFactory}, - fn_service, Service, ServiceFactory, -}; +use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -15,13 +12,14 @@ use crate::{ guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, - service::{AppServiceFactory, ServiceRequest, ServiceResponse}, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, ServiceRequest, + ServiceResponse, + }, Error, HttpResponse, }; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes data factories. @@ -39,7 +37,7 @@ where pub(crate) extensions: RefCell>, pub(crate) async_data_factories: Rc<[FnDataFactory]>, pub(crate) services: Rc>>>, - pub(crate) default: Option>, + pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, } @@ -230,8 +228,14 @@ where } pub struct AppRoutingFactory { - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for AppRoutingFactory { @@ -279,8 +283,8 @@ impl ServiceFactory for AppRoutingFactory { /// The Actix Web router default entry point. pub struct AppRouting { - router: Router, - default: HttpService, + router: Router, + default: BoxedHttpService, } impl Service for AppRouting { diff --git a/src/data.rs b/src/data.rs index d27ad196..b29e4ecf 100644 --- a/src/data.rs +++ b/src/data.rs @@ -284,7 +284,7 @@ mod tests { async fn test_data_from_arc() { let data_new = Data::new(String::from("test-123")); let data_from_arc = Data::from(Arc::new(String::from("test-123"))); - assert_eq!(data_new.0, data_from_arc.0) + assert_eq!(data_new.0, data_from_arc.0); } #[actix_rt::test] diff --git a/src/dev.rs b/src/dev.rs index 59805b82..d4a64985 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,7 +1,7 @@ //! Lower-level types and re-exports. //! //! Most users will not have to interact with the types in this module, but it is useful for those -//! writing extractors, middleware and libraries, or interacting with the service API directly. +//! writing extractors, middleware, libraries, or interacting with the service API directly. pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] @@ -14,11 +14,6 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -#[allow(deprecated)] -pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; - -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::{Server, ServerHandle}; @@ -26,8 +21,10 @@ pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + use crate::http::header::ContentEncoding; -use actix_http::ResponseBuilder; use actix_router::Patterns; @@ -62,7 +59,7 @@ pub trait BodyEncoding { fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; } -impl BodyEncoding for ResponseBuilder { +impl BodyEncoding for actix_http::ResponseBuilder { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) } @@ -73,7 +70,7 @@ impl BodyEncoding for ResponseBuilder { } } -impl BodyEncoding for Response { +impl BodyEncoding for actix_http::Response { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) } @@ -105,3 +102,41 @@ impl BodyEncoding for crate::HttpResponse { self } } + +// TODO: remove this if it doesn't appear to be needed + +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum AnyBody { + None, + Full { body: crate::web::Bytes }, + Boxed { body: actix_http::body::BoxBody }, +} + +impl crate::body::MessageBody for AnyBody { + type Error = crate::BoxError; + + /// Body size hint. + fn size(&self) -> crate::body::BodySize { + match self { + AnyBody::None => crate::body::BodySize::None, + AnyBody::Full { body } => body.size(), + AnyBody::Boxed { body } => body.size(), + } + } + + /// Attempt to pull out the next chunk of body bytes. + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll>> { + match self.get_mut() { + AnyBody::None => std::task::Poll::Ready(None), + AnyBody::Full { body } => { + let bytes = std::mem::take(body); + std::task::Poll::Ready(Some(Ok(bytes))) + } + AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx), + } + } +} diff --git a/src/error/error.rs b/src/error/error.rs index add29086..be17c196 100644 --- a/src/error/error.rs +++ b/src/error/error.rs @@ -1,6 +1,6 @@ use std::{error::Error as StdError, fmt}; -use actix_http::{body::AnyBody, Response}; +use actix_http::{body::BoxBody, Response}; use crate::{HttpResponse, ResponseError}; @@ -69,8 +69,8 @@ impl From for Error { } } -impl From for Response { - fn from(err: Error) -> Response { +impl From for Response { + fn from(err: Error) -> Response { err.error_response().into() } } diff --git a/src/error/internal.rs b/src/error/internal.rs index 3d99012d..c766ba83 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -1,6 +1,10 @@ use std::{cell::RefCell, fmt, io::Write as _}; -use actix_http::{body::AnyBody, header, StatusCode}; +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue as _}, + StatusCode, +}; use bytes::{BufMut as _, BytesMut}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; @@ -84,11 +88,10 @@ where let mut buf = BytesMut::new().writer(); let _ = write!(buf, "{}", self); - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); - res.set_body(AnyBody::from(buf.into_inner())) + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + + res.set_body(BoxBody::new(buf.into_inner())) } InternalErrorType::Response(ref resp) => { @@ -106,7 +109,9 @@ impl Responder for InternalError where T: fmt::Debug + fmt::Display + 'static, { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from_error(self) } } diff --git a/src/error/macros.rs b/src/error/macros.rs index 38650c5e..78b1ed9f 100644 --- a/src/error/macros.rs +++ b/src/error/macros.rs @@ -97,7 +97,7 @@ mod tests { let resp_body: &mut dyn MB = &mut body; let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); - let body = &mut resp_body.downcast_mut::().unwrap(); + let body = resp_body.downcast_mut::().unwrap(); body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); diff --git a/src/error/response_error.rs b/src/error/response_error.rs index 2c34be3c..7260efa1 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -6,11 +6,17 @@ use std::{ io::{self, Write as _}, }; -use actix_http::{body::AnyBody, header, Response, StatusCode}; +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue}, + Response, StatusCode, +}; use bytes::BytesMut; -use crate::error::{downcast_dyn, downcast_get_type_id}; -use crate::{helpers, HttpResponse}; +use crate::{ + error::{downcast_dyn, downcast_get_type_id}, + helpers, HttpResponse, +}; /// Errors that can generate responses. // TODO: add std::error::Error bound when replacement for Box is found @@ -27,18 +33,16 @@ pub trait ResponseError: fmt::Debug + fmt::Display { /// /// By default, the generated response uses a 500 Internal Server Error status code, a /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. - fn error_response(&self) -> HttpResponse { + fn error_response(&self) -> HttpResponse { let mut res = HttpResponse::new(self.status_code()); let mut buf = BytesMut::new(); let _ = write!(helpers::MutWriter(&mut buf), "{}", self); - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); - res.set_body(AnyBody::from(buf)) + res.set_body(BoxBody::new(buf)) } downcast_get_type_id!(); @@ -86,8 +90,8 @@ impl ResponseError for actix_http::Error { StatusCode::INTERNAL_SERVER_ERROR } - fn error_response(&self) -> HttpResponse { - HttpResponse::new(self.status_code()).set_body(self.to_string().into()) + fn error_response(&self) -> HttpResponse { + HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body() } } @@ -123,8 +127,8 @@ impl ResponseError for actix_http::error::ContentTypeError { } impl ResponseError for actix_http::ws::HandshakeError { - fn error_response(&self) -> HttpResponse { - Response::from(self).into() + fn error_response(&self) -> HttpResponse { + Response::from(self).map_into_boxed_body().into() } } diff --git a/src/handler.rs b/src/handler.rs index ddefe8d5..e543ecc7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,21 +1,21 @@ use std::future::Future; -use actix_service::{ - boxed::{self, BoxServiceFactory}, - fn_service, -}; +use actix_service::{boxed, fn_service}; use crate::{ - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, Responder, + body::MessageBody, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, FromRequest, HttpResponse, Responder, }; /// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (i.e., [`impl FromRequest`](crate::FromRequest)) and returns a type -/// that can be converted into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). +/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted +/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). /// /// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See [Request Handlers](https://actix.rs/docs/handlers/) for more information. +/// a valid handler. See for more information. +/// +/// [`impl FromRequest`]: crate::FromRequest pub trait Handler: Clone + 'static where R: Future, @@ -24,29 +24,44 @@ where fn call(&self, param: T) -> R; } -pub fn handler_service( - handler: F, -) -> BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()> +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where F: Handler, T: FromRequest, R: Future, R::Output: Responder, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); + async move { let (req, mut payload) = req.into_parts(); + let res = match T::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), - Ok(data) => handler.call(data).await.respond_to(&req), + + Ok(data) => handler + .call(data) + .await + .respond_to(&req) + .map_into_boxed_body(), }; + Ok(ServiceResponse::new(req, res)) } })) } -/// FromRequest trait impl for tuples +/// Generates a [`Handler`] trait impl for N-ary functions where N is specified with a sequence of +/// space separated type parameters. +/// +/// # Examples +/// ```ignore +/// factory_tuple! {} // implements Handler for types: fn() -> Res +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { impl Handler<($($param,)*), Res> for Func where Func: Fn($($param),*) -> Res + Clone + 'static, diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index fe291c01..70e4118c 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -208,7 +208,7 @@ impl Accept { /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// - /// As per the spec, will return [`Mime::STAR_STAR`] (indicating no preference) if the contained + /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained /// list is empty. /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 diff --git a/src/lib.rs b/src/lib.rs index 3ad77ff5..f6ec4082 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,3 +115,5 @@ pub use crate::scope::Scope; pub use crate::server::HttpServer; // TODO: is exposing the error directly really needed pub use crate::types::{Either, EitherExtractError}; + +pub(crate) type BoxError = Box; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index a7533598..e6ef1806 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -1,13 +1,12 @@ //! For middleware documentation, see [`Compat`]. use std::{ - error::Error as StdError, future::Future, pin::Pin, task::{Context, Poll}, }; -use actix_http::body::{AnyBody, MessageBody}; +use actix_http::body::MessageBody; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; use pin_project_lite::pin_project; @@ -123,10 +122,9 @@ pub trait MapServiceResponseBody { impl MapServiceResponseBody for ServiceResponse where B: MessageBody + Unpin + 'static, - B::Error: Into>, { fn map_body(self) -> ServiceResponse { - self.map_body(|_, body| AnyBody::new_boxed(body)) + self.map_into_boxed_body() } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 3b99fd6b..d017e9a5 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -10,14 +10,13 @@ use std::{ }; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{EitherBody, MessageBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, StatusCode, }; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; -use bytes::Bytes; use futures_core::ready; use once_cell::sync::Lazy; use pin_project_lite::pin_project; @@ -62,7 +61,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); @@ -112,7 +111,7 @@ where S: Service, Error = Error>, B: MessageBody, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Future = Either, Ready>>; @@ -144,19 +143,15 @@ where // There is an HTTP header but we cannot match what client as asked for Some(Err(_)) => { - let res = HttpResponse::new(StatusCode::NOT_ACCEPTABLE); + let res = HttpResponse::with_body( + StatusCode::NOT_ACCEPTABLE, + SUPPORTED_ALGORITHM_NAMES.clone(), + ); - let res: HttpResponse>> = res.map_body(move |head, _| { - let body_bytes = Bytes::from(SUPPORTED_ALGORITHM_NAMES.as_bytes()); - - Encoder::response( - ContentEncoding::Identity, - head, - AnyBody::Bytes(body_bytes), - ) - }); - - Either::right(ok(req.into_response(res))) + Either::right(ok(req + .into_response(res) + .map_into_boxed_body() + .map_into_right_body())) } } } @@ -179,7 +174,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Output = Result>>, Error>; + type Output = Result>>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -193,10 +188,11 @@ where }; Poll::Ready(Ok(resp.map_body(move |head, body| { - Encoder::response(enc, head, AnyBody::Body(body)) + EitherBody::left(Encoder::response(enc, head, body)) }))) } - Err(e) => Poll::Ready(Err(e)), + + Err(err) => Poll::Ready(Err(err)), } } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 6ab16a4e..f89b13a1 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -22,7 +22,7 @@ use regex::{Regex, RegexSet}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ - dev::{BodySize, MessageBody}, + body::{BodySize, MessageBody}, http::HeaderName, service::{ServiceRequest, ServiceResponse}, Error, HttpResponse, Result, diff --git a/src/resource.rs b/src/resource.rs index 851ce0fc..fc417bac 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,32 +1,29 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, rc::Rc}; use actix_http::Extensions; use actix_router::{IntoPatterns, Patterns}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, + apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ + body::MessageBody, data::Data, - dev::{ensure_leading_slash, AppService, HttpServiceFactory, ResourceDef}, + dev::{ensure_leading_slash, AppService, ResourceDef}, guard::Guard, handler::Handler, responder::Responder, route::{Route, RouteService}, - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, + service::{ + BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, + ServiceResponse, + }, + BoxError, Error, FromRequest, HttpResponse, }; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; - /// *Resource* is an entry in resources table which corresponds to requested URL. /// /// Resource in turn has at least one route. @@ -56,7 +53,7 @@ pub struct Resource { routes: Vec, app_data: Option, guards: Vec>, - default: HttpNewService, + default: BoxedHttpServiceFactory, factory_ref: Rc>>, } @@ -242,6 +239,8 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { self.routes.push(Route::new().to(handler)); self @@ -422,7 +421,7 @@ where pub struct ResourceFactory { routes: Vec, - default: HttpNewService, + default: BoxedHttpServiceFactory, } impl ServiceFactory for ResourceFactory { @@ -454,7 +453,7 @@ impl ServiceFactory for ResourceFactory { pub struct ResourceService { routes: Vec, - default: HttpService, + default: BoxedHttpService, } impl Service for ResourceService { diff --git a/src/responder.rs b/src/responder.rs index 8a84be59..9d8a0e8e 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,19 +1,21 @@ use std::borrow::Cow; use actix_http::{ - body::AnyBody, + body::{BoxBody, EitherBody, MessageBody}, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, }; use bytes::{Bytes, BytesMut}; -use crate::{Error, HttpRequest, HttpResponse, HttpResponseBuilder}; +use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; /// Trait implemented by types that can be converted to an HTTP response. /// /// Any types that implement this trait can be used in the return type of a handler. pub trait Responder { + type Body: MessageBody + 'static; + /// Convert self to `HttpResponse`. - fn respond_to(self, req: &HttpRequest) -> HttpResponse; + fn respond_to(self, req: &HttpRequest) -> HttpResponse; /// Override a status code for a Responder. /// @@ -59,38 +61,52 @@ pub trait Responder { } impl Responder for HttpResponse { + type Body = BoxBody; + #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { self } } -impl Responder for actix_http::Response { +impl Responder for actix_http::Response { + type Body = BoxBody; + #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from(self) } } impl Responder for HttpResponseBuilder { + type Body = BoxBody; + #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { self.finish() } } impl Responder for actix_http::ResponseBuilder { + type Body = BoxBody; + #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { - HttpResponse::from(self.finish()) + fn respond_to(mut self, req: &HttpRequest) -> HttpResponse { + self.finish().map_into_boxed_body().respond_to(req) } } -impl Responder for Option { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for Option +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Some(val) => val.respond_to(req), - None => HttpResponse::new(StatusCode::NOT_FOUND), + Some(val) => val.respond_to(req).map_into_left_body(), + None => HttpResponse::new(StatusCode::NOT_FOUND).map_into_right_body(), } } } @@ -98,47 +114,69 @@ impl Responder for Option { impl Responder for Result where T: Responder, + ::Error: Into, E: Into, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Ok(val) => val.respond_to(req), - Err(e) => HttpResponse::from_error(e.into()), + Ok(val) => val.respond_to(req).map_into_left_body(), + Err(err) => HttpResponse::from_error(err.into()).map_into_right_body(), } } } impl Responder for (T, StatusCode) { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = T::Body; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); *res.status_mut() = self.1; res } } -macro_rules! impl_responder { - ($res: ty, $ct: path) => { +macro_rules! impl_responder_by_forward_into_base_response { + ($res:ty, $body:ty) => { impl Responder for $res { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok().content_type($ct).body(self) + type Body = $body; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let res: actix_http::Response<_> = self.into(); + res.into() + } + } + }; + + ($res:ty) => { + impl_responder_by_forward_into_base_response!($res, $res); + }; +} + +impl_responder_by_forward_into_base_response!(&'static [u8]); +impl_responder_by_forward_into_base_response!(Bytes); +impl_responder_by_forward_into_base_response!(BytesMut); + +impl_responder_by_forward_into_base_response!(&'static str); +impl_responder_by_forward_into_base_response!(String); + +macro_rules! impl_into_string_responder { + ($res:ty) => { + impl Responder for $res { + type Body = String; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let string: String = self.into(); + let res: actix_http::Response<_> = string.into(); + res.into() } } }; } -impl_responder!(&'static str, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(String, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(&'_ String, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(Cow<'_, str>, mime::TEXT_PLAIN_UTF_8); - -impl_responder!(&'static [u8], mime::APPLICATION_OCTET_STREAM); - -impl_responder!(Bytes, mime::APPLICATION_OCTET_STREAM); - -impl_responder!(BytesMut, mime::APPLICATION_OCTET_STREAM); +impl_into_string_responder!(&'_ String); +impl_into_string_responder!(Cow<'_, str>); /// Allows overriding status code and headers for a responder. pub struct CustomResponder { @@ -204,11 +242,17 @@ impl CustomResponder { } } -impl Responder for CustomResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for CustomResponder +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let headers = match self.headers { Ok(headers) => headers, - Err(err) => return HttpResponse::from_error(Error::from(err)), + Err(err) => return HttpResponse::from_error(err).map_into_right_body(), }; let mut res = self.responder.respond_to(req); @@ -222,7 +266,7 @@ impl Responder for CustomResponder { res.headers_mut().insert(k, v); } - res + res.map_into_left_body() } } @@ -231,11 +275,15 @@ pub(crate) mod tests { use actix_service::Service; use bytes::{Bytes, BytesMut}; + use actix_http::body::to_bytes; + use super::*; - use crate::dev::AnyBody; - use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; - use crate::test::{init_service, TestRequest}; - use crate::{error, web, App}; + use crate::{ + error, + http::{header::CONTENT_TYPE, HeaderValue, StatusCode}, + test::{assert_body_eq, init_service, TestRequest}, + web, App, + }; #[actix_rt::test] async fn test_option_responder() { @@ -253,112 +301,116 @@ pub(crate) mod tests { let req = TestRequest::with_uri("/some").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"some")); - } - _ => panic!(), - } - } - - pub(crate) trait BodyTest { - fn bin_ref(&self) -> &[u8]; - fn body(&self) -> &AnyBody; - } - - impl BodyTest for AnyBody { - fn bin_ref(&self) -> &[u8] { - match self { - AnyBody::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - } - } - fn body(&self) -> &AnyBody { - self - } + assert_body_eq!(resp, b"some"); } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); - let resp = "test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = "test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = b"test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = b"test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - - let resp = "test".to_string().respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = (&"test".to_string()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = "test".to_string().respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = (&"test".to_string()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let s = String::from("test"); - let resp = Cow::Borrowed(s.as_str()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = Cow::Borrowed(s.as_str()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = Cow::<'_, str>::Owned(s).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = Cow::Borrowed("test").respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = Cow::<'_, str>::Owned(s).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = Bytes::from_static(b"test").respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Cow::Borrowed("test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Bytes::from_static(b"test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - - let resp = BytesMut::from(b"test".as_ref()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = BytesMut::from(b"test".as_ref()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); // InternalError - let resp = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let res = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] @@ -368,11 +420,14 @@ pub(crate) mod tests { // Result let resp = Ok::<_, Error>("test".to_string()).respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); + assert_eq!( + to_bytes(resp.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let res = Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) .respond_to(&req); @@ -389,7 +444,10 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let res = "test" .to_string() @@ -397,11 +455,14 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } #[actix_rt::test] @@ -409,17 +470,23 @@ pub(crate) mod tests { let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::OK) .with_header((CONTENT_TYPE, mime::APPLICATION_JSON)) .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } } diff --git a/src/response/builder.rs b/src/response/builder.rs index e61f7e16..b5bef2e9 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -1,14 +1,13 @@ use std::{ cell::{Ref, RefMut}, convert::TryInto, - error::Error as StdError, future::Future, pin::Pin, task::{Context, Poll}, }; use actix_http::{ - body::{AnyBody, BodyStream}, + body::{BodyStream, BoxBody, MessageBody}, http::{ header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, ConnectionType, Error as HttpError, StatusCode, @@ -26,14 +25,14 @@ use cookie::{Cookie, CookieJar}; use crate::{ error::{Error, JsonPayloadError}, - HttpResponse, + BoxError, HttpResponse, }; /// An HTTP response builder. /// /// This type can be used to construct an instance of `Response` through a builder-like pattern. pub struct HttpResponseBuilder { - res: Option>, + res: Option>, err: Option, #[cfg(feature = "cookies")] cookies: Option, @@ -44,7 +43,7 @@ impl HttpResponseBuilder { /// Create response builder pub fn new(status: StatusCode) -> Self { Self { - res: Some(Response::new(status)), + res: Some(Response::with_body(status, BoxBody::new(()))), err: None, #[cfg(feature = "cookies")] cookies: None, @@ -299,7 +298,6 @@ impl HttpResponseBuilder { } /// Mutable reference to a the response's extensions - #[inline] pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { self.res .as_mut() @@ -307,18 +305,20 @@ impl HttpResponseBuilder { .extensions_mut() } - /// Set a body and generate `Response`. + /// Set a body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. - #[inline] - pub fn body>(&mut self, body: B) -> HttpResponse { - match self.message_body(body.into()) { - Ok(res) => res, + pub fn body(&mut self, body: B) -> HttpResponse + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_into_boxed_body(), Err(err) => HttpResponse::from_error(err), } } - /// Set a body and generate `Response`. + /// Set a body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn message_body(&mut self, body: B) -> Result, Error> { @@ -332,7 +332,7 @@ impl HttpResponseBuilder { .expect("cannot reuse response builder") .set_body(body); - #[allow(unused_mut)] + #[allow(unused_mut)] // mut is only unused when cookies are disabled let mut res = HttpResponse::from(res); #[cfg(feature = "cookies")] @@ -348,19 +348,19 @@ impl HttpResponseBuilder { Ok(res) } - /// Set a streaming body and generate `Response`. + /// Set a streaming body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn streaming(&mut self, stream: S) -> HttpResponse where S: Stream> + 'static, - E: Into> + 'static, + E: Into + 'static, { - self.body(AnyBody::new_boxed(BodyStream::new(stream))) + self.body(BodyStream::new(stream)) } - /// Set a json body and generate `Response` + /// Set a JSON body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. pub fn json(&mut self, value: impl Serialize) -> HttpResponse { @@ -376,18 +376,18 @@ impl HttpResponseBuilder { self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); } - self.body(AnyBody::from(body)) + self.body(body) } Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), } } - /// Set an empty body and generate `Response` + /// Set an empty body and build the `HttpResponse`. /// /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { - self.body(AnyBody::empty()) + self.body(()) } /// This method construct new `HttpResponseBuilder` @@ -416,7 +416,7 @@ impl From for HttpResponse { } } -impl From for Response { +impl From for Response { fn from(mut builder: HttpResponseBuilder) -> Self { builder.finish().into() } @@ -435,12 +435,9 @@ mod tests { use actix_http::body; use super::*; - use crate::{ - dev::AnyBody, - http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + use crate::http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, }; #[test] @@ -475,7 +472,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(AnyBody::empty()); + .body(Bytes::new()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/response.rs b/src/response/response.rs index 23562ab0..97de21e4 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, EitherBody, MessageBody}, http::{header::HeaderMap, StatusCode}, Extensions, Response, ResponseHead, }; @@ -25,12 +25,12 @@ use { use crate::{error::Error, HttpResponseBuilder}; /// An outgoing response. -pub struct HttpResponse { +pub struct HttpResponse { res: Response, pub(crate) error: Option, } -impl HttpResponse { +impl HttpResponse { /// Constructs a response. #[inline] pub fn new(status: StatusCode) -> Self { @@ -227,8 +227,26 @@ impl HttpResponse { } } - // TODO: into_body equivalent - // TODO: into_boxed_body + // TODO: docs for the body map methods below + + #[inline] + pub fn map_into_left_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> HttpResponse + where + B: MessageBody + 'static, + { + // TODO: avoid double boxing with down-casting, if it improves perf + self.map_body(|_, body| BoxBody::new(body)) + } /// Extract response body pub fn into_body(self) -> B { @@ -273,14 +291,14 @@ impl From> for Response { } } -// Future is only implemented for AnyBody payload type because it's the most useful for making +// Future is only implemented for BoxBody payload type because it's the most useful for making // simple handlers without async blocks. Making it generic over all MessageBody types requires a // future impl on Response which would cause it's body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. -impl Future for HttpResponse { - type Output = Result, Error>; +impl Future for HttpResponse { + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { if let Some(err) = self.error.take() { diff --git a/src/route.rs b/src/route.rs index 0c069943..1eb32306 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,19 +1,18 @@ -#![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`) - -use std::{future::Future, rc::Rc}; +use std::{future::Future, mem, rc::Rc}; use actix_http::http::Method; use actix_service::{ - boxed::{self, BoxService, BoxServiceFactory}, - Service, ServiceFactory, ServiceFactoryExt, + boxed::{self, BoxService}, + fn_service, Service, ServiceFactory, ServiceFactoryExt, }; use futures_core::future::LocalBoxFuture; use crate::{ + body::MessageBody, guard::{self, Guard}, handler::{handler_service, Handler}, - service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpResponse, Responder, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, Error, FromRequest, HttpResponse, Responder, }; /// Resource route definition @@ -21,7 +20,7 @@ use crate::{ /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route { - service: BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>, + service: BoxedHttpServiceFactory, guards: Rc>>, } @@ -30,13 +29,15 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: handler_service(HttpResponse::NotFound), + service: boxed::factory(fn_service(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::NotFound())) + })), guards: Rc::new(Vec::new()), } } pub(crate) fn take_guards(&mut self) -> Vec> { - std::mem::take(Rc::get_mut(&mut self.guards).unwrap()) + mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } @@ -181,6 +182,8 @@ impl Route { T: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { self.service = handler_service(handler); self diff --git a/src/scope.rs b/src/scope.rs index c20b5d7c..ff013671 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -3,9 +3,8 @@ use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::Extensions; use actix_router::{ResourceDef, Router}; use actix_service::{ - apply, apply_fn_factory, - boxed::{self, BoxService, BoxServiceFactory}, - IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, + apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, + ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -13,16 +12,17 @@ use futures_util::future::join_all; use crate::{ config::ServiceConfig, data::Data, - dev::{AppService, HttpServiceFactory}, + dev::AppService, guard::Guard, rmap::ResourceMap, - service::{AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse}, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, + ServiceFactoryWrapper, ServiceRequest, ServiceResponse, + }, Error, Resource, Route, }; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Resources scope. /// @@ -58,7 +58,7 @@ pub struct Scope { app_data: Option, services: Vec>, guards: Vec>, - default: Option>, + default: Option>, external: Vec, factory_ref: Rc>>, } @@ -470,8 +470,14 @@ where } pub struct ScopeFactory { - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for ScopeFactory { @@ -518,8 +524,8 @@ impl ServiceFactory for ScopeFactory { } pub struct ScopeService { - router: Router>>, - default: HttpService, + router: Router>>, + default: BoxedHttpService, } impl Service for ScopeService { @@ -580,12 +586,11 @@ mod tests { use bytes::Bytes; use crate::{ - dev::AnyBody, guard, http::{header, HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, service::{ServiceRequest, ServiceResponse}, - test::{call_service, init_service, read_body, TestRequest}, + test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, }; @@ -748,20 +753,13 @@ mod tests { .await; let req = TestRequest::with_uri("/ab-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_body_eq!(res, b"project: project1"); let req = TestRequest::with_uri("/aa-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] @@ -849,16 +847,9 @@ mod tests { .await; let req = TestRequest::with_uri("/app/project_1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: project_1"); } #[actix_rt::test] @@ -877,20 +868,13 @@ mod tests { .await; let req = TestRequest::with_uri("/app/test/1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - AnyBody::Bytes(ref b) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: test - 1"); let req = TestRequest::with_uri("/app/test/1/path2").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] diff --git a/src/server.rs b/src/server.rs index 1bf56655..3db84941 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,6 @@ use std::{ any::Any, - cmp, - error::Error as StdError, - fmt, io, + cmp, fmt, io, marker::PhantomData, net, sync::{Arc, Mutex}, @@ -75,15 +73,13 @@ where I: IntoServiceFactory, S: ServiceFactory + 'static, - // S::Future: 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, S::Service: 'static, - // S::Service: 'static, + B: MessageBody + 'static, - B::Error: Into>, { /// Create new HTTP server with application factory pub fn new(factory: F) -> Self { @@ -656,8 +652,8 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result io::Result { builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; diff --git a/src/service.rs b/src/service.rs index 8ba38df4..df9e809e 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,14 +1,19 @@ -use std::cell::{Ref, RefMut}; -use std::rc::Rc; -use std::{fmt, net}; +use std::{ + cell::{Ref, RefMut}, + fmt, net, + rc::Rc, +}; use actix_http::{ - body::{AnyBody, MessageBody}, + body::{BoxBody, EitherBody, MessageBody}, http::{HeaderMap, Method, StatusCode, Uri, Version}, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; -use actix_service::{IntoServiceFactory, ServiceFactory}; +use actix_service::{ + boxed::{BoxService, BoxServiceFactory}, + IntoServiceFactory, ServiceFactory, +}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; @@ -21,6 +26,10 @@ use crate::{ Error, HttpRequest, HttpResponse, }; +pub(crate) type BoxedHttpService = BoxService, Error>; +pub(crate) type BoxedHttpServiceFactory = + BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; + pub trait HttpServiceFactory { fn register(self, config: &mut AppService); } @@ -326,12 +335,12 @@ impl fmt::Debug for ServiceRequest { } /// A service level response wrapper. -pub struct ServiceResponse { +pub struct ServiceResponse { request: HttpRequest, response: HttpResponse, } -impl ServiceResponse { +impl ServiceResponse { /// Create service response from the error pub fn from_err>(err: E, request: HttpRequest) -> Self { let response = HttpResponse::from_error(err); @@ -401,6 +410,7 @@ impl ServiceResponse { impl ServiceResponse { /// Set a new body + #[inline] pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, B) -> B2, @@ -412,6 +422,24 @@ impl ServiceResponse { request: self.request, } } + + #[inline] + pub fn map_into_left_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> ServiceResponse + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } } impl From> for HttpResponse { diff --git a/src/test.rs b/src/test.rs index 77765e26..2cd01039 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,6 @@ use std::{borrow::Cow, net::SocketAddr, rc::Rc}; pub use actix_http::test::TestBuffer; use actix_http::{ - body, http::{header::IntoHeaderPair, Method, StatusCode, Uri, Version}, test::TestRequest as HttpTestRequest, Extensions, Request, @@ -20,9 +19,10 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::cookie::{Cookie, CookieJar}; use crate::{ app_service::AppInitServiceState, + body::{self, BoxBody, MessageBody}, config::AppConfig, data::Data, - dev::{AnyBody, MessageBody, Payload}, + dev::Payload, http::header::ContentType, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, @@ -32,14 +32,14 @@ use crate::{ /// Create service that always responds with `HttpResponse::Ok()` and no body. pub fn ok_service( -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { default_service(StatusCode::OK) } /// Create service that always responds with given status code and no body. pub fn default_service( status_code: StatusCode, -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { (move |req: ServiceRequest| { ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) @@ -632,6 +632,22 @@ impl TestRequest { } } +/// Reduces boilerplate code when testing expected response payloads. +#[cfg(test)] +macro_rules! assert_body_eq { + ($res:ident, $expected:expr) => { + assert_eq!( + ::actix_http::body::to_bytes($res.into_body()) + .await + .expect("body read should have succeeded"), + Bytes::from_static($expected), + ) + }; +} + +#[cfg(test)] +pub(crate) use assert_body_eq; + #[cfg(test)] mod tests { use std::time::SystemTime; diff --git a/src/types/either.rs b/src/types/either.rs index 0a8a9013..3c759736 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -12,7 +12,7 @@ use futures_core::ready; use pin_project_lite::pin_project; use crate::{ - dev, + body, dev, web::{Form, Json}, Error, FromRequest, HttpRequest, HttpResponse, Responder, }; @@ -146,10 +146,12 @@ where L: Responder, R: Responder, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = body::EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Either::Left(a) => a.respond_to(req), - Either::Right(b) => b.respond_to(req), + Either::Left(a) => a.respond_to(req).map_into_left_body(), + Either::Right(b) => b.respond_to(req).map_into_right_body(), } } } diff --git a/src/types/form.rs b/src/types/form.rs index 098a864d..9c09c6b7 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -20,8 +20,9 @@ use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ - error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, - HttpMessage, HttpRequest, HttpResponse, Responder, + body::EitherBody, error::UrlencodedError, extract::FromRequest, + http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse, + Responder, }; /// URL encoded payload extractor and responder. @@ -180,12 +181,21 @@ impl fmt::Display for Form { /// See [here](#responder) for example of usage as a handler return type. impl Responder for Form { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_urlencoded::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_WWW_FORM_URLENCODED) - .body(body), - Err(err) => HttpResponse::from_error(UrlencodedError::Serialize(err)), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(UrlencodedError::Serialize(err)).map_into_right_body() + } } } } @@ -408,11 +418,14 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::http::{ - header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, - StatusCode, - }; use crate::test::TestRequest; + use crate::{ + http::{ + header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, + }; #[derive(Deserialize, Serialize, Debug, PartialEq)] struct Info { @@ -520,15 +533,13 @@ mod tests { hello: "world".to_string(), counter: 123, }); - let resp = form.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = form.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/x-www-form-urlencoded") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123"); + assert_body_eq!(res, b"hello=world&counter=123"); } #[actix_rt::test] diff --git a/src/types/json.rs b/src/types/json.rs index 6d07fe45..2b4d220e 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -19,6 +19,7 @@ use actix_http::Payload; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ + body::EitherBody, error::{Error, JsonPayloadError}, extract::FromRequest, http::header::CONTENT_LENGTH, @@ -116,12 +117,21 @@ impl Serialize for Json { /// /// If serialization failed impl Responder for Json { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_json::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) - .body(body), - Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() + } } } } @@ -444,7 +454,7 @@ mod tests { header::{self, CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, }, - test::{load_body, TestRequest}, + test::{assert_body_eq, load_body, TestRequest}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -472,15 +482,13 @@ mod tests { let j = Json(MyObject { name: "test".to_string(), }); - let resp = j.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = j.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + res.headers().get(header::CONTENT_TYPE).unwrap(), header::HeaderValue::from_static("application/json") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"{\"name\":\"test\"}"); + assert_body_eq!(res, b"{\"name\":\"test\"}"); } #[actix_rt::test] diff --git a/src/types/path.rs b/src/types/path.rs index cd24deb8..4b60d27c 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -90,7 +90,7 @@ impl fmt::Display for Path { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Path where T: de::DeserializeOwned, diff --git a/src/types/payload.rs b/src/types/payload.rs index 00047e8b..73987def 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -43,12 +43,12 @@ use crate::{ /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` -pub struct Payload(crate::dev::Payload); +pub struct Payload(dev::Payload); impl Payload { /// Unwrap to inner Payload type. #[inline] - pub fn into_inner(self) -> crate::dev::Payload { + pub fn into_inner(self) -> dev::Payload { self.0 } } @@ -62,7 +62,7 @@ impl Stream for Payload { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Payload { type Error = Error; type Future = Ready>; diff --git a/src/types/query.rs b/src/types/query.rs index ba2034bf..9fac2117 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -105,7 +105,7 @@ impl fmt::Display for Query { } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Query { type Error = Error; type Future = Ready>; diff --git a/src/web.rs b/src/web.rs index e9f5c851..b58adc2f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,14 +1,14 @@ //! Essentials helper functions and types for application registration. -use std::future::Future; +use std::{error::Error as StdError, future::Future}; use actix_http::http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, - responder::Responder, route::Route, scope::Scope, service::WebService, + body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, + resource::Resource, responder::Responder, route::Route, scope::Scope, service::WebService, }; pub use crate::config::ServiceConfig; @@ -145,6 +145,8 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody + 'static, + <::Body as MessageBody>::Error: Into>, { Route::new().to(handler) } diff --git a/tests/test_server.rs b/tests/test_server.rs index 3f0fbfcc..a850f228 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -200,13 +200,10 @@ async fn test_body_encoding_override() { .body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - let body = actix_web::dev::AnyBody::Bytes(STR.into()); let mut response = - HttpResponse::with_body(actix_web::http::StatusCode::OK, body); - + HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); response.encoding(ContentEncoding::Deflate); - - response + response.map_into_boxed_body() }))) }); From fa7f3e6908cea673ed64bfc686af697436db1911 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 4 Dec 2021 19:41:15 +0000 Subject: [PATCH 95/99] undeprecate `App::data_factory` (#2484) --- CHANGES.md | 2 ++ src/app.rs | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2dc45c3e..2b5a7100 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ ### Changed * Rename `Accept::{mime_precedence => ranked}`. [#2480] * Rename `Accept::{mime_preference => preference}`. [#2480] +* Un-deprecate `App::data_factory`. [#2484] ### Fixed * Accept wildcard `*` items in `AcceptLanguage`. [#2480] @@ -19,6 +20,7 @@ [#2468]: https://github.com/actix/actix-web/pull/2468 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 +[#2484]: https://github.com/actix/actix-web/pull/2484 ## 4.0.0-beta.13 - 2021-11-30 diff --git a/src/app.rs b/src/app.rs index 36063ec7..efc108cb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -140,10 +140,6 @@ where /// Add application data factory. This function is similar to `.data()` but it accepts a /// "data factory". Data values are constructed asynchronously during application /// initialization, before the server starts accepting requests. - #[deprecated( - since = "4.0.0", - note = "Construct data value before starting server and use `.app_data(Data::new(val))` instead." - )] pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, From 4c9ca7196dab4c745024be19c600630a76388ee4 Mon Sep 17 00:00:00 2001 From: Mohammed Sazid Al Rashid Date: Sun, 5 Dec 2021 04:32:44 +0600 Subject: [PATCH 96/99] Add `WsResponseBuilder` to build web socket session response (#1920) Co-authored-by: Rob Ede --- actix-http/CHANGES.md | 4 + actix-http/src/ws/codec.rs | 12 +- actix-web-actors/CHANGES.md | 4 + actix-web-actors/src/ws.rs | 256 ++++++++++++++++++++++++++---- actix-web-actors/tests/test_ws.rs | 201 +++++++++++++++-------- 5 files changed, 379 insertions(+), 98 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 23c15296..773f1ff3 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -9,6 +9,8 @@ * `body::None` struct. [#2468] * Impl `MessageBody` for `bytestring::ByteString`. [#2468] * `impl Clone for ws::HandshakeError`. [#2468] +* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] +* `impl Default ` for `ws::Codec`. [#1920] ### Changed * Rename `body::BoxBody::{from_body => new}`. [#2468] @@ -24,9 +26,11 @@ * `impl Future` for `ResponseBuilder`. [#2468] * Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] * Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] +* `impl Copy` for `ws::Codec`. [#1920] [#2483]: https://github.com/actix/actix-web/pull/2483 [#2468]: https://github.com/actix/actix-web/pull/2468 +[#1920]: https://github.com/actix/actix-web/pull/1920 ## 3.0.0-beta.14 - 2021-11-30 diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 8655216f..d80613e5 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -63,8 +63,8 @@ pub enum Item { Last(Bytes), } -#[derive(Debug, Copy, Clone)] /// WebSocket protocol codec. +#[derive(Debug, Clone)] pub struct Codec { flags: Flags, max_size: usize, @@ -89,7 +89,8 @@ impl Codec { /// Set max frame size. /// - /// By default max size is set to 64kB. + /// By default max size is set to 64KiB. + #[must_use = "This returns the a new Codec, without modifying the original."] pub fn max_size(mut self, size: usize) -> Self { self.max_size = size; self @@ -98,12 +99,19 @@ impl Codec { /// Set decoder to client mode. /// /// By default decoder works in server mode. + #[must_use = "This returns the a new Codec, without modifying the original."] pub fn client_mode(mut self) -> Self { self.flags.remove(Flags::SERVER); self } } +impl Default for Codec { + fn default() -> Self { + Self::new() + } +} + impl Encoder for Codec { type Error = ProtocolError; diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index e3693f0f..898098ed 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,8 +1,12 @@ # Changes ## Unreleased - 2021-xx-xx +* Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] +* Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] * Minimum supported Rust version (MSRV) is now 1.52. +[#1920]: https://github.com/actix/actix-web/pull/1920 + ## 4.0.0-beta.7 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index b6323c2e..c41268b0 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -1,20 +1,24 @@ //! Websocket integration. -use std::future::Future; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{collections::VecDeque, convert::TryFrom}; - -use actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope, +use std::{ + collections::VecDeque, + convert::TryFrom, + future::Future, + io, mem, + pin::Pin, + task::{Context, Poll}, }; -use actix::fut::ActorFuture; + use actix::{ + dev::{ + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, + ToEnvelope, + }, + fut::ActorFuture, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, SpawnHandle, }; -use actix_codec::{Decoder, Encoder}; +use actix_codec::{Decoder as _, Encoder as _}; pub use actix_http::ws::{ CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; @@ -31,9 +35,188 @@ use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::Stream; use pin_project_lite::pin_project; -use tokio::sync::oneshot::Sender; +use tokio::sync::oneshot; + +/// Builder for Websocket session response. +/// +/// # Examples +/// +/// Create a Websocket session response with default configuration. +/// ```ignore +/// WsResponseBuilder::new(WsActor, &req, stream).start() +/// ``` +/// +/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols. +/// ```ignore +/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB +/// +/// ws::WsResponseBuilder::new(WsActor, &req, stream) +/// .codec(Codec::new()) +/// .protocols(&["A", "B"]) +/// .frame_size(MAX_FRAME_SIZE) +/// .start() +/// ``` +pub struct WsResponseBuilder<'a, A, T> +where + A: Actor> + StreamHandler>, + T: Stream> + 'static, +{ + actor: A, + req: &'a HttpRequest, + stream: T, + codec: Option, + protocols: Option<&'a [&'a str]>, + frame_size: Option, +} + +impl<'a, A, T> WsResponseBuilder<'a, A, T> +where + A: Actor> + StreamHandler>, + T: Stream> + 'static, +{ + /// Construct a new `WsResponseBuilder` with actor, request, and payload stream. + /// + /// For usage example, see docs on [`WsResponseBuilder`] struct. + pub fn new(actor: A, req: &'a HttpRequest, stream: T) -> Self { + WsResponseBuilder { + actor, + req, + stream, + codec: None, + protocols: None, + frame_size: None, + } + } + + /// Set the protocols for the session. + pub fn protocols(mut self, protocols: &'a [&'a str]) -> Self { + self.protocols = Some(protocols); + self + } + + /// Set the max frame size for each message (in bytes). + /// + /// **Note**: This will override any given [`Codec`]'s max frame size. + pub fn frame_size(mut self, frame_size: usize) -> Self { + self.frame_size = Some(frame_size); + self + } + + /// Set the [`Codec`] for the session. If [`Self::frame_size`] is also set, the given + /// [`Codec`]'s max frame size will be overridden. + pub fn codec(mut self, codec: Codec) -> Self { + self.codec = Some(codec); + self + } + + fn handshake_resp(&self) -> Result { + match self.protocols { + Some(protocols) => handshake_with_protocols(self.req, protocols), + None => handshake(self.req), + } + } + + fn set_frame_size(&mut self) { + if let Some(frame_size) = self.frame_size { + match &mut self.codec { + Some(codec) => { + // modify existing codec's max frame size + let orig_codec = mem::take(codec); + *codec = orig_codec.max_size(frame_size); + } + + None => { + // create a new codec with the given size + self.codec = Some(Codec::new().max_size(frame_size)); + } + } + } + } + + /// Create a new Websocket context from an actor, request stream, and codec. + /// + /// Returns a pair, where the first item is an addr for the created actor, and the second item + /// is a stream intended to be set as part of the response + /// via [`HttpResponseBuilder::streaming()`]. + fn create_with_codec_addr( + actor: A, + stream: S, + codec: Codec, + ) -> (Addr, impl Stream>) + where + A: StreamHandler>, + S: Stream> + 'static, + { + let mb = Mailbox::default(); + let mut ctx = WebsocketContext { + inner: ContextParts::new(mb.sender_producer()), + messages: VecDeque::new(), + }; + ctx.add_stream(WsStream::new(stream, codec.clone())); + + let addr = ctx.address(); + + (addr, WebsocketContextFut::new(ctx, actor, mb, codec)) + } + + /// Perform WebSocket handshake and start actor. + /// + /// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change. + /// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a + /// stream of the body request. + /// + /// If there is a problem with the handshake, an error is returned. + /// + /// If successful, consume the [`WsResponseBuilder`] and return a [`HttpResponse`] wrapped in + /// a [`Result`]. + pub fn start(mut self) -> Result { + let mut res = self.handshake_resp()?; + self.set_frame_size(); + + match self.codec { + Some(codec) => { + let out_stream = WebsocketContext::with_codec(self.actor, self.stream, codec); + Ok(res.streaming(out_stream)) + } + None => { + let out_stream = WebsocketContext::create(self.actor, self.stream); + Ok(res.streaming(out_stream)) + } + } + } + + /// Perform WebSocket handshake and start actor. + /// + /// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change. + /// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a + /// stream of the body request. + /// + /// If there is a problem with the handshake, an error is returned. + /// + /// If successful, returns a pair where the first item is an address for the created actor and + /// the second item is the [`HttpResponse`] that should be returned from the websocket request. + pub fn start_with_addr(mut self) -> Result<(Addr, HttpResponse), Error> { + let mut res = self.handshake_resp()?; + self.set_frame_size(); + + match self.codec { + Some(codec) => { + let (addr, out_stream) = + Self::create_with_codec_addr(self.actor, self.stream, codec); + Ok((addr, res.streaming(out_stream))) + } + None => { + let (addr, out_stream) = + WebsocketContext::create_with_addr(self.actor, self.stream); + Ok((addr, res.streaming(out_stream))) + } + } + } +} /// Perform WebSocket handshake and start actor. +/// +/// To customize options, see [`WsResponseBuilder`]. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where A: Actor> + StreamHandler>, @@ -45,15 +228,15 @@ where /// Perform WebSocket handshake and start actor. /// -/// `req` is an HTTP Request that should be requesting a websocket protocol -/// change. `stream` should be a `Bytes` stream (such as -/// `actix_web::web::Payload`) that contains a stream of the body request. +/// `req` is an HTTP Request that should be requesting a websocket protocol change. `stream` should +/// be a `Bytes` stream (such as `actix_web::web::Payload`) that contains a stream of the +/// body request. /// /// If there is a problem with the handshake, an error is returned. /// -/// If successful, returns a pair where the first item is an address for the -/// created actor and the second item is the response that should be returned -/// from the WebSocket request. +/// If successful, returns a pair where the first item is an address for the created actor and the +/// second item is the response that should be returned from the WebSocket request. +#[deprecated(since = "4.0.0", note = "Prefer `WsResponseBuilder::start_with_addr`.")] pub fn start_with_addr( actor: A, req: &HttpRequest, @@ -71,6 +254,10 @@ where /// Do WebSocket handshake and start ws actor. /// /// `protocols` is a sequence of known protocols. +#[deprecated( + since = "4.0.0", + note = "Prefer `WsResponseBuilder` for setting protocols." +)] pub fn start_with_protocols( actor: A, protocols: &[&str], @@ -87,20 +274,19 @@ where /// Prepare WebSocket handshake response. /// -/// This function returns handshake `HttpResponse`, ready to send to peer. -/// It does not perform any IO. +/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform +/// any IO. pub fn handshake(req: &HttpRequest) -> Result { handshake_with_protocols(req, &[]) } /// Prepare WebSocket handshake response. /// -/// This function returns handshake `HttpResponse`, ready to send to peer. -/// It does not perform any IO. +/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform +/// any IO. /// -/// `protocols` is a sequence of known protocols. On successful handshake, -/// the returned response headers contain the first protocol in this list -/// which the server also knows. +/// `protocols` is a sequence of known protocols. On successful handshake, the returned response +/// headers contain the first protocol in this list which the server also knows. pub fn handshake_with_protocols( req: &HttpRequest, protocols: &[&str], @@ -247,8 +433,8 @@ impl WebsocketContext where A: Actor, { + /// Create a new Websocket context from a request and an actor. #[inline] - /// Create a new Websocket context from a request and an actor pub fn create(actor: A, stream: S) -> impl Stream> where A: StreamHandler>, @@ -258,12 +444,11 @@ where stream } - #[inline] /// Create a new Websocket context from a request and an actor. /// - /// Returns a pair, where the first item is an addr for the created actor, - /// and the second item is a stream intended to be set as part of the - /// response via `HttpResponseBuilder::streaming()`. + /// Returns a pair, where the first item is an addr for the created actor, and the second item + /// is a stream intended to be set as part of the response + /// via [`HttpResponseBuilder::streaming()`]. pub fn create_with_addr( actor: A, stream: S, @@ -284,7 +469,6 @@ where (addr, WebsocketContextFut::new(ctx, actor, mb, Codec::new())) } - #[inline] /// Create a new Websocket context from a request, an actor, and a codec pub fn with_codec( actor: A, @@ -300,7 +484,7 @@ where inner: ContextParts::new(mb.sender_producer()), messages: VecDeque::new(), }; - ctx.add_stream(WsStream::new(stream, codec)); + ctx.add_stream(WsStream::new(stream, codec.clone())); WebsocketContextFut::new(ctx, actor, mb, codec) } @@ -458,12 +642,13 @@ where M: ActixMessage + Send + 'static, M::Result: Send, { - fn pack(msg: M, tx: Option>) -> Envelope { + fn pack(msg: M, tx: Option>) -> Envelope { Envelope::new(msg, tx) } } pin_project! { + #[derive(Debug)] struct WsStream { #[pin] stream: S, @@ -549,9 +734,12 @@ where #[cfg(test)] mod tests { + use actix_web::{ + http::{header, Method}, + test::TestRequest, + }; + use super::*; - use actix_web::http::{header, Method}; - use actix_web::test::TestRequest; #[test] fn test_handshake() { diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index e481b283..a9eb3769 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -1,11 +1,9 @@ use actix::prelude::*; -use actix_web::{ - http::{header, StatusCode}, - web, App, HttpRequest, HttpResponse, -}; -use actix_web_actors::*; +use actix_http::ws::Codec; +use actix_web::{web, App, HttpRequest}; +use actix_web_actors::ws; use bytes::Bytes; -use futures_util::{SinkExt as _, StreamExt as _}; +use futures_util::{SinkExt, StreamExt}; struct Ws; @@ -15,37 +13,34 @@ impl Actor for Ws { impl StreamHandler> for Ws { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { - match msg.unwrap() { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => {} + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => ctx.text(text), + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + Ok(ws::Message::Close(reason)) => ctx.close(reason), + _ => ctx.close(Some(ws::CloseCode::Error.into())), } } } -#[actix_rt::test] -async fn test_simple() { - let mut srv = actix_test::start(|| { - App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) }, - )) - }); +const MAX_FRAME_SIZE: usize = 10_000; +const DEFAULT_FRAME_SIZE: usize = 10; +async fn common_test_code(mut srv: actix_test::TestServer, frame_size: usize) { // client service let mut framed = srv.ws().await.unwrap(); - framed.send(ws::Message::Text("text".into())).await.unwrap(); + framed.send(ws::Message::Text("text".into())).await.unwrap(); let item = framed.next().await.unwrap().unwrap(); assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text"))); + let bytes = Bytes::from(vec![0; frame_size]); framed - .send(ws::Message::Binary("text".into())) + .send(ws::Message::Binary(bytes.clone())) .await .unwrap(); let item = framed.next().await.unwrap().unwrap(); - assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text"))); + assert_eq!(item, ws::Frame::Binary(bytes)); framed.send(ws::Message::Ping("text".into())).await.unwrap(); let item = framed.next().await.unwrap().unwrap(); @@ -55,55 +50,137 @@ async fn test_simple() { .send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) .await .unwrap(); - let item = framed.next().await.unwrap().unwrap(); assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into()))); } #[actix_rt::test] -async fn test_with_credentials() { - let mut srv = actix_test::start(|| { +async fn simple_builder() { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { - if req.headers().contains_key("Authorization") { - ws::start(Ws, &req, stream) - } else { - Ok(HttpResponse::new(StatusCode::UNAUTHORIZED)) - } + ws::WsResponseBuilder::new(Ws, &req, stream).start() }, )) }); - // client service without credentials - match srv.ws().await { - Ok(_) => panic!("WebSocket client without credentials should panic"), - Err(awc::error::WsClientError::InvalidResponseStatus(status)) => { - assert_eq!(status, StatusCode::UNAUTHORIZED); - } - Err(e) => panic!("Invalid error from WebSocket client: {}", e), - } - - let headers = srv.client_headers().unwrap(); - headers.insert( - header::AUTHORIZATION, - header::HeaderValue::from_static("Bearer Something"), - ); - - // client service with credentials - let client = srv.ws(); - - let mut framed = client.await.unwrap(); - - framed.send(ws::Message::Text("text".into())).await.unwrap(); - - let item = framed.next().await.unwrap().unwrap(); - assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text"))); - - framed - .send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - .await - .unwrap(); - - let item = framed.next().await.unwrap().unwrap(); - assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into()))); + common_test_code(srv, DEFAULT_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn builder_with_frame_size() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .frame_size(MAX_FRAME_SIZE) + .start() + }, + )) + }); + + common_test_code(srv, MAX_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn builder_with_frame_size_exceeded() { + const MAX_FRAME_SIZE: usize = 64; + + let mut srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .frame_size(MAX_FRAME_SIZE) + .start() + }, + )) + }); + + // client service + let mut framed = srv.ws().await.unwrap(); + + // create a request with a frame size larger than expected + let bytes = Bytes::from(vec![0; MAX_FRAME_SIZE + 1]); + framed.send(ws::Message::Binary(bytes)).await.unwrap(); + + let frame = framed.next().await.unwrap().unwrap(); + let close_reason = match frame { + ws::Frame::Close(Some(reason)) => reason, + _ => panic!("close frame expected"), + }; + assert_eq!(close_reason.code, ws::CloseCode::Error); +} + +#[actix_rt::test] +async fn builder_with_codec() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .codec(Codec::new()) + .start() + }, + )) + }); + + common_test_code(srv, DEFAULT_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn builder_with_protocols() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .protocols(&["A", "B"]) + .start() + }, + )) + }); + + common_test_code(srv, DEFAULT_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn builder_with_codec_and_frame_size() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .codec(Codec::new()) + .frame_size(MAX_FRAME_SIZE) + .start() + }, + )) + }); + + common_test_code(srv, DEFAULT_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn builder_full() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { + ws::WsResponseBuilder::new(Ws, &req, stream) + .frame_size(MAX_FRAME_SIZE) + .codec(Codec::new()) + .protocols(&["A", "B"]) + .start() + }, + )) + }); + + common_test_code(srv, MAX_FRAME_SIZE).await; +} + +#[actix_rt::test] +async fn simple_start() { + let srv = actix_test::start(|| { + App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) }, + )) + }); + + common_test_code(srv, DEFAULT_FRAME_SIZE).await; } From d89c706cd6653b9ce14ecbed4237435209e85f7b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 5 Dec 2021 00:02:25 +0000 Subject: [PATCH 97/99] re-instate Range typed header (#2485) Co-authored-by: RideWindX --- CHANGES.md | 2 + src/http/header/mod.rs | 6 +- src/http/header/range.rs | 319 +++++++++++++++++++++------------------ 3 files changed, 179 insertions(+), 148 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2b5a7100..78aa729d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### Added * Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] * `AcceptEncoding` typed header. [#2482] +* `Range` typed header. [#2485] * `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] * `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] @@ -21,6 +22,7 @@ [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 [#2484]: https://github.com/actix/actix-web/pull/2484 +[#2485]: https://github.com/actix/actix-web/pull/2485 ## 4.0.0-beta.13 - 2021-11-30 diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 98548dad..07b7592d 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -40,7 +40,7 @@ mod if_unmodified_since; mod last_modified; mod macros; mod preference; -// mod range; +mod range; #[cfg(test)] pub(crate) use macros::common_header_test; @@ -68,7 +68,7 @@ pub use self::if_range::IfRange; pub use self::if_unmodified_since::IfUnmodifiedSince; pub use self::last_modified::LastModified; pub use self::preference::Preference; -//pub use self::range::{Range, ByteRangeSpec}; +pub use self::range::{ByteRangeSpec, Range}; /// Format writer ([`fmt::Write`]) for a [`BytesMut`]. #[derive(Debug, Default)] @@ -77,10 +77,12 @@ struct Writer { } impl Writer { + /// Constructs new bytes writer. pub fn new() -> Writer { Writer::default() } + /// Splits bytes out of writer, leaving writer buffer empty. pub fn take(&mut self) -> Bytes { self.buf.split().freeze() } diff --git a/src/http/header/range.rs b/src/http/header/range.rs index 11006fff..c1d60f1e 100644 --- a/src/http/header/range.rs +++ b/src/http/header/range.rs @@ -1,11 +1,12 @@ -// TODO: reinstate module - use std::{ - fmt::{self, Display}, + cmp, + fmt::{self, Display, Write}, str::FromStr, }; -use super::{parsing::from_one_raw_str, Header, Raw}; +use actix_http::{error::ParseError, header, HttpMessage}; + +use super::{Header, HeaderName, HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; /// `Range` header, defined /// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) @@ -16,7 +17,7 @@ use super::{parsing::from_one_raw_str, Header, Raw}; /// /// # ABNF /// ```plain -/// Range = byte-ranges-specifier / other-ranges-specifier +/// Range = byte-ranges-specifier / other-ranges-specifier /// other-ranges-specifier = other-range-unit "=" other-range-set /// other-range-set = 1*VCHAR /// @@ -25,13 +26,15 @@ use super::{parsing::from_one_raw_str, Header, Raw}; /// byte-ranges-specifier = bytes-unit "=" byte-range-set /// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) /// byte-range-spec = first-byte-pos "-" [last-byte-pos] +/// suffix-byte-range-spec = "-" suffix-length +/// suffix-length = 1*DIGIT /// first-byte-pos = 1*DIGIT /// last-byte-pos = 1*DIGIT /// ``` /// /// # Example Values /// * `bytes=1000-` -/// * `bytes=-2000` +/// * `bytes=-50` /// * `bytes=0-1,30-40` /// * `bytes=0-10,20-90,-100` /// * `custom_unit=0-123` @@ -39,81 +42,81 @@ use super::{parsing::from_one_raw_str, Header, Raw}; /// /// # Examples /// ``` -/// use hyper::header::{Headers, Range, ByteRangeSpec}; +/// use actix_web::http::header::{Range, ByteRangeSpec}; +/// use actix_web::HttpResponse; /// -/// let mut headers = Headers::new(); -/// headers.set(Range::Bytes( -/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] +/// let mut builder = HttpResponse::Ok(); +/// builder.insert_header(Range::Bytes( +/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::From(200)] /// )); -/// -/// headers.clear(); -/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); -/// ``` -/// -/// ``` -/// use hyper::header::{Headers, Range}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::bytes(1, 100)); -/// -/// headers.clear(); -/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); +/// builder.insert_header(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); +/// builder.insert_header(Range::bytes(1, 100)); +/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); /// ``` #[derive(PartialEq, Clone, Debug)] pub enum Range { - /// Byte range + /// Byte range. Bytes(Vec), - /// Custom range, with unit not registered at IANA + /// Custom range, with unit not registered at IANA. + /// /// (`other-range-unit`: String , `other-range-set`: String) Unregistered(String, String), } -/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. -/// Each `ByteRangeSpec` defines a range of bytes to fetch -#[derive(PartialEq, Clone, Debug)] +/// A range of bytes to fetch. +/// +/// Each [`Range::Bytes`] header can contain one or more `ByteRangeSpec`s. +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ByteRangeSpec { - /// Get all bytes between x and y ("x-y") + /// All bytes from `x` to `y`, inclusive. + /// + /// Serialized as `x-y`. + /// + /// Example: `bytes=500-999` would represent the second 500 bytes. FromTo(u64, u64), - /// Get all bytes starting from x ("x-") - AllFrom(u64), + /// All bytes starting from `x`, inclusive. + /// + /// Serialized as `x-`. + /// + /// Example: For a file of 1000 bytes, `bytes=950-` would represent bytes 950-999, inclusive. + From(u64), - /// Get last x bytes ("-x") + /// The last `y` bytes, inclusive. + /// + /// Using the spec terminology, this is `suffix-byte-range-spec`. Serialized as `-y`. + /// + /// Example: For a file of 1000 bytes, `bytes=-50` is equivalent to `bytes=950-`. Last(u64), } impl ByteRangeSpec { - /// Given the full length of the entity, attempt to normalize the byte range - /// into an satisfiable end-inclusive (from, to) range. + /// Given the full length of the entity, attempt to normalize the byte range into an satisfiable + /// end-inclusive `(from, to)` range. /// - /// The resulting range is guaranteed to be a satisfiable range within the - /// bounds of `0 <= from <= to < full_length`. + /// The resulting range is guaranteed to be a satisfiable range within the bounds + /// of `0 <= from <= to < full_length`. /// - /// If the byte range is deemed unsatisfiable, `None` is returned. - /// An unsatisfiable range is generally cause for a server to either reject - /// the client request with a `416 Range Not Satisfiable` status code, or to - /// simply ignore the range header and serve the full entity using a `200 - /// OK` status code. + /// If the byte range is deemed unsatisfiable, `None` is returned. An unsatisfiable range is + /// generally cause for a server to either reject the client request with a + /// `416 Range Not Satisfiable` status code, or to simply ignore the range header and serve the + /// full entity using a `200 OK` status code. /// - /// This function closely follows [RFC 7233 §2.1]. - /// As such, it considers ranges to be satisfiable if they meet the - /// following conditions: + /// This function closely follows [RFC 7233 §2.1]. As such, it considers ranges to be + /// satisfiable if they meet the following conditions: /// - /// > If a valid byte-range-set includes at least one byte-range-spec with - /// a first-byte-pos that is less than the current length of the - /// representation, or at least one suffix-byte-range-spec with a - /// non-zero suffix-length, then the byte-range-set is satisfiable. - /// Otherwise, the byte-range-set is unsatisfiable. + /// > If a valid byte-range-set includes at least one byte-range-spec with a first-byte-pos that + /// is less than the current length of the representation, or at least one + /// suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set + /// is satisfiable. Otherwise, the byte-range-set is unsatisfiable. /// /// The function also computes remainder ranges based on the RFC: /// - /// > If the last-byte-pos value is - /// absent, or if the value is greater than or equal to the current - /// length of the representation data, the byte range is interpreted as - /// the remainder of the representation (i.e., the server replaces the - /// value of last-byte-pos with a value that is one less than the current - /// length of the selected representation). + /// > If the last-byte-pos value is absent, or if the value is greater than or equal to the + /// current length of the representation data, the byte range is interpreted as the remainder + /// of the representation (i.e., the server replaces the value of last-byte-pos with a value + /// that is one less than the current length of the selected representation). /// /// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233 pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { @@ -121,26 +124,28 @@ impl ByteRangeSpec { if full_length == 0 { return None; } - match self { - &ByteRangeSpec::FromTo(from, to) => { + + match *self { + ByteRangeSpec::FromTo(from, to) => { if from < full_length && from <= to { - Some((from, ::std::cmp::min(to, full_length - 1))) + Some((from, cmp::min(to, full_length - 1))) } else { None } } - &ByteRangeSpec::AllFrom(from) => { + + ByteRangeSpec::From(from) => { if from < full_length { Some((from, full_length - 1)) } else { None } } - &ByteRangeSpec::Last(last) => { + + ByteRangeSpec::Last(last) => { if last > 0 { - // From the RFC: If the selected representation is shorter - // than the specified suffix-length, - // the entire representation is used. + // From the RFC: If the selected representation is shorter than the specified + // suffix-length, the entire representation is used. if last > full_length { Some((0, full_length - 1)) } else { @@ -155,48 +160,53 @@ impl ByteRangeSpec { } impl Range { - /// Get the most common byte range header ("bytes=from-to") + /// Constructs a common byte range header. + /// + /// Eg: `bytes=from-to` pub fn bytes(from: u64, to: u64) -> Range { Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) } - /// Get byte range header with multiple subranges - /// ("bytes=from1-to1,from2-to2,fromX-toX") + /// Constructs a byte range header with multiple subranges. + /// + /// Eg: `bytes=from1-to1,from2-to2,fromX-toX` pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { Range::Bytes( ranges - .iter() - .map(|r| ByteRangeSpec::FromTo(r.0, r.1)) + .into_iter() + .map(|(from, to)| ByteRangeSpec::FromTo(from, to)) .collect(), ) } } impl fmt::Display for ByteRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), - ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), + ByteRangeSpec::From(pos) => write!(f, "{}-", pos), } } } impl fmt::Display for Range { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Range::Bytes(ref ranges) => { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Range::Bytes(ranges) => { write!(f, "bytes=")?; for (i, range) in ranges.iter().enumerate() { if i != 0 { f.write_str(",")?; } + Display::fmt(range, f)?; } Ok(()) } - Range::Unregistered(ref unit, ref range_str) => { + + Range::Unregistered(unit, range_str) => { write!(f, "{}={}", unit, range_str) } } @@ -204,89 +214,118 @@ impl fmt::Display for Range { } impl FromStr for Range { - type Err = ::Error; + type Err = ParseError; - fn from_str(s: &str) -> ::Result { - let mut iter = s.splitn(2, '='); + fn from_str(s: &str) -> Result { + let (unit, val) = s.split_once('=').ok_or(ParseError::Header)?; - match (iter.next(), iter.next()) { - (Some("bytes"), Some(ranges)) => { + match (unit, val) { + ("bytes", ranges) => { let ranges = from_comma_delimited(ranges); + if ranges.is_empty() { - return Err(::Error::Header); + return Err(ParseError::Header); } + Ok(Range::Bytes(ranges)) } - (Some(unit), Some(range_str)) if unit != "" && range_str != "" => { - Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())) - } - _ => Err(::Error::Header), + + (_, "") => Err(ParseError::Header), + ("", _) => Err(ParseError::Header), + + (unit, range_str) => Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())), } } } impl FromStr for ByteRangeSpec { - type Err = ::Error; + type Err = ParseError; - fn from_str(s: &str) -> ::Result { - let mut parts = s.splitn(2, '-'); + fn from_str(s: &str) -> Result { + let (start, end) = s.split_once('-').ok_or(ParseError::Header)?; - match (parts.next(), parts.next()) { - (Some(""), Some(end)) => end + match (start, end) { + ("", end) => end .parse() - .or(Err(::Error::Header)) + .or(Err(ParseError::Header)) .map(ByteRangeSpec::Last), - (Some(start), Some("")) => start + + (start, "") => start .parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::AllFrom), - (Some(start), Some(end)) => match (start.parse(), end.parse()) { + .or(Err(ParseError::Header)) + .map(ByteRangeSpec::From), + + (start, end) => match (start.parse(), end.parse()) { (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)), - _ => Err(::Error::Header), + _ => Err(ParseError::Header), }, - _ => Err(::Error::Header), } } } impl Header for Range { - fn header_name() -> &'static str { - static NAME: &'static str = "Range"; - NAME + fn name() -> HeaderName { + header::RANGE } - fn parse_header(raw: &Raw) -> ::Result { - from_one_raw_str(raw) + #[inline] + fn parse(msg: &T) -> Result { + header::from_one_raw_str(msg.headers().get(&Self::name())) } +} - fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { - f.fmt_line(self) +impl IntoHeaderValue for Range { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let mut wrt = Writer::new(); + let _ = write!(wrt, "{}", self); + HeaderValue::from_maybe_shared(wrt.take()) } } +/// Parses 0 or more items out of a comma delimited string, ignoring invalid items. +fn from_comma_delimited(s: &str) -> Vec { + s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y), + }) + .filter_map(|x| x.parse().ok()) + .collect() +} + #[cfg(test)] mod tests { + use actix_http::{test::TestRequest, Request}; + use super::*; + fn req(s: &str) -> Request { + TestRequest::default() + .insert_header((header::RANGE, s)) + .finish() + } + #[test] fn test_parse_bytes_range_valid() { - let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); + let r: Range = Header::parse(&req("bytes=1-100")).unwrap(); + let r2: Range = Header::parse(&req("bytes=1-100,-")).unwrap(); let r3 = Range::bytes(1, 100); assert_eq!(r, r2); assert_eq!(r2, r3); - let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r: Range = Header::parse(&req("bytes=1-100,200-")).unwrap(); + let r2: Range = Header::parse(&req("bytes= 1-100 , 101-xxx, 200- ")).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::AllFrom(200), + ByteRangeSpec::From(200), ]); assert_eq!(r, r2); assert_eq!(r2, r3); - let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); + let r: Range = Header::parse(&req("bytes=1-100,-100")).unwrap(); + let r2: Range = Header::parse(&req("bytes=1-100, ,,-100")).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::Last(100), @@ -294,71 +333,65 @@ mod tests { assert_eq!(r, r2); assert_eq!(r2, r3); - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); assert_eq!(r, r2); } #[test] fn test_parse_unregistered_range_valid() { - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); assert_eq!(r, r2); - let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); + let r: Range = Header::parse(&req("custom=abcd")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); assert_eq!(r, r2); - let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); + let r: Range = Header::parse(&req("custom=xxx-yyy")).unwrap(); let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); assert_eq!(r, r2); } #[test] fn test_parse_invalid() { - let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); + let r: Result = Header::parse(&req("bytes=1-a,-")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); + let r: Result = Header::parse(&req("bytes=1-2-3")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"abc".into()); + let r: Result = Header::parse(&req("abc")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); + let r: Result = Header::parse(&req("bytes=1-100=")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"bytes=".into()); + let r: Result = Header::parse(&req("bytes=")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"custom=".into()); + let r: Result = Header::parse(&req("custom=")); assert_eq!(r.ok(), None); - let r: ::Result = Header::parse_header(&"=1-100".into()); + let r: Result = Header::parse(&req("=1-100")); assert_eq!(r.ok(), None); } #[test] fn test_fmt() { - use header::Headers; - - let mut headers = Headers::new(); - - headers.set(Range::Bytes(vec![ + let range = Range::Bytes(vec![ ByteRangeSpec::FromTo(0, 1000), - ByteRangeSpec::AllFrom(2000), - ])); - assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); + ByteRangeSpec::From(2000), + ]); + assert_eq!(&range.to_string(), "bytes=0-1000,2000-"); - headers.clear(); - headers.set(Range::Bytes(vec![])); + let range = Range::Bytes(vec![]); - assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); + assert_eq!(&range.to_string(), "bytes="); - headers.clear(); - headers.set(Range::Unregistered("custom".to_owned(), "1-xxx".to_owned())); + let range = Range::Unregistered("custom".to_owned(), "1-xxx".to_owned()); - assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); + assert_eq!(&range.to_string(), "custom=1-xxx"); } #[test] @@ -379,17 +412,11 @@ mod tests { assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3)); assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0)); - assert_eq!( - Some((0, 2)), - ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) - ); - assert_eq!(None, ByteRangeSpec::AllFrom(3).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::AllFrom(5).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::AllFrom(0).to_satisfiable_range(0)); + assert_eq!(Some((0, 2)), ByteRangeSpec::From(0).to_satisfiable_range(3)); + assert_eq!(Some((2, 2)), ByteRangeSpec::From(2).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::From(3).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::From(5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::From(0).to_satisfiable_range(0)); assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3)); assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3)); From e1a2d9c60684820723a539f4005f3af35281884a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 5 Dec 2021 03:38:08 +0000 Subject: [PATCH 98/99] `Quality` / `QualityItem` improvements (#2486) --- .cargo/config.toml | 2 + actix-http/CHANGES.md | 6 + actix-http/Cargo.toml | 4 + actix-http/benches/quality-value.rs | 90 ++++++++ actix-http/src/header/mod.rs | 7 +- actix-http/src/header/shared/extended.rs | 2 +- actix-http/src/header/shared/mod.rs | 4 +- actix-http/src/header/shared/quality.rs | 208 ++++++++++++++++++ actix-http/src/header/shared/quality_item.rs | 216 ++++++------------- actix-http/src/header/utils.rs | 5 +- actix-multipart/src/server.rs | 11 +- src/http/header/accept.rs | 74 +++---- src/http/header/accept_charset.rs | 12 +- src/http/header/accept_encoding.rs | 24 ++- src/http/header/accept_language.rs | 76 ++++--- src/http/header/content_language.rs | 10 +- 16 files changed, 494 insertions(+), 257 deletions(-) create mode 100644 actix-http/benches/quality-value.rs create mode 100644 actix-http/src/header/shared/quality.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 606c30de..4425e0dd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,8 +5,10 @@ lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dcli # lib checking ci-check-min = "hack --workspace check --no-default-features" ci-check-default = "hack --workspace check" +ci-check-default-tests = "check --workspace --tests" ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check" ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" # testing +ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 773f1ff3..87738058 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -11,6 +11,9 @@ * `impl Clone for ws::HandshakeError`. [#2468] * `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] * `impl Default ` for `ws::Codec`. [#1920] +* `header::QualityItem::{max, min}`. [#2486] +* `header::Quality::{MAX, MIN}`. [#2486] +* `impl Display` for `header::Quality`. [#2486] ### Changed * Rename `body::BoxBody::{from_body => new}`. [#2468] @@ -27,10 +30,13 @@ * Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] * Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] * `impl Copy` for `ws::Codec`. [#1920] +* `header::qitem` helper. Replaced with `header::QualityItem::max` [#2486] +* `impl TryFrom` for `header::Quality` [#2486] [#2483]: https://github.com/actix/actix-web/pull/2483 [#2468]: https://github.com/actix/actix-web/pull/2468 [#1920]: https://github.com/actix/actix-web/pull/1920 +[#2486]: https://github.com/actix/actix-web/pull/2486 ## 3.0.0-beta.14 - 2021-11-30 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index f8b15df7..967f04d0 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -112,3 +112,7 @@ harness = false [[bench]] name = "uninit-headers" harness = false + +[[bench]] +name = "quality-value" +harness = false diff --git a/actix-http/benches/quality-value.rs b/actix-http/benches/quality-value.rs new file mode 100644 index 00000000..31b67f99 --- /dev/null +++ b/actix-http/benches/quality-value.rs @@ -0,0 +1,90 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +const CODES: &[u16] = &[0, 1000, 201, 800, 550]; + +fn bench_quality_display_impls(c: &mut Criterion) { + let mut group = c.benchmark_group("quality value display impls"); + + for i in CODES.iter() { + group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| { + b.iter(|| _new::Quality(i).to_string()) + }); + + group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { + b.iter(|| _naive::Quality(i).to_string()) + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_quality_display_impls); +criterion_main!(benches); + +mod _new { + use std::fmt; + + pub struct Quality(pub(crate) u16); + + impl fmt::Display for Quality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => f.write_str("0"), + 1000 => f.write_str("1"), + + // some number in the range 1–999 + x => { + f.write_str("0.")?; + + // this implementation avoids string allocation otherwise required + // for `.trim_end_matches('0')` + + if x < 10 { + f.write_str("00")?; + // 0 is handled so it's not possible to have a trailing 0, we can just return + itoa::fmt(f, x) + } else if x < 100 { + f.write_str("0")?; + if x % 10 == 0 { + // trailing 0, divide by 10 and write + itoa::fmt(f, x / 10) + } else { + itoa::fmt(f, x) + } + } else { + // x is in range 101–999 + + if x % 100 == 0 { + // two trailing 0s, divide by 100 and write + itoa::fmt(f, x / 100) + } else if x % 10 == 0 { + // one trailing 0, divide by 10 and write + itoa::fmt(f, x / 10) + } else { + itoa::fmt(f, x) + } + } + } + } + } + } +} + +mod _naive { + use std::fmt; + + pub struct Quality(pub(crate) u16); + + impl fmt::Display for Quality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => f.write_str("0"), + 1000 => f.write_str("1"), + + x => { + write!(f, "{}", format!("{:03}", x).trim_end_matches('0')) + } + } + } + } +} diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 308cb012..381842e7 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -38,13 +38,14 @@ pub mod map; mod shared; mod utils; -#[doc(hidden)] -pub use self::shared::*; - pub use self::as_name::AsHeaderName; pub use self::into_pair::IntoHeaderPair; pub use self::into_value::IntoHeaderValue; pub use self::map::HeaderMap; +pub use self::shared::{ + parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, + LanguageTag, Quality, QualityItem, +}; pub use self::utils::{ fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, }; diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index b2cf1d75..60f2d359 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -1,4 +1,4 @@ -// Originally from hyper v0.11.27 src/header/parsing.rs +//! Originally taken from `hyper::header::parsing`. use std::{fmt, str::FromStr}; diff --git a/actix-http/src/header/shared/mod.rs b/actix-http/src/header/shared/mod.rs index 274e1314..257e54d7 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -4,11 +4,13 @@ mod charset; mod content_encoding; mod extended; mod http_date; +mod quality; mod quality_item; pub use self::charset::Charset; pub use self::content_encoding::ContentEncoding; pub use self::extended::{parse_extended_value, ExtendedValue}; pub use self::http_date::HttpDate; -pub use self::quality_item::{q, qitem, Quality, QualityItem}; +pub use self::quality::{q, Quality}; +pub use self::quality_item::QualityItem; pub use language_tags::LanguageTag; diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs new file mode 100644 index 00000000..5321c754 --- /dev/null +++ b/actix-http/src/header/shared/quality.rs @@ -0,0 +1,208 @@ +use std::{ + convert::{TryFrom, TryInto}, + fmt, +}; + +use derive_more::{Display, Error}; + +const MAX_QUALITY_INT: u16 = 1000; +const MAX_QUALITY_FLOAT: f32 = 1.0; + +/// Represents a quality used in q-factor values. +/// +/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value). +/// +/// # Implementation notes +/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places. +/// This means there are 1001 possible values. Since floating point numbers are not exact and the +/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store +/// the quality internally. +/// +/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields. +/// +/// # Examples +/// ``` +/// use actix_http::header::{Quality, q}; +/// assert_eq!(q(1.0), Quality::MAX); +/// +/// assert_eq!(q(0.42).to_string(), "0.42"); +/// assert_eq!(q(1.0).to_string(), "1"); +/// assert_eq!(Quality::MIN.to_string(), "0"); +/// ``` +/// +/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1 +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Quality(pub(super) u16); + +impl Quality { + /// The maximum quality value, equivalent to `q=1.0`. + pub const MAX: Quality = Quality(MAX_QUALITY_INT); + + /// The minimum quality value, equivalent to `q=0.0`. + pub const MIN: Quality = Quality(0); + + /// Converts a float in the range 0.0–1.0 to a `Quality`. + /// + /// Intentionally private. External uses should rely on the `TryFrom` impl. + /// + /// # Panics + /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0. + fn from_f32(value: f32) -> Self { + // Check that `value` is within range should be done before calling this method. + // Just in case, this debug_assert should catch if we were forgetful. + debug_assert!( + (0.0f32..=1.0f32).contains(&value), + "q value must be between 0.0 and 1.0" + ); + + Quality((value * MAX_QUALITY_INT as f32) as u16) + } +} + +/// The default value is [`Quality::MAX`]. +impl Default for Quality { + fn default() -> Quality { + Quality::MAX + } +} + +impl fmt::Display for Quality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + 0 => f.write_str("0"), + MAX_QUALITY_INT => f.write_str("1"), + + // some number in the range 1–999 + x => { + f.write_str("0.")?; + + // This implementation avoids string allocation for removing trailing zeroes. + // In benchmarks it is twice as fast as approach using something like + // `format!("{}").trim_end_matches('0')` for non-fast-path quality values. + + if x < 10 { + // x in is range 1–9 + + f.write_str("00")?; + + // 0 is already handled so it's not possible to have a trailing 0 in this range + // we can just write the integer + itoa::fmt(f, x) + } else if x < 100 { + // x in is range 10–99 + + f.write_str("0")?; + + if x % 10 == 0 { + // trailing 0, divide by 10 and write + itoa::fmt(f, x / 10) + } else { + itoa::fmt(f, x) + } + } else { + // x is in range 100–999 + + if x % 100 == 0 { + // two trailing 0s, divide by 100 and write + itoa::fmt(f, x / 100) + } else if x % 10 == 0 { + // one trailing 0, divide by 10 and write + itoa::fmt(f, x / 10) + } else { + itoa::fmt(f, x) + } + } + } + } + } +} + +#[derive(Debug, Clone, Display, Error)] +#[display(fmt = "quality out of bounds")] +#[non_exhaustive] +pub struct QualityOutOfBounds; + +impl TryFrom for Quality { + type Error = QualityOutOfBounds; + + #[inline] + fn try_from(value: f32) -> Result { + if (0.0..=MAX_QUALITY_FLOAT).contains(&value) { + Ok(Quality::from_f32(value)) + } else { + Err(QualityOutOfBounds) + } + } +} + +/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0). +/// +/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible. +/// +/// # Panics +/// Panics if value is out of range. +/// +/// # Examples +/// ``` +/// # use actix_http::header::{q, Quality}; +/// let q1 = q(1.0); +/// assert_eq!(q1, Quality::MAX); +/// +/// let q2 = q(0.0); +/// assert_eq!(q2, Quality::MIN); +/// +/// let q3 = q(0.42); +/// ``` +/// +/// An out-of-range `f32` quality will panic. +/// ```should_panic +/// # use actix_http::header::q; +/// let _q2 = q(1.42); +/// ``` +#[inline] +pub fn q(quality: T) -> Quality +where + T: TryInto, + T::Error: fmt::Debug, +{ + quality.try_into().expect("quality value was out of bounds") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn q_helper() { + assert_eq!(q(0.5), Quality(500)); + } + + #[test] + fn display_output() { + assert_eq!(q(0.0).to_string(), "0"); + assert_eq!(q(1.0).to_string(), "1"); + assert_eq!(q(0.001).to_string(), "0.001"); + assert_eq!(q(0.5).to_string(), "0.5"); + assert_eq!(q(0.22).to_string(), "0.22"); + assert_eq!(q(0.123).to_string(), "0.123"); + assert_eq!(q(0.999).to_string(), "0.999"); + + for x in 0..=1000 { + // if trailing zeroes are handled correctly, we would not expect the serialized length + // to ever exceed "0." + 3 decimal places = 5 in length + assert!(q(x as f32 / 1000.0).to_string().len() <= 5); + } + } + + #[test] + #[should_panic] + fn negative_quality() { + q(-1.0); + } + + #[test] + #[should_panic] + fn quality_out_of_bounds() { + q(2.0); + } +} diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index a109b44e..9354915a 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,85 +1,36 @@ -use std::{ - cmp, - convert::{TryFrom, TryInto}, - fmt, str, -}; - -use derive_more::{Display, Error}; +use std::{cmp, convert::TryFrom as _, fmt, str}; use crate::error::ParseError; -const MAX_QUALITY: u16 = 1000; -const MAX_FLOAT_QUALITY: f32 = 1.0; - -/// Represents a quality used in quality values. -/// -/// Can be created with the [`q`] function. -/// -/// # Implementation notes -/// -/// The quality value is defined as a number between 0 and 1 with three decimal -/// places. This means there are 1001 possible values. Since floating point -/// numbers are not exact and the smallest floating point data type (`f32`) -/// consumes four bytes, hyper uses an `u16` value to store the -/// quality internally. For performance reasons you may set quality directly to -/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality -/// `q=0.532`. -/// -/// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more -/// information on quality values in HTTP header fields. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Quality(u16); - -impl Quality { - /// # Panics - /// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0. - fn from_f32(value: f32) -> Self { - // Check that `value` is within range should be done before calling this method. - // Just in case, this debug_assert should catch if we were forgetful. - debug_assert!( - (0.0f32..=1.0f32).contains(&value), - "q value must be between 0.0 and 1.0" - ); - - Quality((value * MAX_QUALITY as f32) as u16) - } -} - -impl Default for Quality { - fn default() -> Quality { - Quality(MAX_QUALITY) - } -} - -#[derive(Debug, Clone, Display, Error)] -pub struct QualityOutOfBounds; - -impl TryFrom for Quality { - type Error = QualityOutOfBounds; - - fn try_from(value: u16) -> Result { - if (0..=MAX_QUALITY).contains(&value) { - Ok(Quality(value)) - } else { - Err(QualityOutOfBounds) - } - } -} - -impl TryFrom for Quality { - type Error = QualityOutOfBounds; - - fn try_from(value: f32) -> Result { - if (0.0..=MAX_FLOAT_QUALITY).contains(&value) { - Ok(Quality::from_f32(value)) - } else { - Err(QualityOutOfBounds) - } - } -} +use super::Quality; /// Represents an item with a quality value as defined /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). +/// +/// # Parsing and Formatting +/// This wrapper be used to parse header value items that have a q-factor annotation as well as +/// serialize items with a their q-factor. +/// +/// # Ordering +/// Since this context of use for this type is header value items, ordering is defined for +/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as +/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a +/// correctly sorted sequence. +/// +/// # Examples +/// ``` +/// # use actix_http::header::{QualityItem, q}; +/// let q_item: QualityItem = "hello;q=0.3".parse().unwrap(); +/// assert_eq!(&q_item.item, "hello"); +/// assert_eq!(q_item.quality, q(0.3)); +/// +/// // note that format is normalized compared to parsed item +/// assert_eq!(q_item.to_string(), "hello; q=0.3"); +/// +/// // item with q=0.3 is greater than item with q=0.1 +/// let q_item_fallback: QualityItem = "abc;q=0.1".parse().unwrap(); +/// assert!(q_item > q_item_fallback); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct QualityItem { /// The wrapped contents of the field. @@ -93,12 +44,22 @@ impl QualityItem { /// Constructs a new `QualityItem` from an item and a quality value. /// /// The item can be of any type. The quality should be a value in the range [0, 1]. - pub fn new(item: T, quality: Quality) -> QualityItem { + pub fn new(item: T, quality: Quality) -> Self { QualityItem { item, quality } } + + /// Constructs a new `QualityItem` from an item, using the maximum q-value. + pub fn max(item: T) -> Self { + Self::new(item, Quality::MAX) + } + + /// Constructs a new `QualityItem` from an item, using the minimum q-value. + pub fn min(item: T) -> Self { + Self::new(item, Quality::MIN) + } } -impl cmp::PartialOrd for QualityItem { +impl PartialOrd for QualityItem { fn partial_cmp(&self, other: &QualityItem) -> Option { self.quality.partial_cmp(&other.quality) } @@ -108,10 +69,12 @@ impl fmt::Display for QualityItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.item, f)?; - match self.quality.0 { - MAX_QUALITY => Ok(()), - 0 => f.write_str("; q=0"), - x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')), + match self.quality { + // q-factor value is implied for max value + Quality::MAX => Ok(()), + + Quality::MIN => f.write_str("; q=0"), + q => write!(f, "; q={}", q), } } } @@ -119,78 +82,58 @@ impl fmt::Display for QualityItem { impl str::FromStr for QualityItem { type Err = ParseError; - fn from_str(qitem_str: &str) -> Result { - if !qitem_str.is_ascii() { + fn from_str(q_item_str: &str) -> Result { + if !q_item_str.is_ascii() { return Err(ParseError::Header); } - // Set defaults used if parsing fails. - let mut raw_item = qitem_str; - let mut quality = 1f32; + // set defaults used if quality-item parsing fails, i.e., item has no q attribute + let mut raw_item = q_item_str; + let mut quality = Quality::MAX; - // TODO: MSRV(1.52): use rsplit_once - let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect(); + let parts = q_item_str + .rsplit_once(';') + .map(|(item, q_attr)| (item.trim(), q_attr.trim())); - if parts.len() == 2 { + if let Some((val, q_attr)) = parts { // example for item with q-factor: // - // gzip; q=0.65 - // ^^^^^^ parts[0] - // ^^ start - // ^^^^ q_val - // ^^^^ parts[1] + // gzip;q=0.65 + // ^^^^ val + // ^^^^^^ q_attr + // ^^ q + // ^^^^ q_val - if parts[0].len() < 2 { + if q_attr.len() < 2 { // Can't possibly be an attribute since an attribute needs at least a name followed // by an equals sign. And bare identifiers are forbidden. return Err(ParseError::Header); } - let start = &parts[0][0..2]; + let q = &q_attr[0..2]; - if start == "q=" || start == "Q=" { - let q_val = &parts[0][2..]; + if q == "q=" || q == "Q=" { + let q_val = &q_attr[2..]; if q_val.len() > 5 { // longer than 5 indicates an over-precise q-factor return Err(ParseError::Header); } let q_value = q_val.parse::().map_err(|_| ParseError::Header)?; + let q_value = + Quality::try_from(q_value).map_err(|_| ParseError::Header)?; - if (0f32..=1f32).contains(&q_value) { - quality = q_value; - raw_item = parts[1]; - } else { - return Err(ParseError::Header); - } + quality = q_value; + raw_item = val; } } let item = raw_item.parse::().map_err(|_| ParseError::Header)?; - // we already checked above that the quality is within range - Ok(QualityItem::new(item, Quality::from_f32(quality))) + Ok(QualityItem::new(item, quality)) } } -/// Convenience function to wrap a value in a `QualityItem` -/// Sets `q` to the default 1.0 -pub fn qitem(item: T) -> QualityItem { - QualityItem::new(item, Quality::default()) -} - -/// Convenience function to create a `Quality` from a float or integer. -/// -/// Implemented for `u16` and `f32`. Panics if value is out of range. -pub fn q(val: T) -> Quality -where - T: TryInto, - T::Error: fmt::Debug, -{ - // TODO: on next breaking change, handle unwrap differently - val.try_into().unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -245,7 +188,7 @@ mod tests { #[test] fn test_quality_item_fmt_q_1() { use Encoding::*; - let x = qitem(Chunked); + let x = QualityItem::max(Chunked); assert_eq!(format!("{}", x), "chunked"); } #[test] @@ -344,25 +287,8 @@ mod tests { fn test_quality_item_ordering() { let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); - let comparision_result: bool = x.gt(&y); - assert!(comparision_result) - } - - #[test] - fn test_quality() { - assert_eq!(q(0.5), Quality(500)); - } - - #[test] - #[should_panic] - fn test_quality_invalid() { - q(-1.0); - } - - #[test] - #[should_panic] - fn test_quality_invalid2() { - q(2.0); + let comparison_result: bool = x.gt(&y); + assert!(comparison_result) } #[test] diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index a23f5b75..f4f34d34 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -65,8 +65,9 @@ where Ok(()) } -/// Percent encode a sequence of bytes with a character set defined in -/// +/// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2]. +/// +/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2 #[inline] pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 23397b7e..8eabcee1 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -435,10 +435,10 @@ impl Field { /// Returns the field's Content-Disposition. /// - /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the - /// disposition type is "form-data". The Content-Disposition header field MUST also contain an - /// additional parameter of "name"; the value of the "name" parameter is the original field name - /// from the form.' + /// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the + /// disposition type is `form-data`. The Content-Disposition header field MUST also contain an + /// additional parameter of `name`; the value of the `name` parameter is the original field name + /// from the form." /// /// This crate validates that it exists before returning a `Field`. As such, it is safe to /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as @@ -451,7 +451,8 @@ impl Field { /// Returns the field's name. /// - /// See [content_disposition] regarding guarantees about + /// See [content_disposition](Self::content_disposition) regarding guarantees about existence of + /// the name field. pub fn name(&self) -> &str { self.content_disposition() .get_name() diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 70e4118c..c61e6ab4 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use mime::Mime; -use super::{qitem, QualityItem}; +use super::QualityItem; use crate::http::header; crate::http::header::common_header! { @@ -34,46 +34,40 @@ crate::http::header::common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; + /// use actix_web::http::header::{Accept, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ - /// qitem(mime::TEXT_HTML), + /// QualityItem::max(mime::TEXT_HTML), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; + /// use actix_web::http::header::{Accept, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ - /// qitem(mime::APPLICATION_JSON), + /// QualityItem::max(mime::APPLICATION_JSON), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem, q, qitem}; + /// use actix_web::http::header::{Accept, QualityItem, q}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// qitem("application/xhtml+xml".parse().unwrap()), - /// QualityItem::new( - /// mime::TEXT_XML, - /// q(900) - /// ), - /// qitem("image/webp".parse().unwrap()), - /// QualityItem::new( - /// mime::STAR_STAR, - /// q(800) - /// ), + /// QualityItem::max(mime::TEXT_HTML), + /// QualityItem::max("application/xhtml+xml".parse().unwrap()), + /// QualityItem::new(mime::TEXT_XML, q(0.9)), + /// QualityItem::max("image/webp".parse().unwrap()), + /// QualityItem::new(mime::STAR_STAR, q(0.8)), /// ]) /// ); /// ``` @@ -85,20 +79,20 @@ crate::http::header::common_header! { test1, vec![b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ - QualityItem::new("audio/*".parse().unwrap(), q(200)), - qitem("audio/basic".parse().unwrap()), + QualityItem::new("audio/*".parse().unwrap(), q(0.2)), + QualityItem::max("audio/basic".parse().unwrap()), ]))); crate::http::header::common_header_test!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ - QualityItem::new(mime::TEXT_PLAIN, q(500)), - qitem(mime::TEXT_HTML), + QualityItem::new(mime::TEXT_PLAIN, q(0.5)), + QualityItem::max(mime::TEXT_HTML), QualityItem::new( "text/x-dvi".parse().unwrap(), - q(800)), - qitem("text/x-c".parse().unwrap()), + q(0.8)), + QualityItem::max("text/x-c".parse().unwrap()), ]))); // Custom tests @@ -106,14 +100,14 @@ crate::http::header::common_header! { test3, vec![b"text/plain; charset=utf-8"], Some(Accept(vec![ - qitem(mime::TEXT_PLAIN_UTF_8), + QualityItem::max(mime::TEXT_PLAIN_UTF_8), ]))); crate::http::header::common_header_test!( test4, vec![b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN_UTF_8, - q(500)), + q(0.5)), ]))); #[test] @@ -130,27 +124,27 @@ crate::http::header::common_header! { impl Accept { /// Construct `Accept: */*`. pub fn star() -> Accept { - Accept(vec![qitem(mime::STAR_STAR)]) + Accept(vec![QualityItem::max(mime::STAR_STAR)]) } /// Construct `Accept: application/json`. pub fn json() -> Accept { - Accept(vec![qitem(mime::APPLICATION_JSON)]) + Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]) } /// Construct `Accept: text/*`. pub fn text() -> Accept { - Accept(vec![qitem(mime::TEXT_STAR)]) + Accept(vec![QualityItem::max(mime::TEXT_STAR)]) } /// Construct `Accept: image/*`. pub fn image() -> Accept { - Accept(vec![qitem(mime::IMAGE_STAR)]) + Accept(vec![QualityItem::max(mime::IMAGE_STAR)]) } /// Construct `Accept: text/html`. pub fn html() -> Accept { - Accept(vec![qitem(mime::TEXT_HTML)]) + Accept(vec![QualityItem::max(mime::TEXT_HTML)]) } /// Returns a sorted list of mime types from highest to lowest preference, accounting for @@ -213,10 +207,10 @@ impl Accept { /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn preference(&self) -> Mime { - use actix_http::header::q; + use actix_http::header::Quality; let mut max_item = None; - let mut max_pref = q(0); + let mut max_pref = Quality::MIN; // uses manual max lookup loop since we want the first occurrence in the case of same // preference but `Iterator::max_by_key` would give us the last occurrence @@ -244,11 +238,11 @@ mod tests { let test = Accept(vec![]); assert!(test.ranked().is_empty()); - let test = Accept(vec![qitem(mime::APPLICATION_JSON)]); + let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]); assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); let test = Accept(vec![ - qitem(mime::TEXT_HTML), + QualityItem::max(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), @@ -264,9 +258,9 @@ mod tests { ); let test = Accept(vec![ - qitem(mime::STAR_STAR), - qitem(mime::IMAGE_STAR), - qitem(mime::IMAGE_PNG), + QualityItem::max(mime::STAR_STAR), + QualityItem::max(mime::IMAGE_STAR), + QualityItem::max(mime::IMAGE_PNG), ]); assert_eq!( test.ranked(), @@ -277,7 +271,7 @@ mod tests { #[test] fn preference_selection() { let test = Accept(vec![ - qitem(mime::TEXT_HTML), + QualityItem::max(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), @@ -286,9 +280,9 @@ mod tests { let test = Accept(vec![ QualityItem::new("video/*".parse().unwrap(), q(0.8)), - qitem(mime::IMAGE_PNG), + QualityItem::max(mime::IMAGE_PNG), QualityItem::new(mime::STAR_STAR, q(0.5)), - qitem(mime::IMAGE_SVG), + QualityItem::max(mime::IMAGE_SVG), QualityItem::new(mime::IMAGE_STAR, q(0.8)), ]); assert_eq!(test.preference(), mime::IMAGE_PNG); diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index 5577ab60..c8b918c9 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -22,11 +22,11 @@ crate::http::header::common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; + /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) + /// AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)]) /// ); /// ``` /// @@ -37,19 +37,19 @@ crate::http::header::common_header! { /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![ - /// QualityItem::new(Charset::Us_Ascii, q(900)), - /// QualityItem::new(Charset::Iso_8859_10, q(200)), + /// QualityItem::new(Charset::Us_Ascii, q(0.9)), + /// QualityItem::new(Charset::Iso_8859_10, q(0.2)), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; + /// use actix_web::http::header::{AcceptCharset, Charset, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) + /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) /// ); /// ``` (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index 85cd0a4f..828a0533 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -29,36 +29,38 @@ common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; + /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) + /// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)]) /// ); /// ``` + /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; + /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// qitem(Encoding::Gzip), - /// qitem(Encoding::Deflate), + /// QualityItem::max(Encoding::Chunked), + /// QualityItem::max(Encoding::Gzip), + /// QualityItem::max(Encoding::Deflate), /// ]) /// ); /// ``` + /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q, qitem}; + /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// QualityItem::new(Encoding::Gzip, q(600)), - /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), + /// QualityItem::max(Encoding::Chunked), + /// QualityItem::new(Encoding::Gzip, q(0.60)), + /// QualityItem::min(Encoding::EncodingExt("*".to_owned())), /// ]) /// ); /// ``` @@ -77,3 +79,5 @@ common_header! { common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } } + +// TODO: shortcut for EncodingExt(*) = Any diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index 229f95ef..011257b8 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -1,6 +1,6 @@ use language_tags::LanguageTag; -use super::{common_header, Preference, QualityItem}; +use super::{common_header, Preference, Quality, QualityItem}; use crate::http::header; common_header! { @@ -32,26 +32,26 @@ common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, qitem}; + /// use actix_web::http::header::{AcceptLanguage, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem("en-US".parse().unwrap()) + /// QualityItem::max("en-US".parse().unwrap()) /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; + /// use actix_web::http::header::{AcceptLanguage, QualityItem, q}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// qitem("da".parse().unwrap()), - /// QualityItem::new("en-GB".parse().unwrap(), q(800)), - /// QualityItem::new("en".parse().unwrap(), q(700)), + /// QualityItem::max("da".parse().unwrap()), + /// QualityItem::new("en-GB".parse().unwrap(), q(0.8)), + /// QualityItem::new("en".parse().unwrap(), q(0.7)), /// ]) /// ); /// ``` @@ -72,9 +72,9 @@ common_header! { not_ordered_by_weight, vec![b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ - qitem("en-US".parse().unwrap()), - QualityItem::new("en".parse().unwrap(), q(500)), - qitem("fr".parse().unwrap()), + QualityItem::max("en-US".parse().unwrap()), + QualityItem::new("en".parse().unwrap(), q(0.5)), + QualityItem::max("fr".parse().unwrap()), ])) ); @@ -82,11 +82,11 @@ common_header! { has_wildcard, vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], Some(AcceptLanguage(vec![ - qitem("fr-CH".parse().unwrap()), - QualityItem::new("fr".parse().unwrap(), q(900)), - QualityItem::new("en".parse().unwrap(), q(800)), - QualityItem::new("de".parse().unwrap(), q(700)), - QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::max("fr-CH".parse().unwrap()), + QualityItem::new("fr".parse().unwrap(), q(0.9)), + QualityItem::new("en".parse().unwrap(), q(0.8)), + QualityItem::new("de".parse().unwrap(), q(0.7)), + QualityItem::new("*".parse().unwrap(), q(0.5)), ])) ); } @@ -122,10 +122,8 @@ impl AcceptLanguage { /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn preference(&self) -> Preference { - use actix_http::header::q; - let mut max_item = None; - let mut max_pref = q(0); + let mut max_pref = Quality::MIN; // uses manual max lookup loop since we want the first occurrence in the case of same // preference but `Iterator::max_by_key` would give us the last occurrence @@ -153,15 +151,15 @@ mod tests { let test = AcceptLanguage(vec![]); assert!(test.ranked().is_empty()); - let test = AcceptLanguage(vec![qitem("fr-CH".parse().unwrap())]); + let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]); assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); let test = AcceptLanguage(vec![ - QualityItem::new("fr".parse().unwrap(), q(900)), - QualityItem::new("fr-CH".parse().unwrap(), q(1000)), - QualityItem::new("en".parse().unwrap(), q(800)), - QualityItem::new("*".parse().unwrap(), q(500)), - QualityItem::new("de".parse().unwrap(), q(700)), + QualityItem::new("fr".parse().unwrap(), q(0.900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1.0)), + QualityItem::new("en".parse().unwrap(), q(0.800)), + QualityItem::new("*".parse().unwrap(), q(0.500)), + QualityItem::new("de".parse().unwrap(), q(0.700)), ]); assert_eq!( test.ranked(), @@ -175,11 +173,11 @@ mod tests { ); let test = AcceptLanguage(vec![ - qitem("fr".parse().unwrap()), - qitem("fr-CH".parse().unwrap()), - qitem("en".parse().unwrap()), - qitem("*".parse().unwrap()), - qitem("de".parse().unwrap()), + QualityItem::max("fr".parse().unwrap()), + QualityItem::max("fr-CH".parse().unwrap()), + QualityItem::max("en".parse().unwrap()), + QualityItem::max("*".parse().unwrap()), + QualityItem::max("de".parse().unwrap()), ]); assert_eq!( test.ranked(), @@ -196,11 +194,11 @@ mod tests { #[test] fn preference_selection() { let test = AcceptLanguage(vec![ - QualityItem::new("fr".parse().unwrap(), q(900)), - QualityItem::new("fr-CH".parse().unwrap(), q(1000)), - QualityItem::new("en".parse().unwrap(), q(800)), - QualityItem::new("*".parse().unwrap(), q(500)), - QualityItem::new("de".parse().unwrap(), q(700)), + QualityItem::new("fr".parse().unwrap(), q(0.900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1.0)), + QualityItem::new("en".parse().unwrap(), q(0.800)), + QualityItem::new("*".parse().unwrap(), q(0.500)), + QualityItem::new("de".parse().unwrap(), q(0.700)), ]); assert_eq!( test.preference(), @@ -208,11 +206,11 @@ mod tests { ); let test = AcceptLanguage(vec![ - qitem("fr".parse().unwrap()), - qitem("fr-CH".parse().unwrap()), - qitem("en".parse().unwrap()), - qitem("*".parse().unwrap()), - qitem("de".parse().unwrap()), + QualityItem::max("fr".parse().unwrap()), + QualityItem::max("fr-CH".parse().unwrap()), + QualityItem::max("en".parse().unwrap()), + QualityItem::max("*".parse().unwrap()), + QualityItem::max("de".parse().unwrap()), ]); assert_eq!( test.preference(), diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs index 39ca8da5..ff317e1d 100644 --- a/src/http/header/content_language.rs +++ b/src/http/header/content_language.rs @@ -23,25 +23,25 @@ common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem}; + /// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentLanguage(vec![ - /// qitem(LanguageTag::parse("en").unwrap()), + /// QualityItem::max(LanguageTag::parse("en").unwrap()), /// ]) /// ); /// ``` /// /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem}; + /// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentLanguage(vec![ - /// qitem(LanguageTag::parse("da").unwrap()), - /// qitem(LanguageTag::parse("en-GB").unwrap()), + /// QualityItem::max(LanguageTag::parse("da").unwrap()), + /// QualityItem::max(LanguageTag::parse("en-GB").unwrap()), /// ]) /// ); /// ``` From 59be0c65c6b4766355d8bf08de27531d03730bdd Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Sun, 5 Dec 2021 07:39:18 +0300 Subject: [PATCH 99/99] disallow query or fragements in `url_for` constructions (#2430) Co-authored-by: Rob Ede --- CHANGES.md | 2 ++ src/error/mod.rs | 8 +++--- src/request.rs | 46 +++++++++++++++++------------ src/rmap.rs | 75 +++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 96 insertions(+), 35 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 78aa729d..1b108fee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,12 +12,14 @@ * Rename `Accept::{mime_precedence => ranked}`. [#2480] * Rename `Accept::{mime_preference => preference}`. [#2480] * Un-deprecate `App::data_factory`. [#2484] +* `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430] ### Fixed * Accept wildcard `*` items in `AcceptLanguage`. [#2480] * Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] * Typed headers containing lists that require one or more items now enforce this minimum. [#2482] +[#2430]: https://github.com/actix/actix-web/pull/2430 [#2468]: https://github.com/actix/actix-web/pull/2468 [#2480]: https://github.com/actix/actix-web/pull/2480 [#2482]: https://github.com/actix/actix-web/pull/2482 diff --git a/src/error/mod.rs b/src/error/mod.rs index 3ccd5bba..46d0dccc 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -29,15 +29,15 @@ pub type Result = std::result::Result; #[derive(Debug, PartialEq, Display, Error, From)] #[non_exhaustive] pub enum UrlGenerationError { - /// Resource not found + /// Resource not found. #[display(fmt = "Resource not found")] ResourceNotFound, - /// Not all path pattern covered - #[display(fmt = "Not all path pattern covered")] + /// Not all URL parameters covered. + #[display(fmt = "Not all URL parameters covered")] NotEnoughElements, - /// URL parse error + /// URL parse error. #[display(fmt = "{}", _0)] ParseError(UrlParseError), } diff --git a/src/request.rs b/src/request.rs index 0027f9b4..58222da4 100644 --- a/src/request.rs +++ b/src/request.rs @@ -100,7 +100,7 @@ impl HttpRequest { &self.head().headers } - /// The target path of this Request. + /// The target path of this request. #[inline] pub fn path(&self) -> &str { self.head().uri.path() @@ -108,18 +108,22 @@ impl HttpRequest { /// The query string in the URL. /// - /// E.g., id=10 + /// Example: `id=10` #[inline] pub fn query_string(&self) -> &str { self.uri().query().unwrap_or_default() } - /// Get a reference to the Path parameters. + /// Returns a reference to the URL parameters container. /// - /// Params is a container for url parameters. - /// A variable segment is specified in the form `{identifier}`, - /// where the identifier can be used later in a request handler to - /// access the matched value for that segment. + /// A url parameter is specified in the form `{identifier}`, where the identifier can be used + /// later in a request handler to access the matched value for that parameter. + /// + /// # Percent Encoding and URL Parameters + /// Because each URL parameter is able to capture multiple path segments, both `["%2F", "%25"]` + /// found in the request URI are not decoded into `["/", "%"]` in order to preserve path + /// segment boundaries. If a url parameter is expected to contain these characters, then it is + /// on the user to decode them. #[inline] pub fn match_info(&self) -> &Path { &self.inner.path @@ -161,23 +165,29 @@ impl HttpRequest { self.head().extensions_mut() } - /// Generate url for named resource + /// Generates URL for a named resource. /// + /// This substitutes in sequence all URL parameters that appear in the resource itself and in + /// parent [scopes](crate::web::scope), if any. + /// + /// It is worth noting that the characters `['/', '%']` are not escaped and therefore a single + /// URL parameter may expand into multiple path segments and `elements` can be percent-encoded + /// beforehand without worrying about double encoding. Any other character that is not valid in + /// a URL path context is escaped using percent-encoding. + /// + /// # Examples /// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse}; - /// # /// fn index(req: HttpRequest) -> HttpResponse { - /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource + /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate URL for "foo" resource /// HttpResponse::Ok().into() /// } /// - /// fn main() { - /// let app = App::new() - /// .service(web::resource("/test/{one}/{two}/{three}") - /// .name("foo") // <- set resource name, then it could be used in `url_for` - /// .route(web::get().to(|| HttpResponse::Ok())) - /// ); - /// } + /// let app = App::new() + /// .service(web::resource("/test/{one}/{two}/{three}") + /// .name("foo") // <- set resource name so it can be used in `url_for` + /// .route(web::get().to(|| HttpResponse::Ok())) + /// ); /// ``` pub fn url_for(&self, name: &str, elements: U) -> Result where @@ -196,8 +206,8 @@ impl HttpRequest { self.url_for(name, &NO_PARAMS) } - #[inline] /// Get a reference to a `ResourceMap` of current application. + #[inline] pub fn resource_map(&self) -> &ResourceMap { self.app_state().rmap() } diff --git a/src/rmap.rs b/src/rmap.rs index 8466eda2..432eaf83 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -1,12 +1,14 @@ -use std::cell::RefCell; -use std::rc::{Rc, Weak}; +use std::{ + borrow::Cow, + cell::RefCell, + rc::{Rc, Weak}, +}; use actix_router::ResourceDef; use ahash::AHashMap; use url::Url; -use crate::error::UrlGenerationError; -use crate::request::HttpRequest; +use crate::{error::UrlGenerationError, request::HttpRequest}; #[derive(Clone, Debug)] pub struct ResourceMap { @@ -102,17 +104,28 @@ impl ResourceMap { }) .ok_or(UrlGenerationError::NotEnoughElements)?; - if path.starts_with('/') { + let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') { + // build full URL from connection info parts and resource path let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) + let base = format!("{}://{}", conn.scheme(), conn.host()); + (Cow::Owned(base), path.as_str()) } else { - Ok(Url::parse(&path)?) - } + // external resource; third slash would be the root slash in the path + let third_slash_index = path + .char_indices() + .filter_map(|(i, c)| (c == '/').then(|| i)) + .nth(2) + .unwrap_or_else(|| path.len()); + + ( + Cow::Borrowed(&path[..third_slash_index]), + &path[third_slash_index..], + ) + }; + + let mut url = Url::parse(&base)?; + url.set_path(path); + Ok(url) } pub fn has_resource(&self, path: &str) -> bool { @@ -406,6 +419,42 @@ mod tests { assert!(rmap.url_for(&req, "missing", &["u123"]).is_err()); } + #[test] + fn url_for_parser() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef_1 = ResourceDef::new("/{var}"); + rdef_1.set_name("internal"); + + let mut rdef_2 = ResourceDef::new("http://host.dom/{var}"); + rdef_2.set_name("external.1"); + + let mut rdef_3 = ResourceDef::new("{var}"); + rdef_3.set_name("external.2"); + + root.add(&mut rdef_1, None); + root.add(&mut rdef_2, None); + root.add(&mut rdef_3, None); + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"]; + const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag"; + + let url = rmap.url_for(&req, "internal", INPUT).unwrap(); + assert_eq!(url.path(), OUTPUT); + + let url = rmap.url_for(&req, "external.1", INPUT).unwrap(); + assert_eq!(url.path(), OUTPUT); + + assert!(rmap.url_for(&req, "external.2", INPUT).is_err()); + assert!(rmap.url_for(&req, "external.2", &[""]).is_err()); + } + #[test] fn external_resource_with_no_name() { let mut root = ResourceMap::new(ResourceDef::prefix(""));