From 498af3d9d0d2c47dbd1ae5579f64c4be362e1d10 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 20 Sep 2023 13:55:59 -0700 Subject: [PATCH 001/178] Improve chat example JavaScript code. Resolves #2617. --- examples/chat/static/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/chat/static/script.js b/examples/chat/static/script.js index d78e3e6a3f..99e0fef25e 100644 --- a/examples/chat/static/script.js +++ b/examples/chat/static/script.js @@ -94,7 +94,7 @@ function subscribe(uri) { console.log("raw data", JSON.stringify(ev.data)); console.log("decoded data", JSON.stringify(JSON.parse(ev.data))); const msg = JSON.parse(ev.data); - if (!"message" in msg || !"room" in msg || !"username" in msg) return; + if (!("message" in msg) || !("room" in msg) || !("username" in msg)) return; addMessage(msg.room, msg.username, msg.message, true); }); From 2cf38a5aa37fe046e46b03740835787f0396307b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 20 Sep 2023 15:33:51 -0700 Subject: [PATCH 002/178] Remove use of 'private_in_public' lint. The lint no longer exists. This gets rid of a compile-time warning for users. Resolves #2608. --- core/codegen/src/derive/from_form.rs | 5 ++++- core/codegen/tests/from_form.rs | 2 +- core/codegen/tests/uri_display.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 24db4a0a70..19c4cc0267 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -123,7 +123,9 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { Ok(quote_spanned! { input.span() => /// Rocket generated FormForm context. #[doc(hidden)] - #[allow(private_in_public)] + #[allow(unknown_lints)] + #[allow(renamed_and_removed_lints)] + #[allow(private_in_public, private_bounds)] #vis struct #ctxt_ty #impl_gen #where_clause { __opts: #_form::Options, __errors: #_form::Errors<'r>, @@ -148,6 +150,7 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { #[allow(unused_imports)] use #_http::uncased::AsUncased; }) + .outer_mapper(quote!(#[allow(renamed_and_removed_lints)])) .outer_mapper(quote!(#[allow(private_in_public)])) .outer_mapper(quote!(#[rocket::async_trait])) .inner_mapper(MapperBuild::new() diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index 814ab8814d..621ffa0481 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -984,7 +984,7 @@ fn json_wrapper_works() { assert_eq!(form, JsonToken(Json("foo bar"))); } -// FIXME: https://github.com/rust-lang/rust/issues/86706 +#[allow(renamed_and_removed_lints)] #[allow(private_in_public)] struct Q(T); diff --git a/core/codegen/tests/uri_display.rs b/core/codegen/tests/uri_display.rs index 9ffd14362a..cce97817ad 100644 --- a/core/codegen/tests/uri_display.rs +++ b/core/codegen/tests/uri_display.rs @@ -247,7 +247,7 @@ fn uri_display_serde() { assert_query_value_roundtrip!(JsonFoo, JsonFoo(Json(bam.clone()))); - // FIXME: https://github.com/rust-lang/rust/issues/86706 + #[allow(renamed_and_removed_lints)] #[allow(private_in_public)] #[derive(Debug, PartialEq, Clone, FromForm, UriDisplayQuery)] struct Q(Json); From 28de15858e9cdbc88f1a10e53cde8fd98e7eda3c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 20 Sep 2023 15:33:54 -0700 Subject: [PATCH 003/178] Avoid using 'macro' items on stable. This gets rid of the warning message on stable when building examples. --- core/codegen/Cargo.toml | 1 + core/codegen/src/bang/export.rs | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 900fc4ad58..13b59390a4 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -23,6 +23,7 @@ proc-macro2 = "1.0.27" devise = "0.4" rocket_http = { version = "=0.5.0-rc.3", path = "../http/" } unicode-xid = "0.2" +version_check = "0.9" glob = "0.3" [dev-dependencies] diff --git a/core/codegen/src/bang/export.rs b/core/codegen/src/bang/export.rs index 5004d17aa0..c62b5e5238 100644 --- a/core/codegen/src/bang/export.rs +++ b/core/codegen/src/bang/export.rs @@ -31,6 +31,23 @@ pub fn _macro(input: proc_macro::TokenStream) -> devise::Result { }) .collect(); + // Only try using the `macro` syntax on nightly/dev or when we don't know. + let export = match version_check::is_feature_flaggable() { + Some(true) | None => quote! { + #(#attrs)* + #[cfg(all(nightly, doc))] + pub macro #macro_name { + #decl_macro_tokens + } + + #[cfg(not(all(nightly, doc)))] + pub use #mod_name::#internal_name as #macro_name; + }, + Some(false) => quote! { + pub use #mod_name::#internal_name as #macro_name; + } + }; + Ok(quote! { #[allow(non_snake_case)] mod #mod_name { @@ -43,13 +60,6 @@ pub fn _macro(input: proc_macro::TokenStream) -> devise::Result { pub use #internal_name; } - #(#attrs)* - #[cfg(all(nightly, doc))] - pub macro #macro_name { - #decl_macro_tokens - } - - #[cfg(not(all(nightly, doc)))] - pub use #mod_name::#internal_name as #macro_name; + #export }) } From bbb124eeea13368d3ebfdba097e6a212484c80a6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 20 Sep 2023 16:15:57 -0700 Subject: [PATCH 004/178] Update UI tests for latest rustc. --- .../ui-fail-nightly/responder-types.stderr | 8 +- .../ui-fail-nightly/typed-uri-bad-type.stderr | 223 ++++++++++++++++-- .../tests/ui-fail-stable/from_form.stderr | 6 + 3 files changed, 211 insertions(+), 26 deletions(-) diff --git a/core/codegen/tests/ui-fail-nightly/responder-types.stderr b/core/codegen/tests/ui-fail-nightly/responder-types.stderr index fd26ecfebe..58956ef88f 100644 --- a/core/codegen/tests/ui-fail-nightly/responder-types.stderr +++ b/core/codegen/tests/ui-fail-nightly/responder-types.stderr @@ -1,8 +1,8 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied - --> tests/ui-fail-nightly/responder-types.rs:5:5 + --> tests/ui-fail-nightly/responder-types.rs:5:12 | 5 | thing: u8, - | ^^^^^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` + | ^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: > @@ -39,10 +39,10 @@ note: required by a bound in `rocket::Response::<'r>::set_header` | ^^^^^^^^^^^^^^^^ required by this bound in `Response::<'r>::set_header` error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied - --> tests/ui-fail-nightly/responder-types.rs:16:5 + --> tests/ui-fail-nightly/responder-types.rs:16:12 | 16 | thing: u8, - | ^^^^^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` + | ^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: > diff --git a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr index 2e47e9046f..c54e50fd96 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr @@ -11,10 +11,10 @@ error[E0271]: type mismatch resolving `>::Error == &str` | ^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Empty` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:45:22 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:45:5 | 45 | uri!(simple(id = "hi")); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -22,10 +22,10 @@ error[E0277]: the trait bound `usize: FromUriParam> error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:47:17 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:47:5 | 47 | uri!(simple("hello")); - | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -33,16 +33,33 @@ error[E0277]: the trait bound `usize: FromUriParam> error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:49:22 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:49:5 | 49 | uri!(simple(id = 239239i64)); - | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > > > +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:51:5 + | +51 | uri!(not_uri_display(10, S)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `S` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + > + > + > + > + > + and $N others + error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:51:30 | @@ -61,10 +78,10 @@ error[E0277]: the trait bound `S: FromUriParam` and $N others error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:56:25 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:56:5 | 56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); - | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following other types implement trait `FromUriParam`: > @@ -73,10 +90,10 @@ error[E0277]: the trait bound `i32: FromUriParam` to implement `FromUriParam>` error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:56:42 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:56:5 | 56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); - | ^^^^^^^^^^^^^^^^ the trait `FromUriParam>` is not implemented for `std::string::String` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam>` is not implemented for `std::string::String` | = help: the following other types implement trait `FromUriParam`: > @@ -88,10 +105,10 @@ error[E0277]: the trait bound `std::string::String: FromUriParam` to implement `FromUriParam>` error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:58:19 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:58:5 | 58 | uri!(simple_q("hi")); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following other types implement trait `FromUriParam`: > @@ -99,15 +116,32 @@ error[E0277]: the trait bound `isize: FromUriParam> error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:60:24 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:60:5 | 60 | uri!(simple_q(id = "hi")); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `isize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:62:5 + | +62 | uri!(other_q(100, S)); + | ^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: + > + > + > > > > + > + > + and $N others error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:62:23 @@ -126,6 +160,23 @@ error[E0277]: the trait bound `S: FromUriParam > and $N others +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:64:5 + | +64 | uri!(other_q(rest = S, id = 100)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `S` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + > + > + > + > + > + and $N others + error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:64:25 | @@ -173,6 +224,23 @@ note: required by a bound in `assert_ignorable` | pub fn assert_ignorable>() { } | ^^^^^^^^^^^^ required by this bound in `assert_ignorable` +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:68:5 + | +68 | uri!(other_q(rest = S, id = _)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `S` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + > + > + > + > + > + and $N others + error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:68:25 | @@ -191,10 +259,10 @@ error[E0277]: the trait bound `S: FromUriParam and $N others error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:77:40 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:77:5 | 77 | uri!(uri!("?foo#bar"), simple(id = "hi")); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -220,10 +288,10 @@ note: required by a bound in `RouteUriBuilder::with_prefix` | ^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_prefix` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:78:33 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:78:5 | 78 | uri!(uri!("*"), simple(id = "hi")); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -249,10 +317,10 @@ note: required by a bound in `RouteUriBuilder::with_prefix` | ^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_prefix` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:81:25 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:81:5 | 81 | uri!(_, simple(id = "hi"), uri!("*")); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -282,10 +350,10 @@ note: required by a bound in `RouteUriBuilder::with_suffix` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> tests/ui-fail-nightly/typed-uri-bad-type.rs:82:25 + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:82:5 | 82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: > @@ -313,3 +381,114 @@ note: required by a bound in `RouteUriBuilder::with_suffix` | ----------- required by a bound in this associated function | where S: ValidRouteSuffix> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:45:22 + | +45 | uri!(simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:47:17 + | +47 | uri!(simple("hello")); + | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:49:22 + | +49 | uri!(simple(id = 239239i64)); + | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:56:25 + | +56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); + | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + = note: required for `std::option::Option` to implement `FromUriParam>` + +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:58:19 + | +58 | uri!(simple_q("hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:60:24 + | +60 | uri!(simple_q(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:77:40 + | +77 | uri!(uri!("?foo#bar"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:78:33 + | +78 | uri!(uri!("*"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:81:25 + | +81 | uri!(_, simple(id = "hi"), uri!("*")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> tests/ui-fail-nightly/typed-uri-bad-type.rs:82:25 + | +82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following other types implement trait `FromUriParam`: + > + > + > diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index afd9e509c6..3bc006df65 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -523,6 +523,9 @@ help: the type constructed contains `{integer}` due to the type of the argument | ^^^ this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs + | + | Some(#[stable(feature = "rust1", since = "1.0.0")] T), + | ^^^^ error[E0308]: mismatched types --> tests/ui-fail-stable/from_form.rs:203:33 @@ -542,6 +545,9 @@ help: the type constructed contains `&'static str` due to the type of the argume | this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs + | + | Some(#[stable(feature = "rust1", since = "1.0.0")] T), + | ^^^^ error[E0277]: the trait bound `bool: From<&str>` is not satisfied --> tests/ui-fail-stable/from_form.rs:209:23 From f41474dd6136db0fe744907e06f8b38136259066 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 20 Sep 2023 17:17:22 -0700 Subject: [PATCH 005/178] Fix broken links between codegen and http crates. Also resolves link warnings emitted by rustdoc. Closes #2594 --- core/codegen/src/lib.rs | 1 + core/http/src/uri/fmt/from_uri_param.rs | 2 +- core/http/src/uri/fmt/uri_display.rs | 2 +- core/lib/src/response/mod.rs | 5 ++--- core/lib/src/response/stream/sse.rs | 6 +++--- core/lib/src/serde/json.rs | 2 +- core/lib/src/serde/msgpack.rs | 2 +- scripts/mk-docs.sh | 11 +++++++---- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 08b06dea13..7cec0eb073 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1457,6 +1457,7 @@ pub fn catchers(input: TokenStream) -> TokenStream { /// are not ignorable. /// /// [`Uri`]: ../rocket/http/uri/enum.Uri.html +/// [`Uri::parse_any()`]: ../rocket/http/uri/enum.Uri.html#method.parse_any /// [`Origin`]: ../rocket/http/uri/struct.Origin.html /// [`Asterisk`]: ../rocket/http/uri/struct.Asterisk.html /// [`Authority`]: ../rocket/http/uri/struct.Authority.html diff --git a/core/http/src/uri/fmt/from_uri_param.rs b/core/http/src/uri/fmt/from_uri_param.rs index 0850b905cc..ae162e9bd2 100644 --- a/core/http/src/uri/fmt/from_uri_param.rs +++ b/core/http/src/uri/fmt/from_uri_param.rs @@ -178,7 +178,7 @@ use crate::uri::fmt::{self, Part}; /// assert_eq!(uri.query().unwrap(), "name=Robert%20Mike&nickname=Bob"); /// ``` /// -/// [`uri!`]: rocket::uri +/// [`uri!`]: ../../../../rocket/macro.uri.html /// [`FromUriParam::Target`]: crate::uri::fmt::FromUriParam::Target /// [`Path`]: crate::uri::fmt::Path /// [`Query`]: crate::uri::fmt::Query diff --git a/core/http/src/uri/fmt/uri_display.rs b/core/http/src/uri/fmt/uri_display.rs index fb8902ff02..b8e630e09a 100644 --- a/core/http/src/uri/fmt/uri_display.rs +++ b/core/http/src/uri/fmt/uri_display.rs @@ -106,7 +106,7 @@ use crate::uri::fmt::{Part, Path, Query, Formatter}; /// seen, the implementations will be used to display the value in a URI-safe /// manner. /// -/// [`uri!`]: rocket::uri +/// [`uri!`]: ../../../../rocket/macro.uri.html /// /// # Provided Implementations /// diff --git a/core/lib/src/response/mod.rs b/core/lib/src/response/mod.rs index 22b26eb685..71f0ff6980 100644 --- a/core/lib/src/response/mod.rs +++ b/core/lib/src/response/mod.rs @@ -1,9 +1,8 @@ //! Types and traits to build and send responses. //! //! The return type of a Rocket handler can be any type that implements the -//! [`Responder`](crate::response::Responder) trait, which means that the type knows -//! how to generate a [`Response`]. Among other things, this module contains -//! several such types. +//! [`Responder`] trait, which means that the type knows how to generate a +//! [`Response`]. Among other things, this module contains several such types. //! //! # Composing //! diff --git a/core/lib/src/response/stream/sse.rs b/core/lib/src/response/stream/sse.rs index 3177497221..b123f632d6 100644 --- a/core/lib/src/response/stream/sse.rs +++ b/core/lib/src/response/stream/sse.rs @@ -351,9 +351,9 @@ impl Event { /// # Responder /// /// `EventStream` is a (potentially infinite) responder. The response -/// `Content-Type` is set to [`EventStream`](ContentType::EventStream). The body -/// is [unsized](crate::response::Body#unsized), and values are sent as soon as -/// they are yielded by the internal iterator. +/// `Content-Type` is set to [`EventStream`](const@ContentType::EventStream). +/// The body is [unsized](crate::response::Body#unsized), and values are sent as +/// soon as they are yielded by the internal iterator. /// /// ## Heartbeat /// diff --git a/core/lib/src/serde/json.rs b/core/lib/src/serde/json.rs index 3f8ffe5f23..dfc85a6ba1 100644 --- a/core/lib/src/serde/json.rs +++ b/core/lib/src/serde/json.rs @@ -1,6 +1,6 @@ //! Automatic JSON (de)serialization support. //! -//! See [`Json`](Json) for details. +//! See [`Json`] for details. //! //! # Enabling //! diff --git a/core/lib/src/serde/msgpack.rs b/core/lib/src/serde/msgpack.rs index bb9867e313..e77c6aa76f 100644 --- a/core/lib/src/serde/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -1,6 +1,6 @@ //! Automatic MessagePack (de)serialization support. //! -//! See [`MsgPack`](crate::serde::msgpack::MsgPack) for further details. +//! See [`MsgPack`] for further details. //! //! # Enabling //! diff --git a/scripts/mk-docs.sh b/scripts/mk-docs.sh index 93659d091d..ed81362540 100755 --- a/scripts/mk-docs.sh +++ b/scripts/mk-docs.sh @@ -20,10 +20,13 @@ fi echo ":::: Generating the docs..." pushd "${PROJECT_ROOT}" > /dev/null 2>&1 # Set the crate version and fill in missing doc URLs with docs.rs links. - RUSTDOCFLAGS="-Zunstable-options --crate-version ${DOC_VERSION}" \ - cargo doc -p rocket \ - -p rocket_sync_db_pools -p rocket_dyn_templates -p rocket_db_pools -p rocket_ws \ - -Zrustdoc-map --no-deps --all-features + RUSTDOCFLAGS="-Zunstable-options --crate-version ${DOC_VERSION} --extern-html-root-url rocket=https://api.rocket.rs/rocket/" \ + cargo doc -Zrustdoc-map --no-deps --all-features \ + -p rocket \ + -p rocket_db_pools \ + -p rocket_sync_db_pools \ + -p rocket_dyn_templates \ + -p rocket_ws popd > /dev/null 2>&1 # Blank index, for redirection. From 5d31ad4efb9e86c113ec4385a92392eef298d27f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 28 Sep 2023 23:50:29 -0700 Subject: [PATCH 006/178] Update 'cookie' to 0.18. --- core/codegen/src/bang/uri_parsing.rs | 2 +- core/http/Cargo.toml | 2 +- core/lib/src/cookies.rs | 73 +++++++++++-------- core/lib/src/local/asynchronous/request.rs | 4 +- core/lib/src/local/blocking/request.rs | 2 +- core/lib/src/local/request.rs | 26 ++++--- core/lib/src/request/request.rs | 4 +- core/lib/src/response/flash.rs | 6 +- core/lib/tests/catcher-cookies-1213.rs | 8 +- core/lib/tests/cookies-private.rs | 14 ++-- .../local_request_private_cookie-issue-368.rs | 6 +- core/lib/tests/many-cookie-jars-at-once.rs | 10 +-- core/lib/tests/session-cookies-issue-1506.rs | 3 +- core/lib/tests/untracked-vs-tracked.rs | 4 +- examples/cookies/src/message.rs | 9 +-- examples/cookies/src/session.rs | 4 +- examples/tls/src/tests.rs | 8 +- site/guide/4-requests.md | 2 +- 18 files changed, 99 insertions(+), 88 deletions(-) diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index ea3665a824..efbe41a2d3 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -430,7 +430,7 @@ impl Arg { fn unnamed(&self) -> &ArgExpr { match self { Arg::Unnamed(expr) => expr, - _ => panic!("Called Arg::unnamed() on an Arg::named!"), + _ => panic!("Called Arg::unnamed() on an Arg::Named!"), } } diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 2aa700f01c..41fd3813b8 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -42,7 +42,7 @@ pear = "0.2.3" pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" -cookie = { version = "0.17.0", features = ["percent-encode"] } +cookie = { version = "=0.18.0-rc.0", features = ["percent-encode"] } state = "0.6" futures = { version = "0.3", default-features = false } diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 62515c055b..3e7df583e1 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -28,19 +28,19 @@ pub use self::cookie::{Cookie, SameSite, Iter}; /// /// #[get("/message")] /// fn message(jar: &CookieJar<'_>) { -/// jar.add(Cookie::new("message", "hello!")); -/// jar.add(Cookie::new("other", "bye!")); +/// jar.add(("message", "hello!")); +/// jar.add(Cookie::build(("session", "bye!")).expires(None)); /// /// // `get()` does not reflect changes. -/// assert!(jar.get("other").is_none()); -/// # assert_eq!(jar.get("message").map(|c| c.value()), Some("hi")); +/// assert!(jar.get("session").is_none()); +/// assert_eq!(jar.get("message").map(|c| c.value()), Some("hi")); /// /// // `get_pending()` does. -/// let other_pending = jar.get_pending("other"); +/// let session_pending = jar.get_pending("session"); /// let message_pending = jar.get_pending("message"); -/// assert_eq!(other_pending.as_ref().map(|c| c.value()), Some("bye!")); +/// assert_eq!(session_pending.as_ref().map(|c| c.value()), Some("bye!")); /// assert_eq!(message_pending.as_ref().map(|c| c.value()), Some("hello!")); -/// # jar.remove(Cookie::named("message")); +/// # jar.remove("message"); /// # assert_eq!(jar.get("message").map(|c| c.value()), Some("hi")); /// # assert!(jar.get_pending("message").is_none()); /// } @@ -48,7 +48,7 @@ pub use self::cookie::{Cookie, SameSite, Iter}; /// # use rocket::local::blocking::Client; /// # let client = Client::debug_with(routes![message]).unwrap(); /// # let response = client.get("/message") -/// # .cookie(Cookie::new("message", "hi")) +/// # .cookie(("message", "hi")) /// # .dispatch(); /// # /// # assert!(response.status().class().is_success()); @@ -202,7 +202,7 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { @@ -226,7 +226,7 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { @@ -252,7 +252,7 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { @@ -297,17 +297,18 @@ impl<'a> CookieJar<'a> { /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { - /// jar.add(Cookie::new("first", "value")); + /// jar.add(("first", "value")); /// - /// let cookie = Cookie::build("other", "value_two") + /// let cookie = Cookie::build(("other", "value_two")) /// .path("/") /// .secure(true) /// .same_site(SameSite::Lax); /// - /// jar.add(cookie.finish()); + /// jar.add(cookie); /// } /// ``` - pub fn add(&self, mut cookie: Cookie<'static>) { + pub fn add>>(&self, cookie: C) { + let mut cookie = cookie.into(); Self::set_defaults(self.config, &mut cookie); self.ops.lock().push(Op::Add(cookie, false)); } @@ -334,30 +335,32 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { - /// jar.add_private(Cookie::new("name", "value")); + /// jar.add_private(("name", "value")); /// } /// ``` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] - pub fn add_private(&self, mut cookie: Cookie<'static>) { + pub fn add_private>>(&self, cookie: C) { + let mut cookie = cookie.into(); Self::set_private_defaults(self.config, &mut cookie); self.ops.lock().push(Op::Add(cookie, true)); } /// Removes `cookie` from this collection and generates a "removal" cookies - /// to send to the client on response. For correctness, `cookie` must - /// contain the same `path` and `domain` as the cookie that was initially - /// set. Failure to provide the initial `path` and `domain` will result in - /// cookies that are not properly removed. For convenience, if a path is not - /// set on `cookie`, the `"/"` path will automatically be set. + /// to send to the client on response. A "removal" cookie is a cookie that + /// has the same name as the original cookie but has an empty value, a + /// max-age of 0, and an expiration date far in the past. + /// + /// **Note: For correctness, `cookie` must contain the same `path` and + /// `domain` as the cookie that was initially set. Failure to provide the + /// initial `path` and `domain` will result in cookies that are not properly + /// removed. For convenience, if a path is not set on `cookie`, the `"/"` + /// path will automatically be set.** /// - /// A "removal" cookie is a cookie that has the same name as the original - /// cookie but has an empty value, a max-age of 0, and an expiration date - /// far in the past. /// /// # Example /// @@ -367,10 +370,15 @@ impl<'a> CookieJar<'a> { /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { - /// jar.remove(Cookie::named("name")); + /// // Rocket will set `path` to `/`. + /// jar.remove("name"); + /// + /// // Use a custom-built cookie to set a custom path. + /// jar.remove(Cookie::build("name").path("/login")); /// } /// ``` - pub fn remove(&self, mut cookie: Cookie<'static>) { + pub fn remove>>(&self, cookie: C) { + let mut cookie = cookie.into(); if cookie.path().is_none() { cookie.set_path("/"); } @@ -388,16 +396,17 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { - /// jar.remove_private(Cookie::named("name")); + /// jar.remove_private("name"); /// } /// ``` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] - pub fn remove_private(&self, mut cookie: Cookie<'static>) { + pub fn remove_private>>(&self, cookie: C) { + let mut cookie = cookie.into(); if cookie.path().is_none() { cookie.set_path("/"); } @@ -415,7 +424,7 @@ impl<'a> CookieJar<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::{Cookie, CookieJar}; + /// use rocket::http::CookieJar; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index 994472536a..76ed3f3707 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -24,7 +24,7 @@ use super::{Client, LocalResponse}; /// let req = client.post("/") /// .header(ContentType::JSON) /// .remote("127.0.0.1:8000".parse().unwrap()) -/// .cookie(Cookie::new("name", "value")) +/// .cookie(("name", "value")) /// .body(r#"{ "value": 42 }"#); /// /// let response = req.dispatch().await; @@ -106,7 +106,7 @@ impl<'c> LocalRequest<'c> { for cookie in response.cookies().iter() { if let Some(expires) = cookie.expires_datetime() { if expires <= current_time { - jar.force_remove(cookie); + jar.force_remove(cookie.name()); continue; } } diff --git a/core/lib/src/local/blocking/request.rs b/core/lib/src/local/blocking/request.rs index db2f494494..f094c60e44 100644 --- a/core/lib/src/local/blocking/request.rs +++ b/core/lib/src/local/blocking/request.rs @@ -22,7 +22,7 @@ use super::{Client, LocalResponse}; /// let req = client.post("/") /// .header(ContentType::JSON) /// .remote("127.0.0.1:8000".parse().unwrap()) -/// .cookie(Cookie::new("name", "value")) +/// .cookie(("name", "value")) /// .body(r#"{ "value": 42 }"#); /// /// let response = req.dispatch(); diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index 4ba38634c8..a04ff5d7ba 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -131,13 +131,15 @@ macro_rules! pub_request_impl { /// # Client::_test(|_, request, _| { /// let request: LocalRequest = request; /// let req = request - /// .cookie(Cookie::new("username", "sb")) - /// .cookie(Cookie::new("user_id", "12")); + /// .cookie(("username", "sb")) + /// .cookie(("user_id", "12")); /// # }); /// ``` #[inline] - pub fn cookie(mut self, cookie: crate::http::Cookie<'_>) -> Self { - self._request_mut().cookies_mut().add_original(cookie.into_owned()); + pub fn cookie<'a, C>(mut self, cookie: C) -> Self + where C: Into> + { + self._request_mut().cookies_mut().add_original(cookie.into().into_owned()); self } @@ -151,15 +153,17 @@ macro_rules! pub_request_impl { /// /// # Client::_test(|_, request, _| { /// let request: LocalRequest = request; - /// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")]; + /// let cookies = vec![("a", "b"), ("c", "d")]; /// let req = request.cookies(cookies); /// # }); /// ``` #[inline] - pub fn cookies<'a, C>(mut self, cookies: C) -> Self - where C: IntoIterator> + pub fn cookies<'a, C, I>(mut self, cookies: I) -> Self + where C: Into>, + I: IntoIterator { for cookie in cookies { + let cookie: crate::http::Cookie<'_> = cookie.into(); self._request_mut().cookies_mut().add_original(cookie.into_owned()); } @@ -180,14 +184,16 @@ macro_rules! pub_request_impl { /// /// # Client::_test(|_, request, _| { /// let request: LocalRequest = request; - /// let req = request.private_cookie(Cookie::new("user_id", "sb")); + /// let req = request.private_cookie(("user_id", "sb")); /// # }); /// ``` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[inline] - pub fn private_cookie(mut self, cookie: crate::http::Cookie<'static>) -> Self { - self._request_mut().cookies_mut().add_original_private(cookie); + pub fn private_cookie(mut self, cookie: C) -> Self + where C: Into> + { + self._request_mut().cookies_mut().add_original_private(cookie.into()); self } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 62a2bb1e62..7f7e50e7eb 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -446,8 +446,8 @@ impl<'r> Request<'r> { /// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap(); /// # let request = c.get("/"); /// # let req = request.inner(); - /// req.cookies().add(Cookie::new("key", "val")); - /// req.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4))); + /// req.cookies().add(("key", "val")); + /// req.cookies().add(("ans", format!("life: {}", 38 + 4))); /// /// assert_eq!(req.cookies().get_pending("key").unwrap().value(), "val"); /// assert_eq!(req.cookies().get_pending("ans").unwrap().value(), "life: 42"); diff --git a/core/lib/src/response/flash.rs b/core/lib/src/response/flash.rs index dc66545801..187223ab4f 100644 --- a/core/lib/src/response/flash.rs +++ b/core/lib/src/response/flash.rs @@ -178,9 +178,9 @@ impl Flash { let content = format!("{}{}{}{}", self.kind.len(), FLASH_COOKIE_DELIM, self.kind, self.message); - Cookie::build(FLASH_COOKIE_NAME, content) + Cookie::build((FLASH_COOKIE_NAME, content)) .max_age(Duration::minutes(5)) - .finish() + .build() } } @@ -211,7 +211,7 @@ impl<'r> FlashMessage<'r> { fn clear_cookie_if_needed(&self) { // Remove the cookie if it hasn't already been removed. if !self.consumed.swap(true, Ordering::Relaxed) { - self.inner.remove(Cookie::named(FLASH_COOKIE_NAME)); + self.inner.remove(FLASH_COOKIE_NAME); } } diff --git a/core/lib/tests/catcher-cookies-1213.rs b/core/lib/tests/catcher-cookies-1213.rs index d8d09f1730..0b7e81d474 100644 --- a/core/lib/tests/catcher-cookies-1213.rs +++ b/core/lib/tests/catcher-cookies-1213.rs @@ -1,17 +1,17 @@ #[macro_use] extern crate rocket; use rocket::request::Request; -use rocket::http::{Cookie, CookieJar}; +use rocket::http::CookieJar; #[catch(404)] fn not_found(request: &Request) -> &'static str { - request.cookies().add(Cookie::new("not_found", "404")); + request.cookies().add(("not_found", "404")); "404 - Not Found" } #[get("/")] fn index(cookies: &CookieJar<'_>) -> &'static str { - cookies.add(Cookie::new("index", "hi")); + cookies.add(("index", "hi")); "Hello, world!" } @@ -26,7 +26,7 @@ mod tests { .mount("/", routes![index]) .register("/", catchers![not_found]) .attach(AdHoc::on_request("Add Cookie", |req, _| Box::pin(async move { - req.cookies().add(Cookie::new("fairing", "woo")); + req.cookies().add(("fairing", "woo")); }))); let client = Client::debug(rocket).unwrap(); diff --git a/core/lib/tests/cookies-private.rs b/core/lib/tests/cookies-private.rs index e499ed34e8..c6e11a247b 100644 --- a/core/lib/tests/cookies-private.rs +++ b/core/lib/tests/cookies-private.rs @@ -10,7 +10,7 @@ fn cookie_add_private(jar: &CookieJar<'_>) { jar.add(cookie_a.clone()); let mut cookie_b = Cookie::new("b", "v2"); jar.add_private(cookie_b.clone()); - jar.add(Cookie::new("c", "v3")); + jar.add(("c", "v3")); // private: CookieJar::set_defaults(&mut cookie_a); cookie_a.set_path("/"); @@ -89,9 +89,9 @@ mod cookies_private_tests { let client = Client::debug(rocket()).unwrap(); let response = client .get("/") - .cookie(Cookie::new("a", "Cookie")) - .private_cookie(Cookie::new("b", " tastes ")) - .cookie(Cookie::new("c", "good!")) + .cookie(("a", "Cookie")) + .private_cookie(("b", " tastes ")) + .cookie(("c", "good!")) .dispatch(); assert_eq!(response.into_string().unwrap(), "Cookie tastes good!"); @@ -103,9 +103,9 @@ mod cookies_private_tests { let client = Client::debug(rocket()).unwrap(); let response = client .get("/oh-no") - .cookie(Cookie::new("a", "Cookie")) - .private_cookie(Cookie::new("b", " tastes ")) - .cookie(Cookie::new("c", "good!")) + .cookie(("a", "Cookie")) + .private_cookie(("b", " tastes ")) + .cookie(("c", "good!")) .dispatch(); assert_ne!(response.into_string().unwrap(), "Cookie tastes good!"); diff --git a/core/lib/tests/local_request_private_cookie-issue-368.rs b/core/lib/tests/local_request_private_cookie-issue-368.rs index 2a516f2ad6..34b98ce9db 100644 --- a/core/lib/tests/local_request_private_cookie-issue-368.rs +++ b/core/lib/tests/local_request_private_cookie-issue-368.rs @@ -14,14 +14,14 @@ mod tests { use super::*; use rocket::routes; use rocket::local::blocking::Client; - use rocket::http::{Cookie, Status}; + use rocket::http::Status; #[test] fn private_cookie_is_returned() { let rocket = rocket::build().mount("/", routes![return_private_cookie]); let client = Client::debug(rocket).unwrap(); - let req = client.get("/").private_cookie(Cookie::new("cookie_name", "cookie_value")); + let req = client.get("/").private_cookie(("cookie_name", "cookie_value")); let response = req.dispatch(); assert_eq!(response.headers().get_one("Set-Cookie"), None); @@ -33,7 +33,7 @@ mod tests { let rocket = rocket::build().mount("/", routes![return_private_cookie]); let client = Client::debug(rocket).unwrap(); - let req = client.get("/").cookie(Cookie::new("cookie_name", "cookie_value")); + let req = client.get("/").cookie(("cookie_name", "cookie_value")); let response = req.dispatch(); assert_eq!(response.status(), Status::NotFound); diff --git a/core/lib/tests/many-cookie-jars-at-once.rs b/core/lib/tests/many-cookie-jars-at-once.rs index 5e8a9ff4a5..781daffacf 100644 --- a/core/lib/tests/many-cookie-jars-at-once.rs +++ b/core/lib/tests/many-cookie-jars-at-once.rs @@ -1,11 +1,11 @@ #[macro_use] extern crate rocket; -use rocket::http::{Cookie, CookieJar}; +use rocket::http::CookieJar; #[post("/")] fn multi_add(jar_a: &CookieJar<'_>, jar_b: &CookieJar<'_>) { - jar_a.add(Cookie::new("a", "v1")); - jar_b.add(Cookie::new("b", "v2")); + jar_a.add(("a", "v1")); + jar_b.add(("b", "v2")); } #[get("/")] @@ -41,8 +41,8 @@ mod many_cookie_jars_tests { fn test_multi_get() { let client = Client::debug(rocket()).unwrap(); let response = client.get("/") - .cookie(Cookie::new("a", "a_val")) - .cookie(Cookie::new("b", "hi!")) + .cookie(("a", "a_val")) + .cookie(("b", "hi!")) .dispatch(); assert_eq!(response.into_string().unwrap(), "a_valhi!"); diff --git a/core/lib/tests/session-cookies-issue-1506.rs b/core/lib/tests/session-cookies-issue-1506.rs index 5166214079..7ae72526c7 100644 --- a/core/lib/tests/session-cookies-issue-1506.rs +++ b/core/lib/tests/session-cookies-issue-1506.rs @@ -4,8 +4,7 @@ use rocket::http::{CookieJar, Cookie}; #[rocket::get("/")] fn index(jar: &CookieJar<'_>) { - let session_cookie = Cookie::build("key", "value").expires(None); - jar.add_private(session_cookie.finish()); + jar.add_private(Cookie::build(("key", "value")).expires(None)); } mod test_session_cookies { diff --git a/core/lib/tests/untracked-vs-tracked.rs b/core/lib/tests/untracked-vs-tracked.rs index c9df4f24ef..343fb3ee11 100644 --- a/core/lib/tests/untracked-vs-tracked.rs +++ b/core/lib/tests/untracked-vs-tracked.rs @@ -1,10 +1,10 @@ #[macro_use] extern crate rocket; -use rocket::http::{Cookie, CookieJar}; +use rocket::http::CookieJar; #[post("/")] fn add(jar: &CookieJar<'_>) { - jar.add(Cookie::new("name", "value")); + jar.add(("name", "value")); } #[get("/")] diff --git a/examples/cookies/src/message.rs b/examples/cookies/src/message.rs index aa1fb0d353..d4896adbf6 100644 --- a/examples/cookies/src/message.rs +++ b/examples/cookies/src/message.rs @@ -12,17 +12,14 @@ pub use message_uri as uri; #[post("/", data = "")] fn submit(cookies: &CookieJar<'_>, message: Form<&str>) -> Redirect { - cookies.add(Cookie::new("message", message.to_string())); + cookies.add(("message", message.to_string())); Redirect::to(uri!(index)) } #[get("/")] fn index(cookies: &CookieJar<'_>) -> Template { - let cookie = cookies.get("message"); - - Template::render("message", context! { - message: cookie.map(|c| c.value()), - }) + let message = cookies.get("message").map(|c| c.value()); + Template::render("message", context! { message }) } pub fn routes() -> Vec { diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index c4309a4283..d66a5911aa 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -60,7 +60,7 @@ fn login_page(flash: Option>) -> Template { #[post("/login", data = "")] fn post_login(jar: &CookieJar<'_>, login: Form>) -> Result> { if login.username == "Sergio" && login.password == "password" { - jar.add_private(Cookie::new("user_id", 1.to_string())); + jar.add_private(("user_id", "1")); Ok(Redirect::to(uri!(index))) } else { Err(Flash::error(Redirect::to(uri!(login_page)), "Invalid username/password.")) @@ -69,7 +69,7 @@ fn post_login(jar: &CookieJar<'_>, login: Form>) -> Result) -> Flash { - jar.remove_private(Cookie::named("user_id")); + jar.remove_private("user_id"); Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") } diff --git a/examples/tls/src/tests.rs b/examples/tls/src/tests.rs index 2f48af0274..2629e3c487 100644 --- a/examples/tls/src/tests.rs +++ b/examples/tls/src/tests.rs @@ -27,11 +27,11 @@ fn secure_cookies() { #[get("/cookie")] fn cookie(jar: &CookieJar<'_>) { - jar.add(Cookie::new("k1", "v1")); - jar.add_private(Cookie::new("k2", "v2")); + jar.add(("k1", "v1")); + jar.add_private(("k2", "v2")); - jar.add(Cookie::build("k1u", "v1u").secure(false).finish()); - jar.add_private(Cookie::build("k2u", "v2u").secure(false).finish()); + jar.add(Cookie::build(("k1u", "v1u")).secure(false)); + jar.add_private(Cookie::build(("k2u", "v2u")).secure(false)); } let client = Client::tracked(super::rocket().mount("/", routes![cookie])).unwrap(); diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 4fe8fee5b7..1a1a8c637e 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -631,7 +631,7 @@ fn user_id(cookies: &CookieJar<'_>) -> Option { /// Remove the `user_id` cookie. #[post("/logout")] fn logout(cookies: &CookieJar<'_>) -> Flash { - cookies.remove_private(Cookie::named("user_id")); + cookies.remove_private("user_id"); Flash::success(Redirect::to("/"), "Successfully logged out.") } ``` From 47faac6080b5ce9d6ec33a06ccb6a914b8054966 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 2 Oct 2023 11:59:03 -0700 Subject: [PATCH 007/178] Document that '&[u8]' is a form guard. --- core/lib/src/form/from_form.rs | 4 +++- core/lib/src/form/from_form_field.rs | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index 726ca208f6..f44be8cf99 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -120,7 +120,7 @@ use crate::http::uncased::AsUncased; /// | IP Address | _inherit_ | **no default** | No | Yes | [`IpAddr`], [`Ipv4Addr`], [`Ipv6Addr`] | /// | Socket Address | _inherit_ | **no default** | No | Yes | [`SocketAddr`], [`SocketAddrV4`], [`SocketAddrV6`] | /// | [`TempFile`] | _inherit_ | **no default** | Yes | Yes | Data limits apply. See [`TempFile`]. | -/// | [`Capped`] | _inherit_ | **no default** | Yes | Yes | `C` is `&str`, `String`, or `TempFile`. | +/// | [`Capped`] | _inherit_ | **no default** | Yes | Yes | `C` is `&str`, `String`, `&[u8]` or `TempFile`. | /// | [`time::Date`] | _inherit_ | **no default** | No | Yes | `%F` (`YYYY-MM-DD`). HTML "date" input. | /// | [`time::DateTime`] | _inherit_ | **no default** | No | Yes | `%FT%R` or `%FT%T` (`YYYY-MM-DDTHH:MM[:SS]`) | /// | [`time::Time`] | _inherit_ | **no default** | No | Yes | `%R` or `%T` (`HH:MM[:SS]`) | @@ -628,6 +628,8 @@ impl<'v, T: FromForm<'v> + 'v> FromForm<'v> for Vec { } } +// impl_strict_from_form_field_from_capped!(Vec); + #[doc(hidden)] pub struct MapContext<'v, K, V> where K: FromForm<'v>, V: FromForm<'v> { opts: Options, diff --git a/core/lib/src/form/from_form_field.rs b/core/lib/src/form/from_form_field.rs index 9035ad38ae..d03ee38693 100644 --- a/core/lib/src/form/from_form_field.rs +++ b/core/lib/src/form/from_form_field.rs @@ -28,11 +28,32 @@ use crate::form::prelude::*; /// } /// ``` /// +/// # Semantics +/// +/// The implementation of `FromForm` for a `T: FromFormField` type operates as +/// follows: +/// +/// * When parsing is **strict**, the parser accepts the _first_ value or data +/// field with the corresponding field name and calls `T::from_value()` or +/// `T::from_data()` with the field's value, respectively. If more than one +/// field value is seen, an [`ErrorKind::Duplicate`) is emitted. If no +/// matching field is seen, an [`ErrorKind::Missing`] is emitted. Otherwise, +/// the result from the call is emitted. +/// +/// * When parsing is **lenient**, the parser accepts the first _expected_ +/// value or data field with the corresponding field name and calls +/// `T::from_value()` or `T::from_data()` with the field's value, +/// respectively. Unexpected values, identified by returning an +/// [`ErrorKind::Unexpected`] from `from_value()` or `from_data()` are +/// ignored. Any additional fields with a matching field name are ignored. +/// If no matching field is seen and `T` has a default, it is used, +/// otherwise an [`ErrorKind::Missing`] is emitted. +/// /// # Deriving /// /// `FromFormField` can be derived for C-like enums, where the generated /// implementation case-insensitively parses fields with values equal to the -/// name of the variant or the value in `field(value = "...")`. +/// name of the variant or the value in `field()`. /// /// ```rust /// # use rocket::form::FromFormField; From ae68742048e96d1e60f1d68f28c500793a61a97f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 9 Oct 2023 17:03:21 -0700 Subject: [PATCH 008/178] Update dependencies. * notify: 5 -> 6 * indexmap: 1 -> 2 * cookie: 0.18.0-rc.0 -> 0.18 --- contrib/dyn_templates/Cargo.toml | 2 +- core/codegen/Cargo.toml | 2 +- core/http/Cargo.toml | 4 ++-- core/lib/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 6a226ac181..166bdf8fd6 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -18,7 +18,7 @@ handlebars = ["handlebars_"] [dependencies] glob = "0.3" -notify = "5.0.0" +notify = "6" normpath = "1" [dependencies.rocket] diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 13b59390a4..bda61846b7 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -16,7 +16,7 @@ rust-version = "1.56" proc-macro = true [dependencies] -indexmap = "1.0" +indexmap = "2" quote = "1.0" syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits"] } proc-macro2 = "1.0.27" diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 41fd3813b8..6e3020ff9b 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -29,7 +29,7 @@ smallvec = "1.0" percent-encoding = "2" http = "0.2" time = { version = "0.3", features = ["formatting", "macros"] } -indexmap = { version = "1.5.2", features = ["std"] } +indexmap = "2" rustls = { version = "0.21", optional = true } tokio-rustls = { version = "0.24", optional = true } rustls-pemfile = { version = "1.0.2", optional = true } @@ -42,7 +42,7 @@ pear = "0.2.3" pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" -cookie = { version = "=0.18.0-rc.0", features = ["percent-encode"] } +cookie = { version = "0.18", features = ["percent-encode"] } state = "0.6" futures = { version = "0.3", default-features = false } diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index bed0ec8b76..092e08ff2c 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -52,7 +52,7 @@ figment = { version = "0.10.6", features = ["toml", "env"] } rand = "0.8" either = "1" pin-project-lite = "0.2" -indexmap = { version = "1.0", features = ["serde-1", "std"] } +indexmap = { version = "2", features = ["serde"] } tempfile = "3" async-trait = "0.1.43" async-stream = "0.3.2" From f950d3e0ec497281781523444c2e63b2dd38cb27 Mon Sep 17 00:00:00 2001 From: Fenhl Date: Tue, 19 Sep 2023 01:08:15 +0000 Subject: [PATCH 009/178] Upgrade 'tokio-tungstenite' to 0.20. --- contrib/ws/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index 81a8af7b65..10068cca24 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -17,7 +17,7 @@ default = ["tungstenite"] tungstenite = ["tokio-tungstenite"] [dependencies] -tokio-tungstenite = { version = "0.19", optional = true } +tokio-tungstenite = { version = "0.20", optional = true } [dependencies.rocket] version = "=0.5.0-rc.3" From ed5c755bb69991727b284fc3c255d77e0d2fe1da Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 14 Oct 2023 15:00:36 -0700 Subject: [PATCH 010/178] Avoid using 'glob' to walk templates directory. Previously, `dyn_templates` walked the user-provided `template_dir` path by constructing a glob pattern prefixed with `template_dir`. If `template_dir` contained characters recognized by the glob pattern parser, then at best the pattern failed to parse, and at worst, incorrect directories were searched. This commit removes the use of `glob` to walk the templates directory and instead uses `walkdir`, obviating the issues described above. Fixes #2627. --- contrib/dyn_templates/Cargo.toml | 2 +- contrib/dyn_templates/src/context.rs | 33 ++++++++++++------- contrib/dyn_templates/tests/templates.rs | 10 ++++++ .../templates/tera/[test]/html_test.html.tera | 5 +++ 4 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 contrib/dyn_templates/tests/templates/tera/[test]/html_test.html.tera diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 166bdf8fd6..d4386d2b2b 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -17,7 +17,7 @@ tera = ["tera_"] handlebars = ["handlebars_"] [dependencies] -glob = "0.3" +walkdir = "2.4" notify = "6" normpath = "1" diff --git a/contrib/dyn_templates/src/context.rs b/contrib/dyn_templates/src/context.rs index cf698823d9..ecdd93212a 100644 --- a/contrib/dyn_templates/src/context.rs +++ b/contrib/dyn_templates/src/context.rs @@ -26,6 +26,12 @@ impl Context { /// template engine, and store all of the initialized state in a `Context` /// structure, which is returned if all goes well. pub fn initialize(root: &Path, callback: &Callback) -> Option { + fn is_file_with_ext(entry: &walkdir::DirEntry, ext: &str) -> bool { + let is_file = entry.file_type().is_file(); + let has_ext = entry.path().extension().map_or(false, |e| e == ext); + is_file && has_ext + } + let root = match root.normalize() { Ok(root) => root.into_path_buf(), Err(e) => { @@ -35,18 +41,23 @@ impl Context { }; let mut templates: HashMap = HashMap::new(); - for ext in Engines::ENABLED_EXTENSIONS { - let mut glob_path = root.join("**").join("*"); - glob_path.set_extension(ext); - let glob_path = glob_path.to_str().expect("valid glob path string"); + for &ext in Engines::ENABLED_EXTENSIONS { + for entry in walkdir::WalkDir::new(&root).follow_links(true) { + let entry = match entry { + Ok(entry) if is_file_with_ext(&entry, ext) => entry, + Ok(_) | Err(_) => continue, + }; - for path in glob::glob(glob_path).unwrap().filter_map(Result::ok) { - let (name, data_type_str) = split_path(&root, &path); + let (name, data_type_str) = split_path(&root, entry.path()); if let Some(info) = templates.get(&*name) { - warn_!("Template name '{}' does not have a unique path.", name); - info_!("Existing path: {:?}", info.path); - info_!("Additional path: {:?}", path); - warn_!("Using existing path for template '{}'.", name); + warn_!("Template name '{}' does not have a unique source.", name); + match info.path { + Some(ref path) => info_!("Existing path: {:?}", path), + None => info_!("Existing Content-Type: {}", info.data_type), + } + + info_!("Additional path: {:?}", entry.path()); + warn_!("Keeping existing template '{}'.", name); continue; } @@ -55,7 +66,7 @@ impl Context { .unwrap_or(ContentType::Text); templates.insert(name, TemplateInfo { - path: Some(path.clone()), + path: Some(entry.into_path()), engine_ext: ext, data_type, }); diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index c73927f94a..8db5923df9 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -237,6 +237,16 @@ mod tera_tests { assert_eq!(md_rendered, Some((ContentType::HTML, ESCAPED_EXPECTED.into()))); } + #[async_test] + async fn test_globby_paths() { + use rocket::local::asynchronous::Client; + + let client = Client::debug(rocket()).await.unwrap(); + let req = client.get("/"); + let metadata = Metadata::from_request(&req).await.unwrap(); + assert!(metadata.contains_template("tera/[test]/html_test")); + } + // u128 is not supported. enable when it is. // #[test] // fn test_tera_u128() { diff --git a/contrib/dyn_templates/tests/templates/tera/[test]/html_test.html.tera b/contrib/dyn_templates/tests/templates/tera/[test]/html_test.html.tera new file mode 100644 index 0000000000..17cd1b2023 --- /dev/null +++ b/contrib/dyn_templates/tests/templates/tera/[test]/html_test.html.tera @@ -0,0 +1,5 @@ +{% extends "tera/base" %} +{% block title %}{{ title }}{% endblock title %} +{% block content %} +{{ content }} +{% endblock content %} From 58adc489651cfa9f2e8fedf2feab9e7cd07e2bb7 Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister <4602612+bash@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:49:27 +0200 Subject: [PATCH 011/178] Set 'color-scheme' in default catcher HTML. --- core/lib/src/catcher/catcher.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index faa54758f3..3ed8ff327d 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -373,6 +373,7 @@ r#" + "#, $code, " ", $reason, r#" From ac01e55e8bf732c6826cea3224c79f0456062a0c Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 14 Aug 2023 15:46:21 -0700 Subject: [PATCH 012/178] Fix typo in 'rocket_ws' README. --- contrib/ws/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ws/README.md b/contrib/ws/README.md index bd889e8950..825bdab6d6 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -24,7 +24,7 @@ This crate provides WebSocket support for Rocket via integration with Rocket's ```rust #[get("/echo")] fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::stream! { ws => + ws::Stream! { ws => for await message in ws { yield message?; } From 07fe79796f058ab12683ff9e344558bece263274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=9D=B0=E5=8F=8B=20Jieyou=20Xu=20=28Joe=29?= Date: Sun, 30 Jul 2023 21:32:05 +0800 Subject: [PATCH 013/178] Use Span::mixed_site to avoid let unit warnings. Closes #2568. --- core/codegen/src/attribute/route/mod.rs | 2 +- .../tests/ui-fail-nightly/async-entry.stderr | 7 +++---- .../tests/ui-fail-nightly/responder-types.stderr | 3 +++ .../tests/ui-fail-stable/async-entry.stderr | 16 ++++++++-------- core/codegen/tests/ui-fail-stable/catch.stderr | 2 +- .../tests/ui-fail-stable/responder-types.stderr | 3 +++ .../ui-fail-stable/route-type-errors.stderr | 2 +- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 8003dda430..1079f3066b 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -242,7 +242,7 @@ fn responder_outcome_expr(route: &Route) -> TokenStream { .map(|a| quote_spanned!(a.span() => .await)); define_spanned_export!(ret_span => __req, _route); - quote_spanned! { ret_span => + quote_spanned! { Span::mixed_site().located_at(ret_span) => let ___responder = #user_handler_fn_name(#(#parameter_names),*) #_await; #_route::Outcome::from(#__req, ___responder) } diff --git a/core/codegen/tests/ui-fail-nightly/async-entry.stderr b/core/codegen/tests/ui-fail-nightly/async-entry.stderr index 74374c654a..f6acb1576b 100644 --- a/core/codegen/tests/ui-fail-nightly/async-entry.stderr +++ b/core/codegen/tests/ui-fail-nightly/async-entry.stderr @@ -149,10 +149,9 @@ error[E0308]: mismatched types --> tests/ui-fail-nightly/async-entry.rs:24:21 | 24 | async fn main() { - | ^ - | | - | _____________________expected `()` because of default return type - | | + | ____________________-^ + | | | + | | expected `()` because of default return type 25 | | rocket::build() 26 | | } | | ^- help: consider using a semicolon here: `;` diff --git a/core/codegen/tests/ui-fail-nightly/responder-types.stderr b/core/codegen/tests/ui-fail-nightly/responder-types.stderr index 58956ef88f..c54893bf41 100644 --- a/core/codegen/tests/ui-fail-nightly/responder-types.stderr +++ b/core/codegen/tests/ui-fail-nightly/responder-types.stderr @@ -104,6 +104,8 @@ note: required by a bound in `rocket::Response::<'r>::set_header` error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied --> tests/ui-fail-nightly/responder-types.rs:28:13 | +27 | #[get("/")] + | ----------- in this procedural macro expansion 28 | fn foo() -> usize { 0 } | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | @@ -122,3 +124,4 @@ note: required by a bound in `route::handler:: | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/async-entry.stderr b/core/codegen/tests/ui-fail-stable/async-entry.stderr index dbd9512c43..d3ac4cc7af 100644 --- a/core/codegen/tests/ui-fail-stable/async-entry.stderr +++ b/core/codegen/tests/ui-fail-stable/async-entry.stderr @@ -107,6 +107,14 @@ error[E0728]: `await` is only allowed inside `async` functions and blocks 73 | let _ = rocket::build().launch().await; | ^^^^^ only allowed inside `async` functions and blocks +error[E0277]: `main` has invalid return type `Rocket` + --> tests/ui-fail-stable/async-entry.rs:94:20 + | +94 | async fn main() -> rocket::Rocket { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` can only return types that implement `Termination` + | + = help: consider using `()`, or a `Result` + error[E0308]: mismatched types --> tests/ui-fail-stable/async-entry.rs:35:9 | @@ -165,11 +173,3 @@ error[E0308]: mismatched types | = note: expected struct `Rocket` found struct `std::string::String` - -error[E0277]: `main` has invalid return type `Rocket` - --> tests/ui-fail-stable/async-entry.rs:94:20 - | -94 | async fn main() -> rocket::Rocket { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` can only return types that implement `Termination` - | - = help: consider using `()`, or a `Result` diff --git a/core/codegen/tests/ui-fail-stable/catch.stderr b/core/codegen/tests/ui-fail-stable/catch.stderr index 49ded6be31..fdb47184a9 100644 --- a/core/codegen/tests/ui-fail-stable/catch.stderr +++ b/core/codegen/tests/ui-fail-stable/catch.stderr @@ -67,7 +67,7 @@ note: function defined here | 30 | fn f3(_request: &Request, other: bool) { } | ^^ ------------------ ----------- -help: did you mean +help: provide the argument | 29 | f3(bool, /* bool */) | diff --git a/core/codegen/tests/ui-fail-stable/responder-types.stderr b/core/codegen/tests/ui-fail-stable/responder-types.stderr index 1ee3af24c2..2526127ea7 100644 --- a/core/codegen/tests/ui-fail-stable/responder-types.stderr +++ b/core/codegen/tests/ui-fail-stable/responder-types.stderr @@ -104,6 +104,8 @@ note: required by a bound in `rocket::Response::<'r>::set_header` error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied --> tests/ui-fail-stable/responder-types.rs:28:13 | +27 | #[get("/")] + | ----------- in this procedural macro expansion 28 | fn foo() -> usize { 0 } | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | @@ -122,3 +124,4 @@ note: required by a bound in `route::handler:: | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/route-type-errors.stderr b/core/codegen/tests/ui-fail-stable/route-type-errors.stderr index 40dd0d27cd..dbde5a8882 100644 --- a/core/codegen/tests/ui-fail-stable/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-stable/route-type-errors.stderr @@ -74,8 +74,8 @@ error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied Cow<'_, str> Capped> Capped> - Capped Capped> + Capped Capped<&'r str> Capped<&'r RawStr> and $N others From 260e671d43bfc6345561c40f377d6d70efc1d99e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 27 Oct 2023 14:50:48 -0500 Subject: [PATCH 014/178] Use mixed-site spans to avoid clippy 'uri!' error. Closes #2630. --- core/codegen/src/attribute/route/mod.rs | 3 ++- core/codegen/src/bang/uri.rs | 2 +- core/codegen/src/exports.rs | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 1079f3066b..93fac9b9de 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -10,6 +10,7 @@ use crate::proc_macro_ext::StringLit; use crate::syn_ext::{IdentExt, TypeExt as _}; use crate::http_codegen::{Method, Optional}; use crate::attribute::param::Guard; +use crate::exports::mixed; use self::parse::{Route, Attribute, MethodAttribute}; @@ -242,7 +243,7 @@ fn responder_outcome_expr(route: &Route) -> TokenStream { .map(|a| quote_spanned!(a.span() => .await)); define_spanned_export!(ret_span => __req, _route); - quote_spanned! { Span::mixed_site().located_at(ret_span) => + quote_spanned! { mixed(ret_span) => let ___responder = #user_handler_fn_name(#(#parameter_names),*) #_await; #_route::Outcome::from(#__req, ___responder) } diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index 175b73da7e..b8e6c1d42b 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -126,7 +126,7 @@ fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type let tmp_ident = ident.clone().with_span(expr.span()); let let_stmt = quote_spanned!(span => let #tmp_ident = #expr); - to.push(quote_spanned!(span => + to.push(quote_spanned!(mixed(span) => #[allow(non_snake_case)] #let_stmt; let #ident = <#ty as #_fmt::FromUriParam<#part, _>>::from_uri_param(#tmp_ident); )); diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs index d6c1f4d911..1c947ac355 100644 --- a/core/codegen/src/exports.rs +++ b/core/codegen/src/exports.rs @@ -106,3 +106,9 @@ define_exported_paths! { macro_rules! define_spanned_export { ($span:expr => $($name:ident),*) => ($(define!($span => $name $name);)*) } + +/// Convenience: returns a "mixed site" span located at `span`. +#[inline(always)] +pub fn mixed(span: Span) -> Span { + Span::mixed_site().located_at(span) +} From 11c9c3cbcd2ce66501f0fe44cdb0964ad4ef78a7 Mon Sep 17 00:00:00 2001 From: Steven Murdoch Date: Wed, 14 Jun 2023 13:51:18 +0200 Subject: [PATCH 015/178] Fix off-by-one, improve style in forms example. --- examples/forms/templates/index.html.tera | 17 +++++++++++++---- examples/forms/templates/macros.html.tera | 10 +++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/forms/templates/index.html.tera b/examples/forms/templates/index.html.tera index 78f06469bf..dd227b8e3a 100644 --- a/examples/forms/templates/index.html.tera +++ b/examples/forms/templates/index.html.tera @@ -13,16 +13,25 @@ margin: 0 auto; padding: 20px 10px; } + + h1 { + margin: 10px 0; + }

Form Example

- {% if errors | length > 1 %} - - {{ errors | length }} field(s) have errors - + {% if errors | length > 0 %} +
+
+ + error: {{ errors | length }} field{{ errors | length | pluralize }} + failed to validate + +
+
{% endif %}
diff --git a/examples/forms/templates/macros.html.tera b/examples/forms/templates/macros.html.tera index 2b0128c307..ec7144ebc6 100644 --- a/examples/forms/templates/macros.html.tera +++ b/examples/forms/templates/macros.html.tera @@ -2,7 +2,7 @@ {%- if name in values -%} {{- values | get(key=name) | first -}} {%- endif -%} -{% endmacro %} +{% endmacro value_for %} {% macro errors_for(name) %} {%- if name in errors -%} @@ -11,7 +11,7 @@

{{ error.msg }}

{% endfor %} {%- endif -%} -{% endmacro %} +{% endmacro errors_for %} {% macro input(type, label, name, value="") %} @@ -37,7 +37,7 @@ > {{ label }} -{% endmacro input %} +{% endmacro checkbox %} {% macro textarea(label, name, placeholder="", max=250) %} @@ -49,7 +49,7 @@ {{ self::errors_for(name=name) }} -{% endmacro input %} +{% endmacro textarea %} {% macro select(label, name, options) %} @@ -60,4 +60,4 @@ >{{ value }} {% endfor %} -{% endmacro input %} +{% endmacro select %} From c90812051e21f25eb54649eb0c13f6793de4b50b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 31 Oct 2023 12:11:03 -0500 Subject: [PATCH 016/178] Rename 'Outcome::Failure' to 'Outcome::Error'. The primary motivation is to deconflate the leading `F`s in `Failure` and `Forward`. In particular, when using a generics, we used `F` for forward, which could easily be confused for `F` for `Failure`. This resolves the conflation. --- contrib/db_pools/codegen/src/database.rs | 2 +- contrib/db_pools/lib/src/database.rs | 6 +- contrib/dyn_templates/src/metadata.rs | 2 +- contrib/sync_db_pools/lib/src/connection.rs | 2 +- core/codegen/src/attribute/route/mod.rs | 8 +- core/codegen/src/lib.rs | 7 +- core/http/src/parse/uri/tests.rs | 4 +- core/http/src/tls/mtls.rs | 2 +- core/lib/src/catcher/catcher.rs | 12 +- core/lib/src/config/config.rs | 2 +- core/lib/src/config/secret_key.rs | 4 +- core/lib/src/data/capped.rs | 4 +- core/lib/src/data/from_data.rs | 24 +-- core/lib/src/error.rs | 4 +- core/lib/src/fairing/mod.rs | 6 +- core/lib/src/form/error.rs | 2 +- core/lib/src/form/form.rs | 6 +- core/lib/src/form/from_form_field.rs | 6 +- core/lib/src/form/parser.rs | 2 +- core/lib/src/form/validate.rs | 20 +-- core/lib/src/fs/server.rs | 4 +- core/lib/src/outcome.rs | 165 ++++++++++---------- core/lib/src/request/from_request.rs | 75 ++++----- core/lib/src/response/responder.rs | 2 +- core/lib/src/route/handler.rs | 20 +-- core/lib/src/route/route.rs | 2 +- core/lib/src/serde/json.rs | 6 +- core/lib/src/serde/msgpack.rs | 6 +- core/lib/src/serde/uuid.rs | 2 +- core/lib/src/server.rs | 14 +- core/lib/src/state.rs | 2 +- examples/manual-routing/src/main.rs | 6 +- site/guide/4-requests.md | 10 +- site/guide/5-responses.md | 2 +- site/tests/src/lib.rs | 2 +- 35 files changed, 222 insertions(+), 221 deletions(-) diff --git a/contrib/db_pools/codegen/src/database.rs b/contrib/db_pools/codegen/src/database.rs index 837aa6c8ac..a2ca218de1 100644 --- a/contrib/db_pools/codegen/src/database.rs +++ b/contrib/db_pools/codegen/src/database.rs @@ -67,7 +67,7 @@ pub fn derive_database(input: TokenStream) -> TokenStream { ) -> rocket::request::Outcome { match #db_ty::fetch(req.rocket()) { Some(db) => rocket::outcome::Outcome::Success(db), - None => rocket::outcome::Outcome::Failure(( + None => rocket::outcome::Outcome::Error(( rocket::http::Status::InternalServerError, ())) } } diff --git a/contrib/db_pools/lib/src/database.rs b/contrib/db_pools/lib/src/database.rs index 4afbd1f3e4..b8179be052 100644 --- a/contrib/db_pools/lib/src/database.rs +++ b/contrib/db_pools/lib/src/database.rs @@ -164,7 +164,7 @@ pub struct Initializer(Option<&'static str>, PhantomData D> /// [`connect_timeout`](crate::Config::connect_timeout) seconds. /// * If the `Initializer` fairing was _not_ attached, the guard _fails_ with /// status `InternalServerError`. A [`Sentinel`] guards this condition, and so -/// this type of failure is unlikely to occur. A `None` error is returned. +/// this type of error is unlikely to occur. A `None` error is returned. /// * If a connection is not available within `connect_timeout` seconds or /// another error occurs, the guard _fails_ with status `ServiceUnavailable` /// and the error is returned in `Some`. @@ -288,9 +288,9 @@ impl<'r, D: Database> FromRequest<'r> for Connection { match D::fetch(req.rocket()) { Some(db) => match db.get().await { Ok(conn) => Outcome::Success(Connection(conn)), - Err(e) => Outcome::Failure((Status::ServiceUnavailable, Some(e))), + Err(e) => Outcome::Error((Status::ServiceUnavailable, Some(e))), }, - None => Outcome::Failure((Status::InternalServerError, None)), + None => Outcome::Error((Status::InternalServerError, None)), } } } diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index 06d722d2d1..9f52f33dfb 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -153,7 +153,7 @@ impl<'r> FromRequest<'r> for Metadata<'r> { error_!("Uninitialized template context: missing fairing."); info_!("To use templates, you must attach `Template::fairing()`."); info_!("See the `Template` documentation for more information."); - request::Outcome::Failure((Status::InternalServerError, ())) + request::Outcome::Error((Status::InternalServerError, ())) }) } } diff --git a/contrib/sync_db_pools/lib/src/connection.rs b/contrib/sync_db_pools/lib/src/connection.rs index a52aae72ff..c395dc2866 100644 --- a/contrib/sync_db_pools/lib/src/connection.rs +++ b/contrib/sync_db_pools/lib/src/connection.rs @@ -213,7 +213,7 @@ impl<'r, K: 'static, C: Poolable> FromRequest<'r> for Connection { Some(c) => c.get().await.into_outcome((Status::ServiceUnavailable, ())), None => { error_!("Missing database fairing for `{}`", std::any::type_name::()); - Outcome::Failure((Status::InternalServerError, ())) + Outcome::Error((Status::InternalServerError, ())) } } } diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 93fac9b9de..e30117db00 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -126,9 +126,9 @@ fn request_guard_decl(guard: &Guard) -> TokenStream { #_log::warn_!("Request guard `{}` is forwarding.", stringify!(#ty)); return #Outcome::Forward((#__data, __e)); }, - #Outcome::Failure((__c, __e)) => { + #Outcome::Error((__c, __e)) => { #_log::warn_!("Request guard `{}` failed: {:?}.", stringify!(#ty), __e); - return #Outcome::Failure(__c); + return #Outcome::Error(__c); } }; } @@ -189,9 +189,9 @@ fn data_guard_decl(guard: &Guard) -> TokenStream { #_log::warn_!("Data guard `{}` is forwarding.", stringify!(#ty)); return #Outcome::Forward((__d, __e)); } - #Outcome::Failure((__c, __e)) => { + #Outcome::Error((__c, __e)) => { #_log::warn_!("Data guard `{}` failed: {:?}.", stringify!(#ty), __e); - return #Outcome::Failure(__c); + return #Outcome::Error(__c); } }; } diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 7cec0eb073..abcf387262 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -239,8 +239,7 @@ macro_rules! route_attribute { /// /// If a request guard fails, the request is forwarded if the /// [`Outcome`] is `Forward` or failed if the [`Outcome`] is - /// `Failure`. See [`FromRequest` Outcomes] for further - /// detail. + /// `Error`. See [`FromRequest` Outcomes] for further detail. /// /// 2. Path and query guards in an unspecified order. If a path /// or query guard fails, the request is forwarded. @@ -249,7 +248,7 @@ macro_rules! route_attribute { /// /// If a data guard fails, the request is forwarded if the /// [`Outcome`] is `Forward` or failed if the [`Outcome`] is - /// `Failure`. See [`FromData`] for further detail. + /// `Error`. See [`FromData`] for further detail. /// /// If all validation succeeds, the decorated function is called. /// The returned value is used to generate a [`Response`] via the @@ -448,7 +447,7 @@ pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { /// #[rocket::main] /// async fn main() { /// // Recall that an uninspected `Error` will cause a pretty-printed panic, -/// // so rest assured failures do not go undetected when using `#[launch]`. +/// // so rest assured errors do not go undetected when using `#[launch]`. /// let _ = rocket().launch().await; /// } /// ``` diff --git a/core/http/src/parse/uri/tests.rs b/core/http/src/parse/uri/tests.rs index 7106e4b2f0..ac7b506c9e 100644 --- a/core/http/src/parse/uri/tests.rs +++ b/core/http/src/parse/uri/tests.rs @@ -8,7 +8,7 @@ macro_rules! assert_parse_eq { match from_str($from) { Ok(output) => { if output != expected { - println!("Failure on: {:?}", $from); + println!("Error on: {:?}", $from); assert_eq!(output, expected, "{} != {}", output, expected); } } @@ -53,7 +53,7 @@ macro_rules! assert_displays_eq { Ok(output) => { let output_string = output.to_string(); if output_string != string { - println!("Failure on: {:?}", $string); + println!("Error on: {:?}", $string); println!("Got: {:?}", output_string); println!("Parsed as: {:?}", output); panic!("failed"); diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 9d7554fb7f..45ea38fc48 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -533,7 +533,7 @@ impl fmt::Display for Name<'_> { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Error::Parse(e) => write!(f, "parse failure: {}", e), + Error::Parse(e) => write!(f, "parse error: {}", e), Error::Incomplete(_) => write!(f, "incomplete certificate data"), Error::Trailing(n) => write!(f, "found {} trailing bytes", n), Error::Empty => write!(f, "empty certificate chain"), diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 3ed8ff327d..299d7b08c8 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -18,12 +18,14 @@ use yansi::Paint; /// /// * A failing guard. /// * A failing responder. +/// * A forwarding guard. /// * Routing failure. /// -/// Each failure is paired with a status code. Guards and responders indicate -/// the status code themselves via their `Err` return value while a routing -/// failure is always a `404`. Rocket invokes the error handler for the catcher -/// with the error's status code. +/// Each error or forward is paired with a status code. Guards and responders +/// indicate the status code themselves via their `Err` and `Outcome` return +/// value. A complete routing failure is always a `404`. Rocket invokes the +/// error handler for the catcher with an error's status code, or in the case of +/// every route resulting in a forward, the last forwarded status code. /// /// ### Error Handler Restrictions /// @@ -33,7 +35,7 @@ use yansi::Paint; /// /// # Routing /// -/// If a route fails by returning a failure [`Outcome`], Rocket routes the +/// If a route fails by returning an error [`Outcome`], Rocket routes the /// erroring request to the highest precedence catcher among all the catchers /// that [match](Catcher::matches()). See [`Catcher::matches()`] for details on /// matching. Precedence is determined by the catcher's _base_, which is diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 61b25bffa7..4a23bf4f3d 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -288,7 +288,7 @@ impl Config { /// /// # Panics /// - /// If extraction fails, prints an error message indicating the failure and + /// If extraction fails, prints an error message indicating the error and /// panics. For a version that doesn't panic, use [`Config::try_from()`]. /// /// # Example diff --git a/core/lib/src/config/secret_key.rs b/core/lib/src/config/secret_key.rs index 445f960156..02947e7090 100644 --- a/core/lib/src/config/secret_key.rs +++ b/core/lib/src/config/secret_key.rs @@ -52,7 +52,7 @@ enum Kind { /// ``` /// /// When running in any other profile with the `secrets` feature enabled, -/// providing a key of `0` or not provided a key at all results in a failure at +/// providing a key of `0` or not provided a key at all results in an error at /// launch-time: /// /// ```rust @@ -67,7 +67,7 @@ enum Kind { /// .select(profile.clone()); /// /// let rocket = rocket::custom(figment); -/// let error = Client::tracked(rocket).expect_err("failure in non-debug"); +/// let error = Client::tracked(rocket).expect_err("error in non-debug"); /// assert!(matches!(error.kind(), ErrorKind::InsecureSecretKey(profile))); /// ``` /// diff --git a/core/lib/src/data/capped.rs b/core/lib/src/data/capped.rs index 335c980f1b..804a42d486 100644 --- a/core/lib/src/data/capped.rs +++ b/core/lib/src/data/capped.rs @@ -260,10 +260,10 @@ macro_rules! impl_strict_from_data_from_capped { Success(p) if p.is_complete() => Success(p.into_inner()), Success(_) => { let e = Error::new(UnexpectedEof, "data limit exceeded"); - Failure((Status::BadRequest, e.into())) + Error((Status::BadRequest, e.into())) }, Forward(d) => Forward(d), - Failure((s, e)) => Failure((s, e)), + Error((s, e)) => Error((s, e)), } } } diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index 2b7465c351..b3ac98c816 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -10,14 +10,14 @@ pub type Outcome<'r, T, E = >::Error> = outcome::Outcome, Status)>; impl<'r, S, E> IntoOutcome, Status)> for Result { - type Failure = Status; + type Error = Status; type Forward = (Data<'r>, Status); #[inline] fn into_outcome(self, status: Status) -> Outcome<'r, S, E> { match self { Ok(val) => Success(val), - Err(err) => Failure((status, err)) + Err(err) => Error((status, err)) } } @@ -251,6 +251,7 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// use rocket::request::{self, Request}; /// use rocket::data::{self, Data, FromData, ToByteUnit}; /// use rocket::http::{Status, ContentType}; +/// use rocket::outcome::Outcome; /// /// #[derive(Debug)] /// enum Error { @@ -266,12 +267,11 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// /// async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> { /// use Error::*; -/// use rocket::outcome::Outcome::*; /// /// // Ensure the content type is correct before opening the data. /// let person_ct = ContentType::new("application", "x-person"); /// if req.content_type() != Some(&person_ct) { -/// return Forward((data, Status::NotFound)); +/// return Outcome::Forward((data, Status::NotFound)); /// } /// /// // Use a configured limit with name 'person' or fallback to default. @@ -280,8 +280,8 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// // Read the data into a string. /// let string = match data.open(limit).into_string().await { /// Ok(string) if string.is_complete() => string.into_inner(), -/// Ok(_) => return Failure((Status::PayloadTooLarge, TooLarge)), -/// Err(e) => return Failure((Status::InternalServerError, Io(e))), +/// Ok(_) => return Outcome::Error((Status::PayloadTooLarge, TooLarge)), +/// Err(e) => return Outcome::Error((Status::InternalServerError, Io(e))), /// }; /// /// // We store `string` in request-local cache for long-lived borrows. @@ -290,16 +290,16 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// // Split the string into two pieces at ':'. /// let (name, age) = match string.find(':') { /// Some(i) => (&string[..i], &string[(i + 1)..]), -/// None => return Failure((Status::UnprocessableEntity, NoColon)), +/// None => return Outcome::Error((Status::UnprocessableEntity, NoColon)), /// }; /// /// // Parse the age. /// let age: u16 = match age.parse() { /// Ok(age) => age, -/// Err(_) => return Failure((Status::UnprocessableEntity, InvalidAge)), +/// Err(_) => return Outcome::Error((Status::UnprocessableEntity, InvalidAge)), /// }; /// -/// Success(Person { name, age }) +/// Outcome::Success(Person { name, age }) /// } /// } /// @@ -331,7 +331,7 @@ pub trait FromData<'r>: Sized { /// /// If validation and parsing succeeds, an outcome of `Success` is returned. /// If the data is not appropriate given the type of `Self`, `Forward` is - /// returned. If parsing fails, `Failure` is returned. + /// returned. If parsing fails, `Error` is returned. async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self>; } @@ -428,7 +428,7 @@ impl<'r, T: FromData<'r> + 'r> FromData<'r> for Result { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { match T::from_data(req, data).await { Success(v) => Success(Ok(v)), - Failure((_, e)) => Success(Err(e)), + Error((_, e)) => Success(Err(e)), Forward(d) => Forward(d), } } @@ -441,7 +441,7 @@ impl<'r, T: FromData<'r>> FromData<'r> for Option { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { match T::from_data(req, data).await { Success(v) => Success(Some(v)), - Failure(..) | Forward(..) => Success(None), + Error(..) | Forward(..) => Success(None), } } } diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index b68b255bcb..28e71fc216 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -216,9 +216,9 @@ impl Error { crate::config::pretty_print_error(error.clone()); "aborting due to invalid configuration" } - ErrorKind::SentinelAborts(ref failures) => { + ErrorKind::SentinelAborts(ref errors) => { error!("Rocket failed to launch due to aborting sentinels:"); - for sentry in failures { + for sentry in errors { let name = sentry.type_name.primary().bold(); let (file, line, col) = sentry.location; info_!("{} ({}:{}:{})", name, file, line, col); diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs index 642b80b884..1439d8f4cd 100644 --- a/core/lib/src/fairing/mod.rs +++ b/core/lib/src/fairing/mod.rs @@ -66,7 +66,7 @@ pub type Result, E = Rocket> = std::result::Result, E = Rocket> = std::result::ResultLiftoff (`on_liftoff`)** /// @@ -417,7 +417,7 @@ pub type Result, E = Rocket> = std::result::Result) -> request::Outcome { /// match *request.local_cache(|| TimerStart(None)) { /// TimerStart(Some(time)) => request::Outcome::Success(StartTime(time)), -/// TimerStart(None) => request::Outcome::Failure((Status::InternalServerError, ())), +/// TimerStart(None) => request::Outcome::Error((Status::InternalServerError, ())), /// } /// } /// } diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs index 7167eb5e7b..b6f3c50690 100644 --- a/core/lib/src/form/error.rs +++ b/core/lib/src/form/error.rs @@ -350,7 +350,7 @@ impl<'v> Errors<'v> { /// Returns the highest [`Error::status()`] of all of the errors in `self` /// or [`Status::InternalServerError`] if `self` is empty. This is the /// status that is set by the [`Form`](crate::form::Form) data guard on - /// failure. + /// error. /// /// See [`Error::status()`] for the corresponding status code of each /// [`Error`] variant. diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index c747adc2a1..12b1c40ced 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -65,8 +65,8 @@ use crate::form::prelude::*; /// /// If the request `ContentType` _does_ identify as a form but the form data /// does not parse as `T`, according to `T`'s [`FromForm`] implementation, the -/// guard **fails**. The `Failure` variant contains of the [`Errors`] emitted by -/// `T`'s `FromForm` parser. If the error is not caught by a +/// guard **fails**. The `Error` variant contains a vector of the [`Errors`] +/// emitted by `T`'s `FromForm` parser. If the error is not caught by a /// [`form::Result`](Result) or `Option>` data guard, the status code /// is set to [`Errors::status()`], and the corresponding error catcher is /// called. @@ -334,7 +334,7 @@ impl<'r, T: FromForm<'r>> FromData<'r> for Form { match T::finalize(context) { Ok(value) => Outcome::Success(Form(value)), - Err(e) => Outcome::Failure((e.status(), e)), + Err(e) => Outcome::Error((e.status(), e)), } } } diff --git a/core/lib/src/form/from_form_field.rs b/core/lib/src/form/from_form_field.rs index d03ee38693..7e733133cf 100644 --- a/core/lib/src/form/from_form_field.rs +++ b/core/lib/src/form/from_form_field.rs @@ -297,7 +297,7 @@ impl<'v> FromFormField<'v> for Capped<&'v str> { match as FromData>::from_data(f.request, f.data).await { Outcome::Success(p) => Ok(p), - Outcome::Failure((_, e)) => Err(e)?, + Outcome::Error((_, e)) => Err(e)?, Outcome::Forward(..) => { Err(Error::from(ErrorKind::Unexpected).with_entity(Entity::DataField))? } @@ -318,7 +318,7 @@ impl<'v> FromFormField<'v> for Capped { match as FromData>::from_data(f.request, f.data).await { Outcome::Success(p) => Ok(p), - Outcome::Failure((_, e)) => Err(e)?, + Outcome::Error((_, e)) => Err(e)?, Outcome::Forward(..) => { Err(Error::from(ErrorKind::Unexpected).with_entity(Entity::DataField))? } @@ -354,7 +354,7 @@ impl<'v> FromFormField<'v> for Capped<&'v [u8]> { match as FromData>::from_data(f.request, f.data).await { Outcome::Success(p) => Ok(p), - Outcome::Failure((_, e)) => Err(e)?, + Outcome::Error((_, e)) => Err(e)?, Outcome::Forward(..) => { Err(Error::from(ErrorKind::Unexpected).with_entity(Entity::DataField))? } diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index 89c42eb888..7a9c4437be 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -40,7 +40,7 @@ impl<'r, 'i> Parser<'r, 'i> { match parser { Ok(storage) => Outcome::Success(storage), - Err(e) => Outcome::Failure((e.status(), e.into())) + Err(e) => Outcome::Error((e.status(), e.into())) } } diff --git a/core/lib/src/form/validate.rs b/core/lib/src/form/validate.rs index 821589cbf5..6a079be0ac 100644 --- a/core/lib/src/form/validate.rs +++ b/core/lib/src/form/validate.rs @@ -159,7 +159,7 @@ crate::export! { /// Equality validator: succeeds exactly when `a` == `b`, using [`PartialEq`]. /// -/// On failure, returns a validation error with the following message: +/// On error, returns a validation error with the following message: /// /// ```text /// value does not match expected value @@ -236,7 +236,7 @@ pub fn dbg_eq<'v, A, B>(a: &A, b: B) -> Result<'v, ()> /// Negative equality validator: succeeds exactly when `a` != `b`, using /// [`PartialEq`]. /// -/// On failure, returns a validation error with the following message: +/// On error, returns a validation error with the following message: /// /// ```text /// value is equal to an invalid value @@ -361,7 +361,7 @@ impl> Len for crate::serde::msgpack::MsgPack { /// Length validator: succeeds when the length of a value is within a `range`. /// -/// The value must implement [`Len`]. On failure, returns an [`InvalidLength`] +/// The value must implement [`Len`]. On error, returns an [`InvalidLength`] /// error. See [`Len`] for supported types and how their length is computed. /// /// [`InvalidLength`]: crate::form::error::ErrorKind::InvalidLength @@ -504,7 +504,7 @@ impl + ?Sized> Contains for &T { /// [`Contains`](Contains) where `I` is the type of the `item`. See /// [`Contains`] for supported types and items. /// -/// On failure, returns a validation error with the following message: +/// On error, returns a validation error with the following message: /// /// ```text /// value is equal to an invalid value @@ -590,7 +590,7 @@ pub fn dbg_contains<'v, V, I>(value: V, item: I) -> Result<'v, ()> /// [`Contains`](Contains) where `I` is the type of the `item`. See /// [`Contains`] for supported types and items. /// -/// On failure, returns a validation error with the following message: +/// On error, returns a validation error with the following message: /// /// ```text /// value contains a disallowed item @@ -670,7 +670,7 @@ pub fn dbg_omits<'v, V, I>(value: V, item: I) -> Result<'v, ()> /// Integer range validator: succeeds when an integer value is within a range. /// /// The value must be an integer type that implement `TryInto + Copy`. On -/// failure, returns an [`OutOfRange`] error. +/// error, returns an [`OutOfRange`] error. /// /// [`OutOfRange`]: crate::form::error::ErrorKind::OutOfRange /// @@ -719,7 +719,7 @@ pub fn range<'v, V, R>(value: &V, range: R) -> Result<'v, ()> /// the `item`. The iterator must be [`Clone`]. See [`Contains`] for supported /// types and items. The item must be [`Debug`]. /// -/// On failure, returns a [`InvalidChoice`] error with the debug representation +/// On error, returns a [`InvalidChoice`] error with the debug representation /// of each item in `items`. /// /// [`InvalidChoice`]: crate::form::error::ErrorKind::InvalidChoice @@ -762,7 +762,7 @@ pub fn one_of<'v, V, I, R>(value: V, items: R) -> Result<'v, ()> /// File type validator: succeeds when a [`TempFile`] has the Content-Type /// `content_type`. /// -/// On failure, returns a validation error with one of the following messages: +/// On error, returns a validation error with one of the following messages: /// /// ```text /// // the file has an incorrect extension @@ -810,7 +810,7 @@ pub fn ext<'v>(file: &TempFile<'_>, r#type: ContentType) -> Result<'v, ()> { /// when a more case-specific option does not exist. It succeeds exactly when /// `f` returns `true` and fails otherwise. /// -/// On failure, returns a validation error with the message `msg`. +/// On error, returns a validation error with the message `msg`. /// /// # Example /// @@ -860,7 +860,7 @@ pub fn with<'v, V, F, M>(value: V, f: F, msg: M) -> Result<'v, ()> /// Along with [`with`], this is the most generic validator. It succeeds /// exactly when `f` returns `Ok` and fails otherwise. /// -/// On failure, returns a validation error with the message in the `Err` +/// On error, returns a validation error with the message in the `Err` /// variant converted into a string. /// /// # Example diff --git a/core/lib/src/fs/server.rs b/core/lib/src/fs/server.rs index da78ec3374..8aac5c259b 100644 --- a/core/lib/src/fs/server.rs +++ b/core/lib/src/fs/server.rs @@ -148,12 +148,12 @@ impl FileServer { if !options.contains(Options::IndexFile) && !path.is_dir() { let path = path.display(); error!("FileServer path '{}' is not a directory.", path.primary()); - warn_!("Aborting early to prevent inevitable handler failure."); + warn_!("Aborting early to prevent inevitable handler error."); panic!("invalid directory: refusing to continue"); } else if !path.exists() { let path = path.display(); error!("FileServer path '{}' is not a file.", path.primary()); - warn_!("Aborting early to prevent inevitable handler failure."); + warn_!("Aborting early to prevent inevitable handler error."); panic!("invalid file: refusing to continue"); } } diff --git a/core/lib/src/outcome.rs b/core/lib/src/outcome.rs index 44a38f89de..549e850910 100644 --- a/core/lib/src/outcome.rs +++ b/core/lib/src/outcome.rs @@ -1,10 +1,10 @@ -//! Success, failure, and forward handling. +//! Success, error, and forward handling. //! //! The `Outcome` type is similar to the standard library's `Result` type. It is an enum with three variants, each containing a value: -//! `Success(S)`, which represents a successful outcome, `Failure(E)`, which -//! represents a failing outcome, and `Forward(F)`, which represents neither a -//! success or failure, but instead, indicates that processing could not be +//! `Success(S)`, which represents a successful outcome, `Error(E)`, which +//! represents an erroring outcome, and `Forward(F)`, which represents neither a +//! success or error, but instead, indicates that processing could not be //! handled and should instead be _forwarded_ to whatever can handle the //! processing next. //! @@ -35,15 +35,15 @@ //! `Success(S)`. If `from_data` returns a `Success`, the `Success` value will //! be unwrapped and the value will be used as the value of `my_val`. //! -//! # Failure +//! # Error //! -//! A failure `Outcome`, `Failure(E)`, is returned when a function +//! An error `Outcome`, `Error(E)`, is returned when a function //! fails with some error and no processing can or should continue as a result. -//! The meaning of a failure depends on the context. +//! The meaning of an error depends on the context. //! -//! In Rocket, a `Failure` generally means that a request is taken out of normal +//! In Rocket, an `Error` generally means that a request is taken out of normal //! processing. The request is then given to the catcher corresponding to some -//! status code. Users can catch failures by requesting a type of `Result` +//! status code. Users can catch errors by requesting a type of `Result` //! or `Option` in request handlers. For example, if a user's handler looks //! like: //! @@ -56,10 +56,9 @@ //! ``` //! //! The [`FromData`] implementation for the type `S` returns an `Outcome` with a -//! `Success(S)` and `Failure(E)`. If `from_data` returns a `Failure`, the -//! `Failure` value will be unwrapped and the value will be used as the `Err` -//! value of `my_val` while a `Success` will be unwrapped and used the `Ok` -//! value. +//! `Success(S)` and `Error(E)`. If `from_data` returns an `Error`, the `Error` +//! value will be unwrapped and the value will be used as the `Err` value of +//! `my_val` while a `Success` will be unwrapped and used the `Ok` value. //! //! # Forward //! @@ -79,11 +78,11 @@ //! ``` //! //! The [`FromData`] implementation for the type `S` returns an `Outcome` with a -//! `Success(S)`, `Failure(E)`, and `Forward(F)`. If the `Outcome` is a +//! `Success(S)`, `Error(E)`, and `Forward(F)`. If the `Outcome` is a //! `Forward`, the `hello` handler isn't called. Instead, the incoming request //! is forwarded, or passed on to, the next matching route, if any. Ultimately, //! if there are no non-forwarding routes, forwarded requests are handled by the -//! 404 catcher. Similar to `Failure`s, users can catch `Forward`s by requesting +//! 404 catcher. Similar to `Error`s, users can catch `Forward`s by requesting //! a type of `Option`. If an `Outcome` is a `Forward`, the `Option` will be //! `None`. @@ -93,8 +92,8 @@ use yansi::{Paint, Color}; use self::Outcome::*; -/// An enum representing success (`Success`), failure (`Failure`), or -/// forwarding (`Forward`). +/// An enum representing success (`Success`), error (`Error`), or forwarding +/// (`Forward`). /// /// See the [top level documentation](crate::outcome) for detailed information. #[must_use] @@ -102,24 +101,24 @@ use self::Outcome::*; pub enum Outcome { /// Contains the success value. Success(S), - /// Contains the failure error value. - Failure(E), + /// Contains the error error value. + Error(E), /// Contains the value to forward on. Forward(F), } /// Conversion trait from some type into an Outcome type. pub trait IntoOutcome { - /// The type to use when returning an `Outcome::Failure`. - type Failure: Sized; + /// The type to use when returning an `Outcome::Error`. + type Error: Sized; /// The type to use when returning an `Outcome::Forward`. type Forward: Sized; /// Converts `self` into an `Outcome`. If `self` represents a success, an - /// `Outcome::Success` is returned. Otherwise, an `Outcome::Failure` is - /// returned with `failure` as the inner value. - fn into_outcome(self, failure: Self::Failure) -> Outcome; + /// `Outcome::Success` is returned. Otherwise, an `Outcome::Error` is + /// returned with `error` as the inner value. + fn into_outcome(self, error: Self::Error) -> Outcome; /// Converts `self` into an `Outcome`. If `self` represents a success, an /// `Outcome::Success` is returned. Otherwise, an `Outcome::Forward` is @@ -128,14 +127,14 @@ pub trait IntoOutcome { } impl IntoOutcome for Option { - type Failure = E; + type Error = E; type Forward = F; #[inline] - fn into_outcome(self, failure: E) -> Outcome { + fn into_outcome(self, error: E) -> Outcome { match self { Some(val) => Success(val), - None => Failure(failure) + None => Error(error) } } @@ -208,7 +207,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.is_success(), true); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.is_success(), false); /// /// let x: Outcome = Forward(25); @@ -219,7 +218,7 @@ impl Outcome { matches!(self, Success(_)) } - /// Return true if this `Outcome` is a `Failure`. + /// Return true if this `Outcome` is an `Error`. /// /// # Examples /// @@ -228,17 +227,17 @@ impl Outcome { /// # use rocket::outcome::Outcome::*; /// # /// let x: Outcome = Success(10); - /// assert_eq!(x.is_failure(), false); + /// assert_eq!(x.is_error(), false); /// - /// let x: Outcome = Failure("Hi! I'm an error."); - /// assert_eq!(x.is_failure(), true); + /// let x: Outcome = Error("Hi! I'm an error."); + /// assert_eq!(x.is_error(), true); /// /// let x: Outcome = Forward(25); - /// assert_eq!(x.is_failure(), false); + /// assert_eq!(x.is_error(), false); /// ``` #[inline] - pub fn is_failure(&self) -> bool { - matches!(self, Failure(_)) + pub fn is_error(&self) -> bool { + matches!(self, Error(_)) } /// Return true if this `Outcome` is a `Forward`. @@ -252,7 +251,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.is_forward(), false); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.is_forward(), false); /// /// let x: Outcome = Forward(25); @@ -275,7 +274,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.succeeded(), Some(10)); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.succeeded(), None); /// /// let x: Outcome = Forward(25); @@ -291,7 +290,7 @@ impl Outcome { /// Converts from `Outcome` to `Option`. /// - /// Returns the `Some` of the `Failure` if this is a `Failure`, otherwise + /// Returns the `Some` of the `Error` if this is an `Error`, otherwise /// returns `None`. `self` is consumed, and all other values are discarded. /// /// ```rust @@ -301,7 +300,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.failed(), None); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.failed(), Some("Hi! I'm an error.")); /// /// let x: Outcome = Forward(25); @@ -310,7 +309,7 @@ impl Outcome { #[inline] pub fn failed(self) -> Option { match self { - Failure(val) => Some(val), + Error(val) => Some(val), _ => None } } @@ -327,7 +326,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.forwarded(), None); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.forwarded(), None); /// /// let x: Outcome = Forward(25); @@ -355,7 +354,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.success_or(false), Ok(10)); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.success_or(false), Err(false)); /// /// let x: Outcome = Forward(25); @@ -384,7 +383,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.success_or_else(|| false), Ok(10)); /// - /// let x: Outcome = Failure("Hi! I'm an error."); + /// let x: Outcome = Error("Hi! I'm an error."); /// assert_eq!(x.success_or_else(|| false), Err(false)); /// /// let x: Outcome = Forward(25); @@ -407,14 +406,14 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.as_ref(), Success(&10)); /// - /// let x: Outcome = Failure("Hi! I'm an error."); - /// assert_eq!(x.as_ref(), Failure(&"Hi! I'm an error.")); + /// let x: Outcome = Error("Hi! I'm an error."); + /// assert_eq!(x.as_ref(), Error(&"Hi! I'm an error.")); /// ``` #[inline] pub fn as_ref(&self) -> Outcome<&S, &E, &F> { match *self { Success(ref val) => Success(val), - Failure(ref val) => Failure(val), + Error(ref val) => Error(val), Forward(ref val) => Forward(val), } } @@ -436,7 +435,7 @@ impl Outcome { pub fn as_mut(&mut self) -> Outcome<&mut S, &mut E, &mut F> { match *self { Success(ref mut val) => Success(val), - Failure(ref mut val) => Failure(val), + Error(ref mut val) => Error(val), Forward(ref mut val) => Forward(val), } } @@ -458,29 +457,29 @@ impl Outcome { pub fn map T>(self, f: M) -> Outcome { match self { Success(val) => Success(f(val)), - Failure(val) => Failure(val), + Error(val) => Error(val), Forward(val) => Forward(val), } } - /// Maps the `Failure` value using `f`. Maps an `Outcome` to an + /// Maps the `Error` value using `f`. Maps an `Outcome` to an /// `Outcome` by applying the function `f` to the value of type `E` - /// in `self` if `self` is an `Outcome::Failure`. + /// in `self` if `self` is an `Outcome::Error`. /// /// ```rust /// # use rocket::outcome::Outcome; /// # use rocket::outcome::Outcome::*; /// # - /// let x: Outcome = Failure("hi"); + /// let x: Outcome = Error("hi"); /// - /// let mapped = x.map_failure(|v| if v == "hi" { 10 } else { 0 }); - /// assert_eq!(mapped, Failure(10)); + /// let mapped = x.map_error(|v| if v == "hi" { 10 } else { 0 }); + /// assert_eq!(mapped, Error(10)); /// ``` #[inline] - pub fn map_failure T>(self, f: M) -> Outcome { + pub fn map_error T>(self, f: M) -> Outcome { match self { Success(val) => Success(val), - Failure(val) => Failure(f(val)), + Error(val) => Error(f(val)), Forward(val) => Forward(val), } } @@ -502,7 +501,7 @@ impl Outcome { pub fn map_forward T>(self, f: M) -> Outcome { match self { Success(val) => Success(val), - Failure(val) => Failure(val), + Error(val) => Error(val), Forward(val) => Forward(f(val)), } } @@ -523,7 +522,7 @@ impl Outcome { /// let mapped = x.and_then(|v| match v { /// 10 => Success("10"), /// 1 => Forward(false), - /// _ => Failure("30") + /// _ => Error("30") /// }); /// /// assert_eq!(mapped, Success("10")); @@ -532,15 +531,15 @@ impl Outcome { pub fn and_then Outcome>(self, f: M) -> Outcome { match self { Success(val) => f(val), - Failure(val) => Failure(val), + Error(val) => Error(val), Forward(val) => Forward(val), } } /// Converts from `Outcome` to `Outcome` using `f` to map - /// `Failure(E)` to `Failure(T)`. + /// `Error(E)` to `Error(T)`. /// - /// If `self` is not `Failure`, `self` is returned. + /// If `self` is not `Error`, `self` is returned. /// /// # Examples /// @@ -548,21 +547,21 @@ impl Outcome { /// # use rocket::outcome::Outcome; /// # use rocket::outcome::Outcome::*; /// # - /// let x: Outcome = Failure("hi"); + /// let x: Outcome = Error("hi"); /// - /// let mapped = x.failure_then(|v| match v { - /// "hi" => Failure(10), + /// let mapped = x.error_then(|v| match v { + /// "hi" => Error(10), /// "test" => Forward(false), /// _ => Success(10) /// }); /// - /// assert_eq!(mapped, Failure(10)); + /// assert_eq!(mapped, Error(10)); /// ``` #[inline] - pub fn failure_then Outcome>(self, f: M) -> Outcome { + pub fn error_then Outcome>(self, f: M) -> Outcome { match self { Success(val) => Success(val), - Failure(val) => f(val), + Error(val) => f(val), Forward(val) => Forward(val), } } @@ -583,7 +582,7 @@ impl Outcome { /// let mapped = x.forward_then(|v| match v { /// Some(true) => Success(10), /// Some(false) => Forward(20), - /// None => Failure("10") + /// None => Error("10") /// }); /// /// assert_eq!(mapped, Forward(20)); @@ -592,13 +591,13 @@ impl Outcome { pub fn forward_then Outcome>(self, f: M) -> Outcome { match self { Success(val) => Success(val), - Failure(val) => Failure(val), + Error(val) => Error(val), Forward(val) => f(val), } } /// Converts `Outcome` to `Result` by identity mapping - /// `Success(S)` and `Failure(E)` to `Result` and mapping `Forward(F)` + /// `Success(S)` and `Error(E)` to `Result` and mapping `Forward(F)` /// to `Result` using `f`. /// /// ```rust @@ -608,7 +607,7 @@ impl Outcome { /// let x: Outcome = Success(10); /// assert_eq!(x.ok_map_forward(|x| Ok(x as i32 + 1)), Ok(10)); /// - /// let x: Outcome = Failure("hello"); + /// let x: Outcome = Error("hello"); /// assert_eq!(x.ok_map_forward(|x| Ok(x as i32 + 1)), Err("hello")); /// /// let x: Outcome = Forward(0); @@ -620,13 +619,13 @@ impl Outcome { { match self { Outcome::Success(s) => Ok(s), - Outcome::Failure(e) => Err(e), + Outcome::Error(e) => Err(e), Outcome::Forward(v) => f(v), } } /// Converts `Outcome` to `Result` by identity mapping - /// `Success(S)` and `Forward(F)` to `Result` and mapping `Failure(E)` + /// `Success(S)` and `Forward(F)` to `Result` and mapping `Error(E)` /// to `Result` using `f`. /// /// ```rust @@ -634,21 +633,21 @@ impl Outcome { /// # use rocket::outcome::Outcome::*; /// # /// let x: Outcome = Success(10); - /// assert_eq!(x.ok_map_failure(|s| Ok(123)), Ok(10)); + /// assert_eq!(x.ok_map_error(|s| Ok(123)), Ok(10)); /// - /// let x: Outcome = Failure("hello"); - /// assert_eq!(x.ok_map_failure(|s| Ok(123)), Ok(123)); + /// let x: Outcome = Error("hello"); + /// assert_eq!(x.ok_map_error(|s| Ok(123)), Ok(123)); /// /// let x: Outcome = Forward(0); - /// assert_eq!(x.ok_map_failure(|s| Ok(123)), Err(0)); + /// assert_eq!(x.ok_map_error(|s| Ok(123)), Err(0)); /// ``` #[inline] - pub fn ok_map_failure(self, f: M) -> Result + pub fn ok_map_error(self, f: M) -> Result where M: FnOnce(E) -> Result { match self { Outcome::Success(s) => Ok(s), - Outcome::Failure(e) => f(e), + Outcome::Error(e) => f(e), Outcome::Forward(v) => Err(v), } } @@ -657,7 +656,7 @@ impl Outcome { fn formatting(&self) -> (Color, &'static str) { match *self { Success(..) => (Color::Green, "Success"), - Failure(..) => (Color::Red, "Failure"), + Error(..) => (Color::Red, "Error"), Forward(..) => (Color::Yellow, "Forward"), } } @@ -675,7 +674,7 @@ impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome { crate::export! { /// Unwraps a [`Success`](Outcome::Success) or propagates a `Forward` or - /// `Failure`. + /// `Error` by returning early. /// /// # Syntax /// @@ -691,7 +690,7 @@ crate::export! { /// ``` /// /// This is just like `?` (or previously, `try!`), but for `Outcome`. In the - /// case of a `Forward` or `Failure` variant, the inner type is passed to + /// case of a `Forward` or `Error` variant, the inner type is passed to /// [`From`](std::convert::From), allowing for the conversion between /// specific and more general types. The resulting forward/error is /// immediately returned. Because of the early return, `try_outcome!` can @@ -746,8 +745,8 @@ crate::export! { macro_rules! try_outcome { ($expr:expr $(,)?) => (match $expr { $crate::outcome::Outcome::Success(val) => val, - $crate::outcome::Outcome::Failure(e) => { - return $crate::outcome::Outcome::Failure(::std::convert::From::from(e)) + $crate::outcome::Outcome::Error(e) => { + return $crate::outcome::Outcome::Error(::std::convert::From::from(e)) }, $crate::outcome::Outcome::Forward(f) => { return $crate::outcome::Outcome::Forward(::std::convert::From::from(f)) diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 33aa5ef86e..3d048e8541 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -1,3 +1,4 @@ +use std::convert::Infallible; use std::fmt::Debug; use std::net::{IpAddr, SocketAddr}; @@ -12,14 +13,14 @@ use crate::http::uri::{Host, Origin}; pub type Outcome = outcome::Outcome; impl IntoOutcome for Result { - type Failure = Status; + type Error = Status; type Forward = Status; #[inline] fn into_outcome(self, status: Status) -> Outcome { match self { Ok(val) => Success(val), - Err(err) => Failure((status, err)) + Err(err) => Error((status, err)) } } @@ -85,8 +86,8 @@ impl IntoOutcome for Result { /// ``` /// /// Request guards always fire in left-to-right declaration order. In the -/// example above, the order is `a` followed by `b` followed by `c`. Failure is -/// short-circuiting; if one guard fails, the remaining are not attempted. +/// example above, the order is `a` followed by `b` followed by `c`. Errors are +/// short-circuiting; if one guard errors, the remaining are not attempted. /// /// # Outcomes /// @@ -99,12 +100,12 @@ impl IntoOutcome for Result { /// the value for the corresponding parameter. As long as all other guards /// succeed, the request will be handled. /// -/// * **Failure**(Status, E) +/// * **Error**(Status, E) /// -/// If the `Outcome` is [`Failure`], the request will fail with the given +/// If the `Outcome` is [`Error`], the request will fail with the given /// status code and error. The designated error [`Catcher`](crate::Catcher) /// will be used to respond to the request. Note that users can request types -/// of `Result` and `Option` to catch `Failure`s and retrieve the +/// of `Result` and `Option` to catch `Error`s and retrieve the /// error value. /// /// * **Forward**(Status) @@ -191,7 +192,7 @@ impl IntoOutcome for Result { /// /// The type `T` is derived from the incoming request using `T`'s /// `FromRequest` implementation. If derivation is a `Success`, the value is -/// returned in `Ok`. If the derivation is a `Failure`, the error value is +/// returned in `Ok`. If the derivation is an `Error`, the error value is /// returned in `Err`. If the derivation is a `Forward`, the request is /// forwarded with the same status code as the original forward. /// @@ -232,9 +233,9 @@ impl IntoOutcome for Result { /// } /// /// match req.headers().get_one("x-api-key") { -/// None => Outcome::Failure((Status::BadRequest, ApiKeyError::Missing)), +/// None => Outcome::Error((Status::BadRequest, ApiKeyError::Missing)), /// Some(key) if is_valid(key) => Outcome::Success(ApiKey(key)), -/// Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)), +/// Some(_) => Outcome::Error((Status::BadRequest, ApiKeyError::Invalid)), /// } /// } /// } @@ -390,7 +391,7 @@ pub trait FromRequest<'r>: Sized { /// Derives an instance of `Self` from the incoming request metadata. /// /// If the derivation is successful, an outcome of `Success` is returned. If - /// the derivation fails in an unrecoverable fashion, `Failure` is returned. + /// the derivation fails in an unrecoverable fashion, `Error` is returned. /// `Forward` is returned to indicate that the request should be forwarded /// to other matching routes, if any. async fn from_request(request: &'r Request<'_>) -> Outcome; @@ -398,27 +399,27 @@ pub trait FromRequest<'r>: Sized { #[crate::async_trait] impl<'r> FromRequest<'r> for Method { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { Success(request.method()) } } #[crate::async_trait] impl<'r> FromRequest<'r> for &'r Origin<'r> { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { Success(request.uri()) } } #[crate::async_trait] impl<'r> FromRequest<'r> for &'r Host<'r> { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.host() { Some(host) => Success(host), None => Forward(Status::NotFound) @@ -428,9 +429,9 @@ impl<'r> FromRequest<'r> for &'r Host<'r> { #[crate::async_trait] impl<'r> FromRequest<'r> for &'r Route { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.route() { Some(route) => Success(route), None => Forward(Status::InternalServerError) @@ -440,18 +441,18 @@ impl<'r> FromRequest<'r> for &'r Route { #[crate::async_trait] impl<'r> FromRequest<'r> for &'r CookieJar<'r> { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { Success(request.cookies()) } } #[crate::async_trait] impl<'r> FromRequest<'r> for &'r Accept { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.accept() { Some(accept) => Success(accept), None => Forward(Status::NotFound) @@ -461,9 +462,9 @@ impl<'r> FromRequest<'r> for &'r Accept { #[crate::async_trait] impl<'r> FromRequest<'r> for &'r ContentType { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.content_type() { Some(content_type) => Success(content_type), None => Forward(Status::NotFound) @@ -473,9 +474,9 @@ impl<'r> FromRequest<'r> for &'r ContentType { #[crate::async_trait] impl<'r> FromRequest<'r> for IpAddr { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.client_ip() { Some(addr) => Success(addr), None => Forward(Status::NotFound) @@ -485,9 +486,9 @@ impl<'r> FromRequest<'r> for IpAddr { #[crate::async_trait] impl<'r> FromRequest<'r> for SocketAddr { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match request.remote() { Some(addr) => Success(addr), None => Forward(Status::NotFound) @@ -497,12 +498,12 @@ impl<'r> FromRequest<'r> for SocketAddr { #[crate::async_trait] impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match T::from_request(request).await { Success(val) => Success(Ok(val)), - Failure((_, e)) => Success(Err(e)), + Error((_, e)) => Success(Err(e)), Forward(status) => Forward(status), } } @@ -510,21 +511,21 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result { #[crate::async_trait] impl<'r, T: FromRequest<'r>> FromRequest<'r> for Option { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { match T::from_request(request).await { Success(val) => Success(Some(val)), - Failure(_) | Forward(_) => Success(None), + Error(_) | Forward(_) => Success(None), } } } #[crate::async_trait] impl<'r, T: FromRequest<'r>> FromRequest<'r> for Outcome { - type Error = std::convert::Infallible; + type Error = Infallible; - async fn from_request(request: &'r Request<'_>) -> Outcome { + async fn from_request(request: &'r Request<'_>) -> Outcome { Success(T::from_request(request).await) } } diff --git a/core/lib/src/response/responder.rs b/core/lib/src/response/responder.rs index c6fbdd8b26..3c1ba95699 100644 --- a/core/lib/src/response/responder.rs +++ b/core/lib/src/response/responder.rs @@ -111,7 +111,7 @@ use crate::request::Request; /// * An `Ok(Response)` indicates success. The `Response` will be written out /// to the client. /// -/// * An `Err(Status)` indicates failure. The error catcher for `Status` will +/// * An `Err(Status)` indicates an error. The error catcher for `Status` will /// be invoked to generate a response. /// /// # Implementation Tips diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index 9fee0d67a8..d139942b62 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -141,7 +141,7 @@ pub trait Handler: Cloneable + Send + Sync + 'static { /// The variant of `Outcome` returned by the returned `Future` determines /// what Rocket does next. If the return value is a `Success(Response)`, the /// wrapped `Response` is used to respond to the client. If the return value - /// is a `Failure(Status)`, the error catcher for `Status` is invoked to + /// is an `Error(Status)`, the error catcher for `Status` is invoked to /// generate a response. Otherwise, if the return value is `Forward(Data)`, /// the next matching route is attempted. If there are no other matching /// routes, the `404` error catcher is invoked. @@ -172,7 +172,7 @@ impl<'r, 'o: 'r> Outcome<'o> { /// Return the `Outcome` of response to `req` from `responder`. /// /// If the responder returns `Ok`, an outcome of `Success` is returned with - /// the response. If the responder returns `Err`, an outcome of `Failure` is + /// the response. If the responder returns `Err`, an outcome of `Error` is /// returned with the status code. /// /// # Example @@ -188,14 +188,14 @@ impl<'r, 'o: 'r> Outcome<'o> { pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { match responder.respond_to(req) { Ok(response) => Outcome::Success(response), - Err(status) => Outcome::Failure(status) + Err(status) => Outcome::Error(status) } } /// Return the `Outcome` of response to `req` from `responder`. /// /// If the responder returns `Ok`, an outcome of `Success` is returned with - /// the response. If the responder returns `Err`, an outcome of `Failure` is + /// the response. If the responder returns `Err`, an outcome of `Error` is /// returned with the status code. /// /// # Example @@ -214,7 +214,7 @@ impl<'r, 'o: 'r> Outcome<'o> { let responder = result.map_err(crate::response::Debug); match responder.respond_to(req) { Ok(response) => Outcome::Success(response), - Err(status) => Outcome::Failure(status) + Err(status) => Outcome::Error(status) } } @@ -243,8 +243,8 @@ impl<'r, 'o: 'r> Outcome<'o> { } } - /// Return an `Outcome` of `Failure` with the status code `code`. This is - /// equivalent to `Outcome::Failure(code)`. + /// Return an `Outcome` of `Error` with the status code `code`. This is + /// equivalent to `Outcome::Error(code)`. /// /// This method exists to be used during manual routing. /// @@ -255,12 +255,12 @@ impl<'r, 'o: 'r> Outcome<'o> { /// use rocket::http::Status; /// /// fn bad_req_route<'r>(_: &'r Request, _: Data<'r>) -> route::Outcome<'r> { - /// route::Outcome::failure(Status::BadRequest) + /// route::Outcome::error(Status::BadRequest) /// } /// ``` #[inline(always)] - pub fn failure(code: Status) -> Outcome<'r> { - Outcome::Failure(code) + pub fn error(code: Status) -> Outcome<'r> { + Outcome::Error(code) } /// Return an `Outcome` of `Forward` with the data `data`. This is diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 24853d9517..e78275b91b 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -64,7 +64,7 @@ use crate::sentinel::Sentry; /// higher precedence during routing than routes with higher ranks. Thus, routes /// are attempted in ascending rank order. If a higher precedence route returns /// an `Outcome` of `Forward`, the next highest precedence route is attempted, -/// and so on, until a route returns `Success` or `Failure`, or there are no +/// and so on, until a route returns `Success` or `Error`, or there are no /// more routes to try. When all routes have been attempted, Rocket issues a /// `404` error, handled by the appropriate [`Catcher`](crate::Catcher). /// diff --git a/core/lib/src/serde/json.rs b/core/lib/src/serde/json.rs index dfc85a6ba1..a96f629e85 100644 --- a/core/lib/src/serde/json.rs +++ b/core/lib/src/serde/json.rs @@ -201,12 +201,12 @@ impl<'r, T: Deserialize<'r>> FromData<'r> for Json { match Self::from_data(req, data).await { Ok(value) => Outcome::Success(value), Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { - Outcome::Failure((Status::PayloadTooLarge, Error::Io(e))) + Outcome::Error((Status::PayloadTooLarge, Error::Io(e))) }, Err(Error::Parse(s, e)) if e.classify() == serde_json::error::Category::Data => { - Outcome::Failure((Status::UnprocessableEntity, Error::Parse(s, e))) + Outcome::Error((Status::UnprocessableEntity, Error::Parse(s, e))) }, - Err(e) => Outcome::Failure((Status::BadRequest, e)), + Err(e) => Outcome::Error((Status::BadRequest, e)), } } diff --git a/core/lib/src/serde/msgpack.rs b/core/lib/src/serde/msgpack.rs index e77c6aa76f..3b92f06092 100644 --- a/core/lib/src/serde/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -170,15 +170,15 @@ impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack { match Self::from_data(req, data).await { Ok(value) => Outcome::Success(value), Err(Error::InvalidDataRead(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { - Outcome::Failure((Status::PayloadTooLarge, Error::InvalidDataRead(e))) + Outcome::Error((Status::PayloadTooLarge, Error::InvalidDataRead(e))) }, | Err(e@Error::TypeMismatch(_)) | Err(e@Error::OutOfRange) | Err(e@Error::LengthMismatch(_)) => { - Outcome::Failure((Status::UnprocessableEntity, e)) + Outcome::Error((Status::UnprocessableEntity, e)) }, - Err(e) => Outcome::Failure((Status::BadRequest, e)), + Err(e) => Outcome::Error((Status::BadRequest, e)), } } } diff --git a/core/lib/src/serde/uuid.rs b/core/lib/src/serde/uuid.rs index 8e66465710..8c377168c6 100644 --- a/core/lib/src/serde/uuid.rs +++ b/core/lib/src/serde/uuid.rs @@ -79,7 +79,7 @@ use crate::request::FromParam; use crate::form::{self, FromFormField, ValueField}; -/// Error returned on [`FromParam`] or [`FromFormField`] failure. +/// Error returned on [`FromParam`] or [`FromFormField`] failures. /// pub use uuid_::Error; diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index e3836984fd..e874b0ff6b 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -114,7 +114,7 @@ async fn hyper_service_fn( } impl Rocket { - /// Wrapper around `_send_response` to log a success or failure. + /// Wrapper around `_send_response` to log a success or error. #[inline] async fn send_response( &self, @@ -290,12 +290,12 @@ impl Rocket { request._set_method(Method::Get); match self.route(request, data).await { Outcome::Success(response) => response, - Outcome::Failure(status) => self.handle_error(status, request).await, + Outcome::Error(status) => self.handle_error(status, request).await, Outcome::Forward((_, status)) => self.handle_error(status, request).await, } } Outcome::Forward((_, status)) => self.handle_error(status, request).await, - Outcome::Failure(status) => self.handle_error(status, request).await, + Outcome::Error(status) => self.handle_error(status, request).await, }; // Set the cookies. Note that error responses will only include cookies @@ -310,7 +310,7 @@ impl Rocket { /// Tries to find a `Responder` for a given `request`. It does this by /// routing the request and calling the handler for each matching route - /// until one of the handlers returns success or failure, or there are no + /// until one of the handlers returns success or error, or there are no /// additional routes to try (forward). The corresponding outcome for each /// condition is returned. #[inline] @@ -329,14 +329,14 @@ impl Rocket { let name = route.name.as_deref(); let outcome = handle(name, || route.handler.handle(request, data)).await - .unwrap_or(Outcome::Failure(Status::InternalServerError)); + .unwrap_or(Outcome::Error(Status::InternalServerError)); // Check if the request processing completed (Some) or if the // request needs to be forwarded. If it does, continue the loop // (None) to try again. info_!("{} {}", "Outcome:".primary().bold(), outcome); match outcome { - o@Outcome::Success(_) | o@Outcome::Failure(_) => return o, + o@Outcome::Success(_) | o@Outcome::Error(_) => return o, Outcome::Forward(forwarded) => (data, status) = forwarded, } } @@ -380,7 +380,7 @@ impl Rocket { // Invokes the catcher for `status`. Returns the response on success. // - // On catcher failure, the 500 error catcher is attempted. If _that_ fails, + // On catcher error, the 500 error catcher is attempted. If _that_ errors, // the (infallible) default 500 error cather is used. pub(crate) async fn handle_error<'s, 'r: 's>( &'s self, diff --git a/core/lib/src/state.rs b/core/lib/src/state.rs index 6b5edcdf28..9ecf24e173 100644 --- a/core/lib/src/state.rs +++ b/core/lib/src/state.rs @@ -202,7 +202,7 @@ impl<'r, T: Send + Sync + 'static> FromRequest<'r> for &'r State { Some(state) => Outcome::Success(state), None => { error_!("Attempted to retrieve unmanaged state `{}`!", type_name::()); - Outcome::Failure((Status::InternalServerError, ())) + Outcome::Error((Status::InternalServerError, ())) } } } diff --git a/examples/manual-routing/src/main.rs b/examples/manual-routing/src/main.rs index 704b69c705..2da4c42ad2 100644 --- a/examples/manual-routing/src/main.rs +++ b/examples/manual-routing/src/main.rs @@ -38,7 +38,7 @@ fn upload<'r>(req: &'r Request, data: Data<'r>) -> route::BoxFuture<'r> { Box::pin(async move { if !req.content_type().map_or(false, |ct| ct.is_plain()) { println!(" => Content-Type of upload must be text/plain. Ignoring."); - return route::Outcome::failure(Status::BadRequest); + return route::Outcome::error(Status::BadRequest); } let path = req.rocket().config().temp_dir.relative().join("upload.txt"); @@ -49,10 +49,10 @@ fn upload<'r>(req: &'r Request, data: Data<'r>) -> route::BoxFuture<'r> { } println!(" => Failed copying."); - route::Outcome::failure(Status::InternalServerError) + route::Outcome::error(Status::InternalServerError) } else { println!(" => Couldn't open file: {:?}", file.unwrap_err()); - route::Outcome::failure(Status::InternalServerError) + route::Outcome::error(Status::InternalServerError) } }) } diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 1a1a8c637e..56b927cebe 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -263,7 +263,7 @@ would never forward. An `Ok` variant would indicate that `` was a valid ! tip: It's not just forwards that can be caught! In general, when any guard fails for any reason, including parameter guards, - you can use an `Option` or `Result` type in its place to catch the failure. + you can use an `Option` or `Result` type in its place to catch the error. By the way, if you were to omit the `rank` parameter in the `user_str` or `user_int` routes, Rocket would emit an error and abort launch, indicating that @@ -353,7 +353,7 @@ fn index(param: isize, a: A, b: B, c: C) { /* ... */ } ``` Request guards always fire in left-to-right declaration order. In the example -above, the order will be `A` followed by `B` followed by `C`. Failure is +above, the order will be `A` followed by `B` followed by `C`. Errors are short-circuiting; if one guard fails, the remaining are not attempted. To learn more about request guards and implementing them, see the [`FromRequest`] documentation. @@ -484,7 +484,7 @@ The three routes above encode authentication _and_ authorization. The the admin panel displayed. If the user is not an admin, the `AdminUser` guard will forward. Since the `admin_panel_user` route is ranked next highest, it is attempted next. This route succeeds if there is _any_ user signed in, and an -authorization failure message is displayed. Finally, if a user isn't signed in, +authorization error message is displayed. Finally, if a user isn't signed in, the `admin_panel_redirect` route is attempted. Since this route has no guards, it always succeeds. The user is redirected to a log in page. @@ -522,7 +522,7 @@ If the `User` guard forwards or fails, the `Option` will be `None`. If it succeeds, it will be `Some(User)`. For guards that may fail (and not just forward), the `Result` guard allows -retrieving the error type `E` on failure. As an example, when the +retrieving the error type `E` on error. As an example, when the [`mtls::Certificate`] type fails, it reports the reason in an [`mtls::Error`] type. The value can be retrieved in a handler by using a `Result` guard: @@ -876,7 +876,7 @@ fields implement [`FromForm`], or equivalently, [`FromFormField`]. If a `POST /todo` request arrives, the form data will automatically be parsed into the `Task` structure. If the data that arrives isn't of the correct Content-Type, the request is forwarded. If the data doesn't parse or is simply -invalid, a customizable error is returned. As before, a forward or failure can +invalid, a customizable error is returned. As before, a forward or error can be caught by using the `Option` and `Result` types: ```rust diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 2a3889500c..56656d91ac 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -289,7 +289,7 @@ fn handler() -> &'static str { `Option` is a _wrapping_ responder: an `Option` can only be returned when `T` implements `Responder`. If the `Option` is `Some`, the wrapped responder is used -to respond to the client. Otherwise, a error of **404 - Not Found** is returned +to respond to the client. Otherwise, an error of **404 - Not Found** is returned to the client. This implementation makes `Option` a convenient type to return when it is not diff --git a/site/tests/src/lib.rs b/site/tests/src/lib.rs index 33114eee03..2bf0a0b219 100644 --- a/site/tests/src/lib.rs +++ b/site/tests/src/lib.rs @@ -26,7 +26,7 @@ macro_rules! assert_form_parses { Ok(v) => assert_eq!(v, $value, "{}", $form), Err(e) => { eprintln!("form failed to parse\n> form: {:?}\n> error: {:?}", $form, e); - panic!("form parse failure"); + panic!("form parse error"); } } ); From fbd1a0d06905dd359307ed584c46f07df02e2c94 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 31 Oct 2023 18:27:03 -0500 Subject: [PATCH 017/178] Improve forwarding status code precision. Previously, the `NotFound` status code was used to signal many kinds of recoverable, forwarding errors. This included validation errors, incorrect Content-Type errors, and more. This commit modifies the status code used to forward in these instances to more precisely indicate the forwarding condition. In particular: * Parameter `FromParam` errors now forward as 422 (`UnprocessableEntity`). * Query paramater errors now forward as 422 (`UnprocessableEntity`). * Use of incorrect form content-type forwards as 413 (`UnsupportedMediaType`). * `WebSocket` guard now forwards as 400 (`BadRequest`). * `&Host`, `&Accept`, `&ContentType`, `IpAddr`, and `SocketAddr` all forward with a 500 (`InternalServerError`). Additionally, the `IntoOutcome` trait was overhauled to support functionality previously offered by methods on `Outcome`. The `Outcome::forward()` method now requires a status code to use for the forwarding outcome. Finally, logging of `Outcome`s now includes the relevant status code. Resolves #2626. --- contrib/sync_db_pools/lib/src/connection.rs | 2 +- contrib/ws/src/websocket.rs | 4 +- core/codegen/src/attribute/route/mod.rs | 4 +- core/codegen/tests/route.rs | 2 +- core/lib/src/data/data.rs | 2 +- core/lib/src/data/from_data.rs | 27 +-- core/lib/src/form/parser.rs | 2 +- core/lib/src/fs/server.rs | 28 +-- core/lib/src/fs/temp_file.rs | 2 +- core/lib/src/mtls.rs | 2 +- core/lib/src/outcome.rs | 202 +++++++++++++++----- core/lib/src/request/from_request.rs | 56 ++---- core/lib/src/response/flash.rs | 2 +- core/lib/src/route/handler.rs | 36 +--- core/lib/src/server.rs | 2 +- examples/error-handling/src/tests.rs | 20 +- examples/hello/src/tests.rs | 4 +- examples/manual-routing/src/main.rs | 4 +- examples/serialization/src/tests.rs | 5 +- 19 files changed, 233 insertions(+), 173 deletions(-) diff --git a/contrib/sync_db_pools/lib/src/connection.rs b/contrib/sync_db_pools/lib/src/connection.rs index c395dc2866..73c6913b48 100644 --- a/contrib/sync_db_pools/lib/src/connection.rs +++ b/contrib/sync_db_pools/lib/src/connection.rs @@ -210,7 +210,7 @@ impl<'r, K: 'static, C: Poolable> FromRequest<'r> for Connection { #[inline] async fn from_request(request: &'r Request<'_>) -> Outcome { match request.rocket().state::>() { - Some(c) => c.get().await.into_outcome((Status::ServiceUnavailable, ())), + Some(c) => c.get().await.or_error((Status::ServiceUnavailable, ())), None => { error_!("Missing database fairing for `{}`", std::any::type_name::()); Outcome::Error((Status::InternalServerError, ())) diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index 6ad295ac85..32b598e818 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -30,7 +30,7 @@ use crate::result::{Result, Error}; /// ### Forwarding /// /// If the incoming request is not a valid WebSocket request, the guard -/// forwards. The guard never fails. +/// forwards with a status of `BadRequest`. The guard never fails. pub struct WebSocket { config: Config, key: String, @@ -203,7 +203,7 @@ impl<'r> FromRequest<'r> for WebSocket { let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); match key { Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)), - Some(_) | None => Outcome::Forward(Status::NotFound) + Some(_) | None => Outcome::Forward(Status::BadRequest) } } } diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index e30117db00..c2e3043ab6 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -105,7 +105,7 @@ fn query_decls(route: &Route) -> Option { if !__e.is_empty() { #_log::warn_!("Query string failed to match route declaration."); for _err in __e { #_log::warn_!("{}", _err); } - return #Outcome::Forward((#__data, #Status::NotFound)); + return #Outcome::Forward((#__data, #Status::UnprocessableEntity)); } (#(#ident.unwrap()),*) @@ -146,7 +146,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { #_log::warn_!("Parameter guard `{}: {}` is forwarding: {:?}.", #name, stringify!(#ty), __error); - #Outcome::Forward((#__data, #Status::NotFound)) + #Outcome::Forward((#__data, #Status::UnprocessableEntity)) }); // All dynamic parameters should be found if this function is being called; diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index d8e4509418..de65f2fa65 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -269,7 +269,7 @@ fn test_query_collection() { let colors = &["red"]; let dog = &["name=Fido"]; - assert_eq!(run(&client, colors, dog).0, Status::NotFound); + assert_eq!(run(&client, colors, dog).0, Status::UnprocessableEntity); let colors = &["red"]; let dog = &["name=Fido", "age=2"]; diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs index 3f393c5598..bbd2bb75a4 100644 --- a/core/lib/src/data/data.rs +++ b/core/lib/src/data/data.rs @@ -112,7 +112,7 @@ impl<'r> Data<'r> { /// /// async fn from_data(r: &'r Request<'_>, mut data: Data<'r>) -> Outcome<'r, Self> { /// if data.peek(2).await != b"hi" { - /// return Outcome::Forward((data, Status::NotFound)) + /// return Outcome::Forward((data, Status::BadRequest)) /// } /// /// /* .. */ diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index b3ac98c816..3eec28932d 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -9,27 +9,6 @@ use crate::outcome::{self, IntoOutcome, try_outcome, Outcome::*}; pub type Outcome<'r, T, E = >::Error> = outcome::Outcome, Status)>; -impl<'r, S, E> IntoOutcome, Status)> for Result { - type Error = Status; - type Forward = (Data<'r>, Status); - - #[inline] - fn into_outcome(self, status: Status) -> Outcome<'r, S, E> { - match self { - Ok(val) => Success(val), - Err(err) => Error((status, err)) - } - } - - #[inline] - fn or_forward(self, (data, status): (Data<'r>, Status)) -> Outcome<'r, S, E> { - match self { - Ok(val) => Success(val), - Err(_) => Forward((data, status)) - } - } -} - /// Trait implemented by data guards to derive a value from request body data. /// /// # Data Guards @@ -271,7 +250,7 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// // Ensure the content type is correct before opening the data. /// let person_ct = ContentType::new("application", "x-person"); /// if req.content_type() != Some(&person_ct) { -/// return Outcome::Forward((data, Status::NotFound)); +/// return Outcome::Forward((data, Status::UnsupportedMediaType)); /// } /// /// // Use a configured limit with name 'person' or fallback to default. @@ -343,7 +322,7 @@ impl<'r> FromData<'r> for Capped { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { let limit = req.limits().get("string").unwrap_or(Limits::STRING); - data.open(limit).into_string().await.into_outcome(Status::BadRequest) + data.open(limit).into_string().await.or_error(Status::BadRequest) } } @@ -406,7 +385,7 @@ impl<'r> FromData<'r> for Capped> { async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { let limit = req.limits().get("bytes").unwrap_or(Limits::BYTES); - data.open(limit).into_bytes().await.into_outcome(Status::BadRequest) + data.open(limit).into_bytes().await.or_error(Status::BadRequest) } } diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index 7a9c4437be..b53e2a5008 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -35,7 +35,7 @@ impl<'r, 'i> Parser<'r, 'i> { let parser = match req.content_type() { Some(c) if c.is_form() => Self::from_form(req, data).await, Some(c) if c.is_form_data() => Self::from_multipart(req, data).await, - _ => return Outcome::Forward((data, Status::NotFound)), + _ => return Outcome::Forward((data, Status::UnsupportedMediaType)), }; match parser { diff --git a/core/lib/src/fs/server.rs b/core/lib/src/fs/server.rs index 8aac5c259b..45300db592 100644 --- a/core/lib/src/fs/server.rs +++ b/core/lib/src/fs/server.rs @@ -1,9 +1,10 @@ use std::path::{PathBuf, Path}; use crate::{Request, Data}; -use crate::http::{Method, uri::Segments, ext::IntoOwned}; +use crate::http::{Method, Status, uri::Segments, ext::IntoOwned}; use crate::route::{Route, Handler, Outcome}; -use crate::response::Redirect; +use crate::response::{Redirect, Responder}; +use crate::outcome::IntoOutcome; use crate::fs::NamedFile; /// Custom handler for serving static files. @@ -203,10 +204,10 @@ impl Handler for FileServer { }; if segments.is_empty() { - let file = NamedFile::open(&self.root).await.ok(); - return Outcome::from_or_forward(req, data, file); + let file = NamedFile::open(&self.root).await; + return file.respond_to(req).or_forward((data, Status::NotFound)); } else { - return Outcome::forward(data); + return Outcome::forward(data, Status::NotFound); } } @@ -224,18 +225,23 @@ impl Handler for FileServer { .expect("adding a trailing slash to a known good path => valid path") .into_owned(); - return Outcome::from_or_forward(req, data, Redirect::permanent(normal)); + return Redirect::permanent(normal) + .respond_to(req) + .or_forward((data, Status::InternalServerError)); } if !options.contains(Options::Index) { - return Outcome::forward(data); + return Outcome::forward(data, Status::NotFound); } - let index = NamedFile::open(p.join("index.html")).await.ok(); - Outcome::from_or_forward(req, data, index) + let index = NamedFile::open(p.join("index.html")).await; + index.respond_to(req).or_forward((data, Status::NotFound)) }, - Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()), - None => Outcome::forward(data), + Some(p) => { + let file = NamedFile::open(p).await; + file.respond_to(req).or_forward((data, Status::NotFound)) + } + None => Outcome::forward(data, Status::NotFound), } } } diff --git a/core/lib/src/fs/temp_file.rs b/core/lib/src/fs/temp_file.rs index 0f0ca9f5d0..fe969e7e9c 100644 --- a/core/lib/src/fs/temp_file.rs +++ b/core/lib/src/fs/temp_file.rs @@ -543,7 +543,7 @@ impl<'r> FromData<'r> for Capped> { } TempFile::from(req, data, None, req.content_type().cloned()).await - .into_outcome(Status::BadRequest) + .or_error(Status::BadRequest) } } diff --git a/core/lib/src/mtls.rs b/core/lib/src/mtls.rs index 07367bcf30..5910bb520c 100644 --- a/core/lib/src/mtls.rs +++ b/core/lib/src/mtls.rs @@ -20,6 +20,6 @@ impl<'r> FromRequest<'r> for Certificate<'r> { async fn from_request(req: &'r Request<'_>) -> Outcome { let certs = req.connection.client_certificates.as_ref().or_forward(Status::Unauthorized); let data = try_outcome!(try_outcome!(certs).chain_data().or_forward(Status::Unauthorized)); - Certificate::parse(data).into_outcome(Status::Unauthorized) + Certificate::parse(data).or_error(Status::Unauthorized) } } diff --git a/core/lib/src/outcome.rs b/core/lib/src/outcome.rs index 549e850910..88828b364c 100644 --- a/core/lib/src/outcome.rs +++ b/core/lib/src/outcome.rs @@ -90,6 +90,10 @@ use std::fmt; use yansi::{Paint, Color}; +use crate::{route, request, response}; +use crate::data::{self, Data, FromData}; +use crate::http::Status; + use self::Outcome::*; /// An enum representing success (`Success`), error (`Error`), or forwarding @@ -107,46 +111,6 @@ pub enum Outcome { Forward(F), } -/// Conversion trait from some type into an Outcome type. -pub trait IntoOutcome { - /// The type to use when returning an `Outcome::Error`. - type Error: Sized; - - /// The type to use when returning an `Outcome::Forward`. - type Forward: Sized; - - /// Converts `self` into an `Outcome`. If `self` represents a success, an - /// `Outcome::Success` is returned. Otherwise, an `Outcome::Error` is - /// returned with `error` as the inner value. - fn into_outcome(self, error: Self::Error) -> Outcome; - - /// Converts `self` into an `Outcome`. If `self` represents a success, an - /// `Outcome::Success` is returned. Otherwise, an `Outcome::Forward` is - /// returned with `forward` as the inner value. - fn or_forward(self, forward: Self::Forward) -> Outcome; -} - -impl IntoOutcome for Option { - type Error = E; - type Forward = F; - - #[inline] - fn into_outcome(self, error: E) -> Outcome { - match self { - Some(val) => Success(val), - None => Error(error) - } - } - - #[inline] - fn or_forward(self, forward: F) -> Outcome { - match self { - Some(val) => Success(val), - None => Forward(forward) - } - } -} - impl Outcome { /// Unwraps the Outcome, yielding the contents of a Success. /// @@ -651,15 +615,6 @@ impl Outcome { Outcome::Forward(v) => Err(v), } } - - #[inline] - fn formatting(&self) -> (Color, &'static str) { - match *self { - Success(..) => (Color::Green, "Success"), - Error(..) => (Color::Red, "Error"), - Forward(..) => (Color::Yellow, "Forward"), - } - } } impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome { @@ -755,15 +710,158 @@ crate::export! { } } +impl Outcome { + #[inline] + fn dbg_str(&self) -> &'static str { + match self { + Success(..) => "Success", + Error(..) => "Error", + Forward(..) => "Forward", + } + } + + #[inline] + fn color(&self) -> Color { + match self { + Success(..) => Color::Green, + Error(..) => Color::Red, + Forward(..) => Color::Yellow, + } + } +} + +pub(crate) struct Display<'a, 'r>(&'a route::Outcome<'r>); + +impl<'r> route::Outcome<'r> { + pub(crate) fn log_display(&self) -> Display<'_, 'r> { + impl fmt::Display for Display<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", "Outcome: ".primary().bold())?; + + let color = self.0.color(); + match self.0 { + Success(r) => write!(f, "{}({})", "Success".paint(color), r.status().primary()), + Error(s) => write!(f, "{}({})", "Error".paint(color), s.primary()), + Forward((_, s)) => write!(f, "{}({})", "Forward".paint(color), s.primary()), + } + } + } + + Display(self) + } +} + impl fmt::Debug for Outcome { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Outcome::{}", self.formatting().1) + write!(f, "Outcome::{}", self.dbg_str()) } } impl fmt::Display for Outcome { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (color, string) = self.formatting(); - write!(f, "{}", string.paint(color)) + write!(f, "{}", self.dbg_str().paint(self.color())) + } +} + +/// Conversion trait from some type into an Outcome type. +pub trait IntoOutcome { + /// The type to use when returning an `Outcome::Error`. + type Error: Sized; + + /// The type to use when returning an `Outcome::Forward`. + type Forward: Sized; + + /// Converts `self` into an `Outcome`. If `self` represents a success, an + /// `Outcome::Success` is returned. Otherwise, an `Outcome::Error` is + /// returned with `error` as the inner value. + fn or_error(self, error: Self::Error) -> Outcome; + + /// Converts `self` into an `Outcome`. If `self` represents a success, an + /// `Outcome::Success` is returned. Otherwise, an `Outcome::Forward` is + /// returned with `forward` as the inner value. + fn or_forward(self, forward: Self::Forward) -> Outcome; +} + +impl IntoOutcome> for Option { + type Error = E; + type Forward = F; + + #[inline] + fn or_error(self, error: E) -> Outcome { + match self { + Some(val) => Success(val), + None => Error(error) + } + } + + #[inline] + fn or_forward(self, forward: F) -> Outcome { + match self { + Some(val) => Success(val), + None => Forward(forward) + } + } +} + +impl<'r, T: FromData<'r>> IntoOutcome> for Result { + type Error = Status; + type Forward = (Data<'r>, Status); + + #[inline] + fn or_error(self, error: Status) -> data::Outcome<'r, T> { + match self { + Ok(val) => Success(val), + Err(err) => Error((error, err)) + } + } + + #[inline] + fn or_forward(self, (data, forward): (Data<'r>, Status)) -> data::Outcome<'r, T> { + match self { + Ok(val) => Success(val), + Err(_) => Forward((data, forward)) + } + } +} + +impl IntoOutcome> for Result { + type Error = Status; + type Forward = Status; + + #[inline] + fn or_error(self, error: Status) -> request::Outcome { + match self { + Ok(val) => Success(val), + Err(err) => Error((error, err)) + } + } + + #[inline] + fn or_forward(self, status: Status) -> request::Outcome { + match self { + Ok(val) => Success(val), + Err(_) => Forward(status) + } + } +} + +impl<'r, 'o: 'r> IntoOutcome> for response::Result<'o> { + type Error = (); + type Forward = (Data<'r>, Status); + + #[inline] + fn or_error(self, _: ()) -> route::Outcome<'r> { + match self { + Ok(val) => Success(val), + Err(status) => Error(status), + } + } + + #[inline] + fn or_forward(self, (data, forward): (Data<'r>, Status)) -> route::Outcome<'r> { + match self { + Ok(val) => Success(val), + Err(_) => Forward((data, forward)) + } } } diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 3d048e8541..203c7d490f 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -3,36 +3,14 @@ use std::fmt::Debug; use std::net::{IpAddr, SocketAddr}; use crate::{Request, Route}; -use crate::outcome::{self, IntoOutcome}; -use crate::outcome::Outcome::*; +use crate::outcome::{self, Outcome::*}; -use crate::http::{Status, ContentType, Accept, Method, CookieJar}; use crate::http::uri::{Host, Origin}; +use crate::http::{Status, ContentType, Accept, Method, CookieJar}; /// Type alias for the `Outcome` of a `FromRequest` conversion. pub type Outcome = outcome::Outcome; -impl IntoOutcome for Result { - type Error = Status; - type Forward = Status; - - #[inline] - fn into_outcome(self, status: Status) -> Outcome { - match self { - Ok(val) => Success(val), - Err(err) => Error((status, err)) - } - } - - #[inline] - fn or_forward(self, status: Status) -> Outcome { - match self { - Ok(val) => Success(val), - Err(_) => Forward(status) - } - } -} - /// Trait implemented by request guards to derive a value from incoming /// requests. /// @@ -136,7 +114,8 @@ impl IntoOutcome for Result { /// * **&Host** /// /// Extracts the [`Host`] from the incoming request, if it exists. See -/// [`Request::host()`] for details. +/// [`Request::host()`] for details. If it does not exist, the request is +/// forwarded with a 500 Internal Server Error status. /// /// * **&Route** /// @@ -162,23 +141,30 @@ impl IntoOutcome for Result { /// /// _This implementation always returns successfully._ /// -/// * **ContentType** +/// * **&ContentType** /// /// Extracts the [`ContentType`] from the incoming request via /// [`Request::content_type()`]. If the request didn't specify a -/// Content-Type, the request is forwarded with a 404 Not Found status. +/// Content-Type, the request is forwarded with a 500 Internal Server Error +/// status. +/// +/// * **&ContentType** /// -/// * **IpAddr** +/// Extracts the [`Accept`] from the incoming request via +/// [`Request::accept()`]. If the request didn't specify an `Accept`, the +/// request is forwarded with a 500 Internal Server Error status. +/// +/// * ***IpAddr** /// /// Extracts the client ip address of the incoming request as an [`IpAddr`] /// via [`Request::client_ip()`]. If the client's IP address is not known, -/// the request is forwarded with a 404 Not Found status. +/// the request is forwarded with a 500 Internal Server Error status. /// /// * **SocketAddr** /// /// Extracts the remote address of the incoming request as a [`SocketAddr`] /// via [`Request::remote()`]. If the remote address is not known, the -/// request is forwarded with a 404 Not Found status. +/// request is forwarded with a 500 Internal Server Error status. /// /// * **Option<T>** _where_ **T: FromRequest** /// @@ -422,7 +408,7 @@ impl<'r> FromRequest<'r> for &'r Host<'r> { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.host() { Some(host) => Success(host), - None => Forward(Status::NotFound) + None => Forward(Status::InternalServerError) } } } @@ -455,7 +441,7 @@ impl<'r> FromRequest<'r> for &'r Accept { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.accept() { Some(accept) => Success(accept), - None => Forward(Status::NotFound) + None => Forward(Status::InternalServerError) } } } @@ -467,7 +453,7 @@ impl<'r> FromRequest<'r> for &'r ContentType { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.content_type() { Some(content_type) => Success(content_type), - None => Forward(Status::NotFound) + None => Forward(Status::InternalServerError) } } } @@ -479,7 +465,7 @@ impl<'r> FromRequest<'r> for IpAddr { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.client_ip() { Some(addr) => Success(addr), - None => Forward(Status::NotFound) + None => Forward(Status::InternalServerError) } } } @@ -491,7 +477,7 @@ impl<'r> FromRequest<'r> for SocketAddr { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.remote() { Some(addr) => Success(addr), - None => Forward(Status::NotFound) + None => Forward(Status::InternalServerError) } } } diff --git a/core/lib/src/response/flash.rs b/core/lib/src/response/flash.rs index 187223ab4f..688e6d0e2b 100644 --- a/core/lib/src/response/flash.rs +++ b/core/lib/src/response/flash.rs @@ -259,7 +259,7 @@ impl<'r> FromRequest<'r> for FlashMessage<'r> { Ok(i) if i <= kv.len() => Ok(Flash::named(&kv[..i], &kv[i..], req)), _ => Err(()) } - }).into_outcome(Status::BadRequest) + }).or_error(Status::BadRequest) } } diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index d139942b62..e29be6d570 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -218,31 +218,6 @@ impl<'r, 'o: 'r> Outcome<'o> { } } - /// Return the `Outcome` of response to `req` from `responder`. - /// - /// If the responder returns `Ok`, an outcome of `Success` is returned with - /// the response. If the responder returns `Err`, an outcome of `Forward` - /// with a status of `404 Not Found` is returned. - /// - /// # Example - /// - /// ```rust - /// use rocket::{Request, Data, route}; - /// - /// fn str_responder<'r>(req: &'r Request, data: Data<'r>) -> route::Outcome<'r> { - /// route::Outcome::from_or_forward(req, data, "Hello, world!") - /// } - /// ``` - #[inline] - pub fn from_or_forward(req: &'r Request<'_>, data: Data<'r>, responder: R) -> Outcome<'r> - where R: Responder<'r, 'o> - { - match responder.respond_to(req) { - Ok(response) => Outcome::Success(response), - Err(_) => Outcome::Forward((data, Status::NotFound)) - } - } - /// Return an `Outcome` of `Error` with the status code `code`. This is /// equivalent to `Outcome::Error(code)`. /// @@ -263,8 +238,8 @@ impl<'r, 'o: 'r> Outcome<'o> { Outcome::Error(code) } - /// Return an `Outcome` of `Forward` with the data `data`. This is - /// equivalent to `Outcome::Forward((data, Status::NotFound))`. + /// Return an `Outcome` of `Forward` with the data `data` and status + /// `status`. This is equivalent to `Outcome::Forward((data, status))`. /// /// This method exists to be used during manual routing. /// @@ -272,14 +247,15 @@ impl<'r, 'o: 'r> Outcome<'o> { /// /// ```rust /// use rocket::{Request, Data, route}; + /// use rocket::http::Status; /// /// fn always_forward<'r>(_: &'r Request, data: Data<'r>) -> route::Outcome<'r> { - /// route::Outcome::forward(data) + /// route::Outcome::forward(data, Status::InternalServerError) /// } /// ``` #[inline(always)] - pub fn forward(data: Data<'r>) -> Outcome<'r> { - Outcome::Forward((data, Status::NotFound)) + pub fn forward(data: Data<'r>, status: Status) -> Outcome<'r> { + Outcome::Forward((data, status)) } } diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index e874b0ff6b..800c9dbe63 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -334,7 +334,7 @@ impl Rocket { // Check if the request processing completed (Some) or if the // request needs to be forwarded. If it does, continue the loop // (None) to try again. - info_!("{} {}", "Outcome:".primary().bold(), outcome); + info_!("{}", outcome.log_display()); match outcome { o@Outcome::Success(_) | o@Outcome::Error(_) => return o, Outcome::Forward(forwarded) => (data, status) = forwarded, diff --git a/examples/error-handling/src/tests.rs b/examples/error-handling/src/tests.rs index db5ac18d7f..585913e148 100644 --- a/examples/error-handling/src/tests.rs +++ b/examples/error-handling/src/tests.rs @@ -46,7 +46,15 @@ fn forced_error() { fn test_hello_invalid_age() { let client = Client::tracked(super::rocket()).unwrap(); - for path in &["Ford/-129", "Trillian/128", "foo/bar/baz"] { + for path in &["Ford/-129", "Trillian/128"] { + let request = client.get(format!("/hello/{}", path)); + let expected = super::default_catcher(Status::UnprocessableEntity, request.inner()); + let response = request.dispatch(); + assert_eq!(response.status(), Status::UnprocessableEntity); + assert_eq!(response.into_string().unwrap(), expected.1); + } + + for path in &["foo/bar/baz"] { let request = client.get(format!("/hello/{}", path)); let expected = super::hello_not_found(request.inner()); let response = request.dispatch(); @@ -59,7 +67,15 @@ fn test_hello_invalid_age() { fn test_hello_sergio() { let client = Client::tracked(super::rocket()).unwrap(); - for path in &["oops", "-129", "foo/bar", "/foo/bar/baz"] { + for path in &["oops", "-129"] { + let request = client.get(format!("/hello/Sergio/{}", path)); + let expected = super::sergio_error(); + let response = request.dispatch(); + assert_eq!(response.status(), Status::UnprocessableEntity); + assert_eq!(response.into_string().unwrap(), expected); + } + + for path in &["foo/bar", "/foo/bar/baz"] { let request = client.get(format!("/hello/Sergio/{}", path)); let expected = super::sergio_error(); let response = request.dispatch(); diff --git a/examples/hello/src/tests.rs b/examples/hello/src/tests.rs index fd5b628d96..130ff0f31d 100644 --- a/examples/hello/src/tests.rs +++ b/examples/hello/src/tests.rs @@ -62,10 +62,10 @@ fn wave() { let response = client.get(uri).dispatch(); assert_eq!(response.into_string().unwrap(), expected); - for bad_age in &["1000", "-1", "bird", "?"] { + for bad_age in &["1000", "-1", "bird"] { let bad_uri = format!("/wave/{}/{}", name, bad_age); let response = client.get(bad_uri).dispatch(); - assert_eq!(response.status(), Status::NotFound); + assert_eq!(response.status(), Status::UnprocessableEntity); } } } diff --git a/examples/manual-routing/src/main.rs b/examples/manual-routing/src/main.rs index 2da4c42ad2..e4a21620f0 100644 --- a/examples/manual-routing/src/main.rs +++ b/examples/manual-routing/src/main.rs @@ -9,7 +9,7 @@ use rocket::outcome::{try_outcome, IntoOutcome}; use rocket::tokio::fs::File; fn forward<'r>(_req: &'r Request, data: Data<'r>) -> route::BoxFuture<'r> { - Box::pin(async move { route::Outcome::forward(data) }) + Box::pin(async move { route::Outcome::forward(data, Status::NotFound) }) } fn hi<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { @@ -27,7 +27,7 @@ fn name<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { fn echo_url<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { let param_outcome = req.param::<&str>(1) .and_then(Result::ok) - .into_outcome(Status::BadRequest); + .or_error(Status::BadRequest); Box::pin(async move { route::Outcome::from(req, try_outcome!(param_outcome)) diff --git a/examples/serialization/src/tests.rs b/examples/serialization/src/tests.rs index 1a7846ba11..8a46c13dcc 100644 --- a/examples/serialization/src/tests.rs +++ b/examples/serialization/src/tests.rs @@ -34,8 +34,7 @@ fn json_bad_get_put() { // Try to get a message with an invalid ID. let res = client.get("/json/hi").header(ContentType::JSON).dispatch(); - assert_eq!(res.status(), Status::NotFound); - assert!(res.into_string().unwrap().contains("error")); + assert_eq!(res.status(), Status::UnprocessableEntity); // Try to put a message without a proper body. let res = client.put("/json/80").header(ContentType::JSON).dispatch(); @@ -134,5 +133,5 @@ fn uuid() { } let res = client.get("/people/not-a-uuid").dispatch(); - assert_eq!(res.status(), Status::NotFound); + assert_eq!(res.status(), Status::UnprocessableEntity); } From 48d1b82e840348842cc0d089428a02ba9b982bef Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 01:16:28 -0500 Subject: [PATCH 018/178] Qualm various emerging unused warnings. --- core/codegen/src/attribute/route/mod.rs | 1 + core/http/src/tls/mtls.rs | 1 - examples/cookies/src/message.rs | 2 +- examples/cookies/src/session.rs | 2 +- examples/forms/src/main.rs | 2 ++ examples/upgrade/src/main.rs | 3 ++- 6 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index c2e3043ab6..3b72f470a7 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -225,6 +225,7 @@ fn internal_uri_macro_decl(route: &Route) -> TokenStream { } #[doc(hidden)] + #[allow(unused)] pub use #inner_macro_name as #macro_name; } } diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 45ea38fc48..7a7cd1697c 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -4,7 +4,6 @@ pub mod oid { //! [`der-parser`](https://docs.rs/der-parser/7). pub use x509_parser::oid_registry::*; - pub use x509_parser::der_parser::oid::*; pub use x509_parser::objects::*; } diff --git a/examples/cookies/src/message.rs b/examples/cookies/src/message.rs index d4896adbf6..06a33a0085 100644 --- a/examples/cookies/src/message.rs +++ b/examples/cookies/src/message.rs @@ -1,6 +1,6 @@ use rocket::form::Form; use rocket::response::Redirect; -use rocket::http::{Cookie, CookieJar}; +use rocket::http::CookieJar; use rocket_dyn_templates::{Template, context}; #[macro_export] diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index d66a5911aa..11bf55d149 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -1,7 +1,7 @@ use rocket::outcome::IntoOutcome; use rocket::request::{self, FlashMessage, FromRequest, Request}; use rocket::response::{Redirect, Flash}; -use rocket::http::{Cookie, CookieJar, Status}; +use rocket::http::{CookieJar, Status}; use rocket::form::Form; use rocket_dyn_templates::{Template, context}; diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 97c13e728b..a908eb4705 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -11,7 +11,9 @@ use rocket_dyn_templates::Template; struct Password<'v> { #[field(validate = len(6..))] #[field(validate = eq(self.second))] + #[allow(unused)] first: &'v str, + #[allow(unused)] #[field(validate = eq(self.first))] second: &'v str, } diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index 943ff07098..7526b5b155 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -16,7 +16,8 @@ fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { fn echo_channel(ws: ws::WebSocket) -> ws::Channel<'static> { // This is entirely optional. Change default configuration. let ws = ws.config(ws::Config { - max_send_queue: Some(5), + // set max message size to 3MiB + max_message_size: Some(3 << 20), ..Default::default() }); From fa0c778276b381d26da1e8807a6de7a3427a3262 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 12:08:26 -0500 Subject: [PATCH 019/178] Set 'SameSite' to 'Lax' on removal cookies. This avoids needless warnings from certain browsers. --- core/lib/src/cookies.rs | 75 ++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 3e7df583e1..368172cd61 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -350,17 +350,24 @@ impl<'a> CookieJar<'a> { self.ops.lock().push(Op::Add(cookie, true)); } - /// Removes `cookie` from this collection and generates a "removal" cookies + /// Removes `cookie` from this collection and generates a "removal" cookie /// to send to the client on response. A "removal" cookie is a cookie that /// has the same name as the original cookie but has an empty value, a /// max-age of 0, and an expiration date far in the past. /// - /// **Note: For correctness, `cookie` must contain the same `path` and - /// `domain` as the cookie that was initially set. Failure to provide the - /// initial `path` and `domain` will result in cookies that are not properly - /// removed. For convenience, if a path is not set on `cookie`, the `"/"` - /// path will automatically be set.** + /// **For successful removal, `cookie` must contain the same `path` and + /// `domain` as the cookie that was originally set. The cookie will fail to + /// be deleted if any other `path` and `domain` are provided. For + /// convenience, a path of `"/"` is automatically set when one is not + /// specified.** The full list of defaults when corresponding values aren't + /// specified is: /// + /// * `path`: `"/"` + /// * `SameSite`: `Lax` + /// + /// Note: a default setting of `Lax` for `SameSite` carries no + /// security implications: the removal cookie has expired, so it is never + /// transferred to any origin. /// /// # Example /// @@ -370,47 +377,62 @@ impl<'a> CookieJar<'a> { /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { - /// // Rocket will set `path` to `/`. + /// // `path` and `SameSite` are set to defaults (`/` and `Lax`) /// jar.remove("name"); /// /// // Use a custom-built cookie to set a custom path. /// jar.remove(Cookie::build("name").path("/login")); + /// + /// // Use a custom-built cookie to set a custom path and domain. + /// jar.remove(Cookie::build("id").path("/guide").domain("rocket.rs")); /// } /// ``` pub fn remove>>(&self, cookie: C) { let mut cookie = cookie.into(); - if cookie.path().is_none() { - cookie.set_path("/"); - } - + Self::set_removal_defaults(&mut cookie); self.ops.lock().push(Op::Remove(cookie, false)); } /// Removes the private `cookie` from the collection. /// - /// For correct removal, the passed in `cookie` must contain the same `path` - /// and `domain` as the cookie that was initially set. If a path is not set - /// on `cookie`, the `"/"` path will automatically be set. + /// **For successful removal, `cookie` must contain the same `path` and + /// `domain` as the cookie that was originally set. The cookie will fail to + /// be deleted if any other `path` and `domain` are provided. For + /// convenience, a path of `"/"` is automatically set when one is not + /// specified.** The full list of defaults when corresponding values aren't + /// specified is: + /// + /// * `path`: `"/"` + /// * `SameSite`: `Lax` + /// + /// Note: a default setting of `Lax` for `SameSite` carries no + /// security implications: the removal cookie has expired, so it is never + /// transferred to any origin. /// /// # Example /// /// ```rust /// # #[macro_use] extern crate rocket; - /// use rocket::http::CookieJar; + /// use rocket::http::{CookieJar, Cookie}; /// /// #[get("/")] /// fn handler(jar: &CookieJar<'_>) { + /// // `path` and `SameSite` are set to defaults (`/` and `Lax`) /// jar.remove_private("name"); + /// + /// // Use a custom-built cookie to set a custom path. + /// jar.remove_private(Cookie::build("name").path("/login")); + /// + /// // Use a custom-built cookie to set a custom path and domain. + /// let cookie = Cookie::build("id").path("/guide").domain("rocket.rs"); + /// jar.remove_private(cookie); /// } /// ``` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub fn remove_private>>(&self, cookie: C) { let mut cookie = cookie.into(); - if cookie.path().is_none() { - cookie.set_path("/"); - } - + Self::set_removal_defaults(&mut cookie); self.ops.lock().push(Op::Remove(cookie, true)); } @@ -508,6 +530,21 @@ impl<'a> CookieJar<'a> { } } + /// For each property below, this method checks if there is a provided value + /// and if there is none, sets a default value. Default values are: + /// + /// * `path`: `"/"` + /// * `SameSite`: `Lax` + fn set_removal_defaults(cookie: &mut Cookie<'static>) { + if cookie.path().is_none() { + cookie.set_path("/"); + } + + if cookie.same_site().is_none() { + cookie.set_same_site(SameSite::Lax); + } + } + /// For each property mentioned below, this method checks if there is a /// provided value and if there is none, sets a default value. Default /// values are: From 1df854e13a07f1f4f075bfd885a4a5da55d303be Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 12:08:57 -0500 Subject: [PATCH 020/178] Add a "clear message" button to cookies example. --- examples/cookies/src/message.rs | 11 +++++-- examples/cookies/templates/message.html.hbs | 32 +++++++++++++++------ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/cookies/src/message.rs b/examples/cookies/src/message.rs index 06a33a0085..a1d8c9b033 100644 --- a/examples/cookies/src/message.rs +++ b/examples/cookies/src/message.rs @@ -16,12 +16,19 @@ fn submit(cookies: &CookieJar<'_>, message: Form<&str>) -> Redirect { Redirect::to(uri!(index)) } +#[delete("/")] +fn delete(cookies: &CookieJar<'_>) -> Redirect { + cookies.remove("message"); + Redirect::to(uri!(index)) +} + #[get("/")] fn index(cookies: &CookieJar<'_>) -> Template { let message = cookies.get("message").map(|c| c.value()); - Template::render("message", context! { message }) + let present = cookies.get("message").is_some(); + Template::render("message", context! { present, message }) } pub fn routes() -> Vec { - routes![submit, index] + routes![index, submit, delete] } diff --git a/examples/cookies/templates/message.html.hbs b/examples/cookies/templates/message.html.hbs index 7dcec374c7..df724332d5 100644 --- a/examples/cookies/templates/message.html.hbs +++ b/examples/cookies/templates/message.html.hbs @@ -7,18 +7,32 @@

Rocket Cookie Message

- {{#if message }} -

{{message}}

- {{else}} -

No message yet.

- {{/if}} - - -

+

+ {{#if present}} + {{#if message}} + Message: {{message}} + {{else}} + Message: [empty message] + {{/if}} + {{else}} + No message yet. + {{/if}} +

+ + + + + +
+
+

+ + +

+ Home From f14f93afa7e62bd92068e9d3f425f3b2fd75e43f Mon Sep 17 00:00:00 2001 From: Benjamin B <7598058+BBlackwo@users.noreply.github.com> Date: Thu, 2 Nov 2023 09:14:15 +1100 Subject: [PATCH 021/178] Fix typo in pastebin tutorial: 'route' -> 'wrote'. --- site/guide/10-pastebin-tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/guide/10-pastebin-tutorial.md b/site/guide/10-pastebin-tutorial.md index 875c72ae3c..6cdda784c9 100644 --- a/site/guide/10-pastebin-tutorial.md +++ b/site/guide/10-pastebin-tutorial.md @@ -384,7 +384,7 @@ Now that we can retrieve pastes safely, it's time to actually store them. We'll write an `upload` route that, according to our design, takes a paste's contents and writes them to a file with a randomly generated ID inside of the `upload/` directory. It'll return a URL to the client for the paste corresponding to the -`retrieve` route we just route. +`retrieve` route we just wrote. ### Streaming Data From 15637186bae98eb6b3a907d5bdade4fbacdc4e59 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 18:48:41 -0500 Subject: [PATCH 022/178] Document '&[u8]' form/data guard, limits. --- core/lib/src/data/limits.rs | 4 +++- core/lib/src/form/from_form.rs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/lib/src/data/limits.rs b/core/lib/src/data/limits.rs index 9606304e38..535d843aad 100644 --- a/core/lib/src/data/limits.rs +++ b/core/lib/src/data/limits.rs @@ -59,8 +59,10 @@ use crate::http::uncased::Uncased; /// | `data-form` | 2MiB | [`Form`] | entire data-based form | /// | `file` | 1MiB | [`TempFile`] | [`TempFile`] data guard or form field | /// | `file/$ext` | _N/A_ | [`TempFile`] | file form field with extension `$ext` | -/// | `string` | 8KiB | [`String`] | data guard or data form field | +/// | `string` | 8KiB | [`String`] | data guard or form field | +/// | `string` | 8KiB | [`&str`] | data guard or form field | /// | `bytes` | 8KiB | [`Vec`] | data guard | +/// | `bytes` | 8KiB | [`&[u8]`] | data guard or form field | /// | `json` | 1MiB | [`Json`] | JSON data and form payloads | /// | `msgpack` | 1MiB | [`MsgPack`] | MessagePack data and form payloads | /// diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index f44be8cf99..347b62d80c 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -116,11 +116,12 @@ use crate::http::uncased::AsUncased; /// | _nonzero_ int | _inherit_ | **no default** | No | Yes | `NonZero{I,U}{size,8,16,32,64,128}` | /// | float | _inherit_ | **no default** | No | Yes | `f{32,64}` | /// | `&str` | _inherit_ | **no default** | Yes | Yes | Percent-decoded. Data limit `string` applies. | +/// | `&[u8]` | _inherit_ | **no default** | Yes | Yes | Raw bytes. Data limit `bytes` applies. | /// | `String` | _inherit_ | **no default** | Yes | Yes | Exactly `&str`, but owned. Prefer `&str`. | /// | IP Address | _inherit_ | **no default** | No | Yes | [`IpAddr`], [`Ipv4Addr`], [`Ipv6Addr`] | /// | Socket Address | _inherit_ | **no default** | No | Yes | [`SocketAddr`], [`SocketAddrV4`], [`SocketAddrV6`] | /// | [`TempFile`] | _inherit_ | **no default** | Yes | Yes | Data limits apply. See [`TempFile`]. | -/// | [`Capped`] | _inherit_ | **no default** | Yes | Yes | `C` is `&str`, `String`, `&[u8]` or `TempFile`. | +/// | [`Capped`] | _inherit_ | **no default** | Yes | Yes | `C` is `&str`, `String`, `&[u8]` or `TempFile`. | /// | [`time::Date`] | _inherit_ | **no default** | No | Yes | `%F` (`YYYY-MM-DD`). HTML "date" input. | /// | [`time::DateTime`] | _inherit_ | **no default** | No | Yes | `%FT%R` or `%FT%T` (`YYYY-MM-DDTHH:MM[:SS]`) | /// | [`time::Time`] | _inherit_ | **no default** | No | Yes | `%R` or `%T` (`HH:MM[:SS]`) | From 1c3342d5afd462f6f765941bac0e674d3ff90ecc Mon Sep 17 00:00:00 2001 From: Juhasz Sandor Date: Tue, 7 Nov 2023 19:29:47 +0100 Subject: [PATCH 023/178] Update 'diesel-async' to '0.4.1'. --- contrib/db_pools/lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 540e5348b2..97c10b7745 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -61,7 +61,7 @@ features = ["tokio-runtime"] optional = true [dependencies.diesel-async] -version = "0.3.1" +version = "0.4.1" default-features = false optional = true From 3a935c2c22248e3f77dc2d19f9b1e4f307981482 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 18:48:14 -0500 Subject: [PATCH 024/178] Add CHANGELOG for 0.5.0-rc.4. --- CHANGELOG.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f059b2a6e1..3012f566df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,161 @@ +# Version 0.5.0-rc.4 (Nov 1, 2023) + +## Major Features and Improvements + + * Introduced [request connection upgrade APIs]. + + The APIs allow responders to handle upgrade requests by [registering] + [`IoHandler`]s for upgraded protocols. The [`IoHandler`] can perform raw + byte I/O with the client. This functionality is used to implement WebSocket + support by the newly introduced [`rocket_ws`] library. + + * Introduced WebSocket support: [`rocket_ws`]. + + The newly introduced [`rocket_ws`] contrib library -- entirely external to + Rocket itself -- provides complete, multi-modal support for interacting with + clients over WebSockets. Modalities include async streams: + + ```rust + #[get("/echo?compose")] + fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { + ws.stream(|io| io) + } + ``` + + And generator syntax, among others: + + ```rust + #[get("/echo?stream")] + fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::Stream! { ws => + for await message in ws { + yield message?; + } + } + } + ``` + +[request connection upgrade APIs]: https://api.rocket.rs/v0.5-rc/rocket/struct.Response.html#upgrading +[`rocket_ws`]: https://api.rocket.rs/v0.5-rc/rocket_ws/ +[registering]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Response.html#method.add_upgrade +[`IoHandler`]: https://api.rocket.rs/v0.5-rc/rocket/data/trait.IoHandler.html + +## Breaking Changes + + * The types of responders in [`response::status`] were unified to all be of + the form `Status(R)`. + + Previously, some responders were of the form `Status(Option)`. + + * [Custom form errors] must now specify an associated `Status`. + + Change errors created as `ErrorKind(error)` to `ErrorKind(status, error)`. + + * [Route `Forward` outcomes] are now associated with a `Status`. + + This allows guards to determine which catcher will be invoked when all + routes forward. All [`request::Outcome`] `Forward` values must be modified + to include a `Status` hint. + + * `Outcome::Failure` was renamed to [`Outcome::Error`]. + + This disambiguates the leading `F`s in `Failure` and `Forward`. In + particular, generics of `E` and `F` now clearly correspond to `Error` and + `Forward`, respectively. + + * The status codes used when built-in guards forward were changed. + + Previously, the `NotFound` status code was used to signal many kinds of + recoverable, forwarding errors. This included validation errors, incorrect + Content-Type errors, and more. This has been changed such that the status + code used more closely matches the issue encountered. + + The net effect is that different error catchers will be invoked when + built-in guards fail. The complete changes are: + + * Route parameter `FromParam` errors now forward as 422. + * Query paramater errors now forward as 422. + * Incorrect form content-type errors forwards as 413. + * `&Host`, `&Accept`, `&ContentType`, `IpAddr`, and `SocketAddr` all forward + with a 500. + + * [`IntoOutcome`] was overhauled to supplant methods now removed in `Outcome`. + + * `IntoOutcome::into_outcome()` is now `or_error()`. + * `IntoOutcome` is implemented for all `Outcome` type aliases. + * `Outcome::forward()` requires specifying a status code. + * `Outcome::from()` and `Outcome::from_or_forward()` were removed. + + Use `responder.respond_to(req)` followed by a chaining of `.or_error()` or + `.or_forward()`. + + * `Secure` cookie flags are set by default when serving over TLS. + + * Removal cookies now have `SameSite` set to `Lax`. + + * [`MediaType::JavaScript`] is now `text/javascript`. + +[`response::status`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/index.html +[Custom form errors]: https://api.rocket.rs/v0.5-rc/rocket/form/error/enum.ErrorKind.html#variant.Custom +[`request::Outcome`]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward +[Route `Forward` outcomes]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward +[`Outcome::Error`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/enum.Outcome.html#variant.Error +[`IntoOutcome`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/trait.IntoOutcome.html +[`MediaType::JavaScript`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html#associatedconstant.JavaScript + +## General Improvements + + * [`rocket_db_pools`] gained support for [`diesel-async`]. + * Added [`TempFile::open()`] to stream `TempFile` data. + * mTLS certificates can now be set on local requests via + [`LocalRequest::identity()`]. + * Added [`Error::pretty_print()`] for pretty-printing errors like Rocket. + * The TLS example now includes an HTTP to HTTPS redirection fairing. + * When data limits are reached, warnings are logged. + * A warning is emitted when `String` is used as a route parameter. + * Configuration provenance information is logged under the `debug` log level. + * `template_dir` as recognized by `rocket_dyn_templates` is more reliably + interpreted. + * Logging of `Outcome`s now includes the relevant status code. + +[`diesel-async`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#diesel-v2 +[`LocalRequest::identity()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.identity +[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open +[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print + +### Known Media Types + + * The `.mjs` extension is now recognized as JavaScript. + * '.exe', '.iso', '.dmg' are marked as known extensions. + +### Trait Implementations + + * Implemented `De(Serialize)` for `Status`. + +### Updated Dependencies + + * `rustls` to 0.21. + * `tokio-rustls` to 0.24. + * `state` to 0.21. + * `sqlx` to 0.7. + * `notify` to 6. + * `indexmap` to 2. + * `cookie` to 0.18. + +### Documentation + + * Driver versions in `db_pools` were fixed and updated. + * Added details on using `Status` as a field in a derived `Responder`. + * All built-in data guards are now properly documented. + * Further details on deriving `UriDisplay` were added. + * The guide now includes notes on enabling crate features when needed. + +## Infrastructure + + * The CI now frees disk space before proceeding to avoid out-of-disk errors. + * `Span::mixed_site()` is used in codegen to reduce errant `clippy` warnings. + * All workspaces now use `resolver = 2`. + # Version 0.5.0-rc.3 (Mar 23, 2023) ## Major Features and Improvements From a1c29c735c0e49d657b4c4103f02d3da2a9c1d17 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 1 Nov 2023 19:18:11 -0500 Subject: [PATCH 025/178] New version: 0.5.0-rc.4. New contrib versions: 0.1.0-rc.4. --- contrib/db_pools/README.md | 2 +- contrib/db_pools/codegen/Cargo.toml | 2 +- contrib/db_pools/lib/Cargo.toml | 6 ++--- contrib/db_pools/lib/src/diesel.rs | 4 +-- contrib/db_pools/lib/src/lib.rs | 4 +-- contrib/dyn_templates/Cargo.toml | 4 +-- contrib/dyn_templates/README.md | 2 +- contrib/dyn_templates/src/lib.rs | 2 +- contrib/sync_db_pools/README.md | 2 +- contrib/sync_db_pools/codegen/Cargo.toml | 2 +- contrib/sync_db_pools/lib/Cargo.toml | 6 ++--- contrib/sync_db_pools/lib/src/lib.rs | 2 +- contrib/ws/Cargo.toml | 4 +-- contrib/ws/README.md | 2 +- contrib/ws/src/lib.rs | 2 +- core/codegen/Cargo.toml | 4 +-- core/codegen/src/lib.rs | 2 +- core/http/Cargo.toml | 2 +- core/lib/Cargo.toml | 6 ++--- core/lib/src/lib.rs | 6 ++--- core/lib/src/serde/json.rs | 2 +- core/lib/src/serde/msgpack.rs | 2 +- core/lib/src/serde/uuid.rs | 2 +- site/guide/01-upgrading.md | 6 ++--- site/guide/10-pastebin-tutorial.md | 2 +- site/guide/12-faq.md | 2 +- site/guide/2-getting-started.md | 2 +- site/guide/4-requests.md | 4 +-- site/guide/6-state.md | 4 +-- site/guide/9-configuration.md | 4 +-- site/index.toml | 4 +-- site/news/2023-11-01-version-0.5-rc.4.md | 31 ++++++++++++++++++++++++ site/news/index.toml | 12 +++++++++ site/tests/Cargo.toml | 2 +- 34 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 site/news/2023-11-01-version-0.5-rc.4.md diff --git a/contrib/db_pools/README.md b/contrib/db_pools/README.md index 77f64633a8..9ec29d181c 100644 --- a/contrib/db_pools/README.md +++ b/contrib/db_pools/README.md @@ -17,7 +17,7 @@ full usage details. ```toml [dependencies.rocket_db_pools] - version = "=0.1.0-rc.3" + version = "=0.1.0-rc.4" features = ["sqlx_sqlite"] ``` diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index 21ad699e2f..3d23598bf2 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools_codegen" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Procedural macros for rocket_db_pools." repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 97c10b7745..598fc8273a 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Rocket async database pooling support" repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" @@ -29,12 +29,12 @@ diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpo [dependencies.rocket] path = "../../../core/lib" -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" default-features = false [dependencies.rocket_db_pools_codegen] path = "../codegen" -version = "=0.1.0-rc.3" +version = "=0.1.0-rc.4" [dependencies.deadpool] version = "0.9" diff --git a/contrib/db_pools/lib/src/diesel.rs b/contrib/db_pools/lib/src/diesel.rs index 89c606be97..65974d3a51 100644 --- a/contrib/db_pools/lib/src/diesel.rs +++ b/contrib/db_pools/lib/src/diesel.rs @@ -8,11 +8,11 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.3" +//! rocket = "=0.5.0-rc.4" //! diesel = "2" //! //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.3" +//! version = "=0.1.0-rc.4" //! features = ["diesel_mysql"] //! ``` //! diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index c00375c895..05259e16e9 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -7,7 +7,7 @@ //! //! ```toml //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.3" +//! version = "=0.1.0-rc.4" //! features = ["sqlx_sqlite"] //! ``` //! @@ -165,7 +165,7 @@ //! features = ["macros", "migrate"] //! //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.3" +//! version = "=0.1.0-rc.4" //! features = ["sqlx_sqlite"] //! ``` //! diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index d4386d2b2b..1fe40ef642 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_dyn_templates" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez "] description = "Dynamic templating engine integration for Rocket." documentation = "https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/" @@ -22,7 +22,7 @@ notify = "6" normpath = "1" [dependencies.rocket] -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" path = "../../core/lib" default-features = false diff --git a/contrib/dyn_templates/README.md b/contrib/dyn_templates/README.md index 4bfd30cde8..810f48461b 100644 --- a/contrib/dyn_templates/README.md +++ b/contrib/dyn_templates/README.md @@ -22,7 +22,7 @@ supports [Handlebars] and [Tera]. ```toml [dependencies.rocket_dyn_templates] - version = "=0.1.0-rc.3" + version = "=0.1.0-rc.4" features = ["handlebars", "tera"] ``` diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 8ae1040a07..7202a3a610 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -12,7 +12,7 @@ //! //! ```toml //! [dependencies.rocket_dyn_templates] -//! version = "=0.1.0-rc.3" +//! version = "=0.1.0-rc.4" //! features = ["handlebars", "tera"] //! ``` //! diff --git a/contrib/sync_db_pools/README.md b/contrib/sync_db_pools/README.md index 1e376eeb23..9cfa24d00f 100644 --- a/contrib/sync_db_pools/README.md +++ b/contrib/sync_db_pools/README.md @@ -19,7 +19,7 @@ First, enable the feature corresponding to your database type: ```toml [dependencies.rocket_sync_db_pools] -version = "=0.1.0-rc.3" +version = "=0.1.0-rc.4" features = ["diesel_sqlite_pool"] ``` diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index cee6a484ef..c3319e9f53 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_sync_db_pools_codegen" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez "] description = "Procedural macros for rocket_sync_db_pools." repository = "https://github.com/SergioBenitez/Rocket/contrib/sync_db_pools" diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index 8a2903f166..3f65e3c230 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_sync_db_pools" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez "] description = "Rocket async database pooling support for sync database drivers." repository = "https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/contrib/sync_db_pools" @@ -35,11 +35,11 @@ memcache = { version = "0.15", optional = true } r2d2-memcache = { version = "0.6", optional = true } [dependencies.rocket_sync_db_pools_codegen] -version = "=0.1.0-rc.3" +version = "=0.1.0-rc.4" path = "../codegen" [dependencies.rocket] -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" path = "../../../core/lib" default-features = false diff --git a/contrib/sync_db_pools/lib/src/lib.rs b/contrib/sync_db_pools/lib/src/lib.rs index 731b2230fd..e390d725b4 100644 --- a/contrib/sync_db_pools/lib/src/lib.rs +++ b/contrib/sync_db_pools/lib/src/lib.rs @@ -30,7 +30,7 @@ //! //! ```toml //! [dependencies.rocket_sync_db_pools] -//! version = "=0.1.0-rc.3" +//! version = "=0.1.0-rc.4" //! features = ["diesel_sqlite_pool"] //! ``` //! diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index 10068cca24..d4ae211f60 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_ws" -version = "0.1.0-rc.3" +version = "0.1.0-rc.4" authors = ["Sergio Benitez "] description = "WebSocket support for Rocket." documentation = "https://api.rocket.rs/v0.5-rc/rocket_ws/" @@ -20,7 +20,7 @@ tungstenite = ["tokio-tungstenite"] tokio-tungstenite = { version = "0.20", optional = true } [dependencies.rocket] -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" path = "../../core/lib" default-features = false diff --git a/contrib/ws/README.md b/contrib/ws/README.md index 825bdab6d6..d7e947250d 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -16,7 +16,7 @@ This crate provides WebSocket support for Rocket via integration with Rocket's ```toml [dependencies] - ws = { package = "rocket_ws", version ="=0.1.0-rc.3" } + ws = { package = "rocket_ws", version ="=0.1.0-rc.4" } ``` 2. Use it! diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs index 4b424a7697..a8c2d8955c 100644 --- a/contrib/ws/src/lib.rs +++ b/contrib/ws/src/lib.rs @@ -10,7 +10,7 @@ //! //! ```toml //! [dependencies] -//! ws = { package = "rocket_ws", version ="=0.1.0-rc.3" } +//! ws = { package = "rocket_ws", version ="=0.1.0-rc.4" } //! ``` //! //! Then, use [`WebSocket`] as a request guard in any route and either call diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index bda61846b7..c0d4f0b152 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_codegen" -version = "0.5.0-rc.3" +version = "0.5.0-rc.4" authors = ["Sergio Benitez "] description = "Procedural macros for the Rocket web framework." documentation = "https://api.rocket.rs/v0.5-rc/rocket_codegen/" @@ -21,7 +21,7 @@ quote = "1.0" syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits"] } proc-macro2 = "1.0.27" devise = "0.4" -rocket_http = { version = "=0.5.0-rc.3", path = "../http/" } +rocket_http = { version = "=0.5.0-rc.4", path = "../http/" } unicode-xid = "0.2" version_check = "0.9" glob = "0.3" diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index abcf387262..4f9a6b2faf 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -21,7 +21,7 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.3" +//! rocket = "=0.5.0-rc.4" //! ``` //! //! And to import all macros, attributes, and derives via `#[macro_use]` in the diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 6e3020ff9b..8994e70492 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_http" -version = "0.5.0-rc.3" +version = "0.5.0-rc.4" authors = ["Sergio Benitez "] description = """ Types, traits, and parsers for HTTP requests, responses, and headers. diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 092e08ff2c..c29d1d968b 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket" -version = "0.5.0-rc.3" +version = "0.5.0-rc.4" authors = ["Sergio Benitez "] description = """ Web framework with a focus on usability, security, extensibility, and speed. @@ -61,11 +61,11 @@ tokio-stream = { version = "0.1.6", features = ["signal", "time"] } state = "0.6" [dependencies.rocket_codegen] -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" path = "../codegen" [dependencies.rocket_http] -version = "=0.5.0-rc.3" +version = "=0.5.0-rc.4" path = "../http" features = ["serde"] diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index f3a11ddf33..8ac0f9944b 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -29,7 +29,7 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.3" +//! rocket = "=0.5.0-rc.4" //! ``` //! //! Note that development versions, tagged with `-dev`, are not published @@ -73,14 +73,14 @@ //! //! ```toml //! [dependencies] -//! rocket = { version = "=0.5.0-rc.3", features = ["secrets", "tls", "json"] } +//! rocket = { version = "=0.5.0-rc.4", features = ["secrets", "tls", "json"] } //! ``` //! //! Conversely, HTTP/2 can be disabled: //! //! ```toml //! [dependencies] -//! rocket = { version = "=0.5.0-rc.3", default-features = false } +//! rocket = { version = "=0.5.0-rc.4", default-features = false } //! ``` //! //! [JSON (de)serialization]: crate::serde::json diff --git a/core/lib/src/serde/json.rs b/core/lib/src/serde/json.rs index a96f629e85..3a22f82258 100644 --- a/core/lib/src/serde/json.rs +++ b/core/lib/src/serde/json.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.3" +//! version = "=0.5.0-rc.4" //! features = ["json"] //! ``` //! diff --git a/core/lib/src/serde/msgpack.rs b/core/lib/src/serde/msgpack.rs index 3b92f06092..ff2381dc34 100644 --- a/core/lib/src/serde/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.3" +//! version = "=0.5.0-rc.4" //! features = ["msgpack"] //! ``` //! diff --git a/core/lib/src/serde/uuid.rs b/core/lib/src/serde/uuid.rs index 8c377168c6..03c5f7811a 100644 --- a/core/lib/src/serde/uuid.rs +++ b/core/lib/src/serde/uuid.rs @@ -7,7 +7,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.3" +//! version = "=0.5.0-rc.4" //! features = ["uuid"] //! ``` //! diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index 80dd3ab194..65f23013d9 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -38,7 +38,7 @@ private cookies, you _must_ enable the `secrets` feature in `Cargo.toml`: ```toml [dependencies] -rocket = { version = "=0.5.0-rc.3", features = ["secrets"] } +rocket = { version = "=0.5.0-rc.4", features = ["secrets"] } ``` ### Contrib Deprecation @@ -59,8 +59,8 @@ to `Cargo.toml`: [dependencies] - rocket = "0.4" - rocket_contrib = { version = "0.4", features = ["json"], default-features = false } -+ rocket = { version = "=0.5.0-rc.3", features = ["json"] } -+ rocket_dyn_templates = { version = "=0.1.0-rc.3", features = ["tera"] } ++ rocket = { version = "=0.5.0-rc.4", features = ["json"] } ++ rocket_dyn_templates = { version = "=0.1.0-rc.4", features = ["tera"] } ``` ! note: `rocket_dyn_templates` (and co.) _does not_ follow in version lock-step diff --git a/site/guide/10-pastebin-tutorial.md b/site/guide/10-pastebin-tutorial.md index 6cdda784c9..e0a6d222f8 100644 --- a/site/guide/10-pastebin-tutorial.md +++ b/site/guide/10-pastebin-tutorial.md @@ -52,7 +52,7 @@ Then add the usual Rocket dependencies to the `Cargo.toml` file: ```toml [dependencies] -rocket = "=0.5.0-rc.3" +rocket = "=0.5.0-rc.4" ``` And finally, create a skeleton Rocket application to work off of in diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index 1356ef31cf..a11078fd9e 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -646,7 +646,7 @@ is to depend on a `contrib` library from git while also depending on a `crates.io` version of Rocket or vice-versa: ```toml -rocket = "=0.5.0-rc.3" +rocket = "=0.5.0-rc.4" rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" } ``` diff --git a/site/guide/2-getting-started.md b/site/guide/2-getting-started.md index 368c2b56af..74d601a473 100644 --- a/site/guide/2-getting-started.md +++ b/site/guide/2-getting-started.md @@ -43,7 +43,7 @@ Now, add Rocket as a dependency in your `Cargo.toml`: ```toml [dependencies] -rocket = "=0.5.0-rc.3" +rocket = "=0.5.0-rc.4" ``` ! warning: Development versions must be _git_ dependencies. diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 56b927cebe..c0c0d6e551 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -606,7 +606,7 @@ feature: ```toml ## in Cargo.toml -rocket = { version = "=0.5.0-rc.3", features = ["secrets"] } +rocket = { version = "=0.5.0-rc.4", features = ["secrets"] } ``` The API for retrieving, adding, and removing private cookies is identical except @@ -784,7 +784,7 @@ complete example. feature can be enabled in the `Cargo.toml`: ` - rocket = { version = "=0.5.0-rc.3", features = ["json"] } + rocket = { version = "=0.5.0-rc.4", features = ["json"] } ` ### Temporary Files diff --git a/site/guide/6-state.md b/site/guide/6-state.md index b845d3474e..ac974ffd5f 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -231,7 +231,7 @@ in three simple steps: ```toml [dependencies.rocket_db_pools] - version = "=0.1.0-rc.3" + version = "=0.1.0-rc.4" features = ["sqlx_sqlite"] ``` @@ -299,7 +299,7 @@ default-features = false features = ["macros", "migrate"] [dependencies.rocket_db_pools] -version = "=0.1.0-rc.3" +version = "=0.1.0-rc.4" features = ["sqlx_sqlite"] ``` diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index c9f7130115..19a0d8d639 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -237,7 +237,7 @@ Security). To enable TLS support: ```toml,ignore [dependencies] - rocket = { version = "=0.5.0-rc.3", features = ["tls"] } + rocket = { version = "=0.5.0-rc.4", features = ["tls"] } ``` 2. Configure a TLS certificate chain and private key via the `tls.key` and @@ -302,7 +302,7 @@ enabled and support configured via the `tls.mutual` config parameter: ```toml,ignore [dependencies] - rocket = { version = "=0.5.0-rc.3", features = ["mtls"] } + rocket = { version = "=0.5.0-rc.4", features = ["mtls"] } ``` This implicitly enables the `tls` feature. diff --git a/site/index.toml b/site/index.toml index 64ad7b19b4..87bad8cee8 100644 --- a/site/index.toml +++ b/site/index.toml @@ -3,8 +3,8 @@ ############################################################################### [release] -version = "0.5.0-rc.3" -date = "Mar 23, 2023" +version = "0.5.0-rc.4" +date = "Nov 1, 2023" ############################################################################### # Top features: displayed in the header under the introductory text. diff --git a/site/news/2023-11-01-version-0.5-rc.4.md b/site/news/2023-11-01-version-0.5-rc.4.md new file mode 100644 index 0000000000..31a3b7503a --- /dev/null +++ b/site/news/2023-11-01-version-0.5-rc.4.md @@ -0,0 +1,31 @@ +# Rocket's 4th v0.5 Release Candidate + + + +Rocket `0.5.0-rc.4`, a release candidate for Rocket v0.5, is now available. + +This release builds on the previous release candidate and brings further +features, improvements, and fixes to Rocket. As before, this is an opportunity +to discover issues with Rocket v0.5 and its documentation before its general +release. We encourage all users to migrate their applications to the fourth +release candidate and report any issues to the [GitHub issue tracker]. Please +see the new [migration guide] for complete details on how to upgrade from Rocket +v0.4. For changes since Rocket v0.5.0-rc.3, please see the [CHANGELOG]. + +[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[migration guide]: ../../guide/upgrading +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.4/CHANGELOG.md + +## About Rocket + +Rocket is a web framework for Rust with a focus on usability, security, +extensibility, and speed. Rocket makes it simple to write fast, secure web +applications without sacrificing usability. + +Not already using Rocket? Join the tens of thousands of users and hundreds of +companies happily using Rocket today! Rocket's extensive documentation makes it +easy: get started by [reading through the guide](../../guide) or learning more +from [the overview](../../overview). diff --git a/site/news/index.toml b/site/news/index.toml index cd137e2a9e..75f140ab70 100644 --- a/site/news/index.toml +++ b/site/news/index.toml @@ -1,3 +1,15 @@ +[[articles]] +title = """ + Rocket's 4th v0.5 Release Candidate +""" +slug = "2023-11-01-version-0.5-rc.4" +author = "Sergio Benitez" +author_url = "https://sergio.bz" +date = "November 01, 2023" +snippet = """ +Rocket `0.5.0-rc.4`, a release candidate for Rocket v0.5, is now available. +""" + [[articles]] title = """ Rocket's 3rd v0.5 Release Candidate diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index c6861023c2..561c9a54ba 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_guide_tests" -version = "0.5.0-rc.3" +version = "0.5.0-rc.4" workspace = "../../" edition = "2021" publish = false From 0d48743bd860dd1cd12b45d06ea0b7c193ff8aa7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 3 Nov 2023 14:49:09 -0500 Subject: [PATCH 026/178] Fix broken site links. --- site/guide/4-requests.md | 2 +- site/guide/index.md | 2 +- site/news/2017-02-06-version-0.2.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index c0c0d6e551..fcdf6fb353 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -1196,7 +1196,7 @@ use std::str::FromStr; struct Token<'r>(&'r str); ``` -[`try_with`]: rocket/form/validate/fn.try_with.html +[`try_with`]: @api/rocket/form/validate/fn.try_with.html ### Collections diff --git a/site/guide/index.md b/site/guide/index.md index 82b2f099fc..46dc9d6238 100644 --- a/site/guide/index.md +++ b/site/guide/index.md @@ -39,7 +39,7 @@ aspect of Rocket. The sections are: The official community support channels are [`#rocket:mozilla.org`] on Matrix and the bridged [`#rocket`] IRC channel on Libera.Chat at `irc.libera.chat`. We recommend joining us on [Matrix via Element]. If you prefer IRC, you can join -via the [Kiwi IRC client] or a client of your own. The [FAQ](../faq/) also +via the [Kiwi IRC client] or a client of your own. The [FAQ](faq/) also provides answers to commonly asked questions. [`#rocket:mozilla.org`]: https://chat.mozilla.org/#/room/#rocket:mozilla.org diff --git a/site/news/2017-02-06-version-0.2.md b/site/news/2017-02-06-version-0.2.md index 246c5dcdee..b94aabeac9 100644 --- a/site/news/2017-02-06-version-0.2.md +++ b/site/news/2017-02-06-version-0.2.md @@ -174,7 +174,7 @@ Configuration parameters set via environment variables take precedence over parameters set via the `Rocket.toml` configuration file. Note that _any_ parameter can be set via an environment variable, include _extras_. For more about configuration in Rocket, see the [configuration section of the -guide](@guide-v0.3/overview#configuration). +guide](@guide-v0.3/configuration). ### And Plenty More! From 124ec94b46b8f427ca150186dbf238bc1da02c21 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 3 Nov 2023 17:03:46 -0500 Subject: [PATCH 027/178] Fix typos. Co-authored-by: cui fliter --- contrib/ws/src/lib.rs | 2 +- core/codegen/tests/from_form.rs | 2 +- core/codegen/tests/ui-fail-nightly/async-entry.stderr | 2 +- core/codegen/tests/ui-fail-stable/async-entry.stderr | 2 +- core/codegen/tests/ui-fail/async-entry.rs | 2 +- core/lib/src/form/mod.rs | 2 +- core/lib/src/local/request.rs | 2 +- core/lib/src/request/from_request.rs | 2 +- core/lib/src/response/stream/sse.rs | 2 +- core/lib/src/shield/policy.rs | 8 ++++---- core/lib/tests/file_server.rs | 2 +- site/guide/5-responses.md | 4 ++-- site/guide/6-state.md | 2 +- site/news/2017-02-06-version-0.2.md | 2 +- site/news/2017-07-14-version-0.3.md | 2 +- site/news/2018-10-31-version-0.4-rc.md | 2 +- site/news/2018-11-30-version-0.4-rc-2.md | 2 +- 17 files changed, 21 insertions(+), 21 deletions(-) diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs index a8c2d8955c..06affb0005 100644 --- a/contrib/ws/src/lib.rs +++ b/contrib/ws/src/lib.rs @@ -141,7 +141,7 @@ pub use self::tungstenite::Message; /// max_send_queue: Some(5), /// // Decrease the maximum (complete) message size to 4MiB. /// max_message_size: Some(4.mebibytes().as_u64() as usize), -/// // Decrease the maximum size of _one_ frame (not messag) to 1MiB. +/// // Decrease the maximum size of _one_ frame (not message) to 1MiB. /// max_frame_size: Some(1.mebibytes().as_u64() as usize), /// // Use the default values for the rest. /// ..Default::default() diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index 621ffa0481..568840d191 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -444,7 +444,7 @@ fn form_validate_contains_all_errors() { #[field(validate = evaluate())] firstname: String, check: bool, - // this validator is hardcoded to return an error but it doesnt + // this validator is hardcoded to return an error but it doesn't #[field(validate = evaluate_with_argument(self.check))] lastname: String, } diff --git a/core/codegen/tests/ui-fail-nightly/async-entry.stderr b/core/codegen/tests/ui-fail-nightly/async-entry.stderr index f6acb1576b..3a76303356 100644 --- a/core/codegen/tests/ui-fail-nightly/async-entry.stderr +++ b/core/codegen/tests/ui-fail-nightly/async-entry.stderr @@ -47,7 +47,7 @@ error: attribute cannot be applied to `main` function note: this function cannot be `main` --> tests/ui-fail-nightly/async-entry.rs:50:8 | -50 | fn main() -> rocekt::Rocket { +50 | fn main() -> rocket::Rocket { | ^^^^ = note: this error originates in the attribute macro `rocket::launch` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/async-entry.stderr b/core/codegen/tests/ui-fail-stable/async-entry.stderr index d3ac4cc7af..b300d054ff 100644 --- a/core/codegen/tests/ui-fail-stable/async-entry.stderr +++ b/core/codegen/tests/ui-fail-stable/async-entry.stderr @@ -38,7 +38,7 @@ error: attribute cannot be applied to `main` function error: [note] this function cannot be `main` --> tests/ui-fail-stable/async-entry.rs:50:8 | -50 | fn main() -> rocekt::Rocket { +50 | fn main() -> rocket::Rocket { | ^^^^ error: attribute can only be applied to functions that return a value diff --git a/core/codegen/tests/ui-fail/async-entry.rs b/core/codegen/tests/ui-fail/async-entry.rs index bea02a422c..35f5e13b5d 100644 --- a/core/codegen/tests/ui-fail/async-entry.rs +++ b/core/codegen/tests/ui-fail/async-entry.rs @@ -47,7 +47,7 @@ mod launch_b { mod launch_c { #[rocket::launch] - fn main() -> rocekt::Rocket { + fn main() -> rocket::Rocket { rocket::build() } } diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 442c455a14..44487876f9 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -114,7 +114,7 @@ // .map_err(|e| errors.push(e)) // .map(Some).unwrap_or(None); // -// let field_b = B::finblize(&mut this.field_b) +// let field_b = B::finalize(&mut this.field_b) // .map_err(|e| errors.push(e)) // .map(Some).unwrap_or(None); // diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index a04ff5d7ba..eabe933ee7 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -200,7 +200,7 @@ macro_rules! pub_request_impl { /// Set mTLS client certificates to send along with the request. /// /// If the request already contained certificates, they are replaced with - /// thsoe in `reader.` + /// those in `reader.` /// /// `reader` is expected to be PEM-formatted and contain X509 certificates. /// If it contains more than one certificate, the entire chain is set on the diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 203c7d490f..a0776aaa9b 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -89,7 +89,7 @@ pub type Outcome = outcome::Outcome; /// * **Forward**(Status) /// /// If the `Outcome` is [`Forward`], the request will be forwarded to the next -/// matching route until either one succeds or there are no further matching +/// matching route until either one succeeds or there are no further matching /// routes to attempt. In the latter case, the request will be sent to the /// [`Catcher`](crate::Catcher) for the designated `Status`. Note that users /// can request an `Option` to catch `Forward`s. diff --git a/core/lib/src/response/stream/sse.rs b/core/lib/src/response/stream/sse.rs index b123f632d6..ae2445795f 100644 --- a/core/lib/src/response/stream/sse.rs +++ b/core/lib/src/response/stream/sse.rs @@ -783,7 +783,7 @@ mod sse_tests { yield Event::data("bar"); }; - // We expect: foo\n\n [heartbeat] bar\n\n [maybe beartbeat]. + // We expect: foo\n\n [heartbeat] bar\n\n [maybe heartbeat]. let string = stream.heartbeat(Duration::from_millis(350)).into_string(); let heartbeats = string.matches(HEARTBEAT).count(); assert!(heartbeats >= 1 && heartbeats <= 3, "got {} beat(s)", heartbeats); diff --git a/core/lib/src/shield/policy.rs b/core/lib/src/shield/policy.rs index 18280d462a..3ac3bc795a 100644 --- a/core/lib/src/shield/policy.rs +++ b/core/lib/src/shield/policy.rs @@ -176,10 +176,10 @@ impl From<&Referrer> for Header<'static> { /// The [Expect-CT] header: enables reporting and/or enforcement of [Certificate /// Transparency]. /// -/// [Certificate Transparency] can detect and prevent the use of misissued, -/// malicious, or revoked TLS certificates. It solves a variety of problems with -/// public TLS/SSL certificate management and is valuable measure for all public -/// TLS applications. +/// [Certificate Transparency] can detect and prevent the use of incorrectly +/// issued malicious, or revoked TLS certificates. It solves a variety of +/// problems with public TLS/SSL certificate management and is valuable measure +/// for all public TLS applications. /// /// If you're just [getting started] with certificate transparency, ensure that /// your [site is in compliance][getting started] before you enable enforcement diff --git a/core/lib/tests/file_server.rs b/core/lib/tests/file_server.rs index 69416edae2..c0b33e53a8 100644 --- a/core/lib/tests/file_server.rs +++ b/core/lib/tests/file_server.rs @@ -145,7 +145,7 @@ fn test_forwarding() { fn test_redirection() { let client = Client::debug(rocket()).expect("valid rocket"); - // Redirection only happens if enabled, and doesn't affect index behaviour. + // Redirection only happens if enabled, and doesn't affect index behavior. let response = client.get("/no_index/inner").dispatch(); assert_eq!(response.status(), Status::NotFound); diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 56656d91ac..831b31ac8f 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -504,8 +504,8 @@ fn index() -> Template { } ``` -For a template to be renderable, it must first be registered. The `Template` -fairing automatically registers all discoverable templates when attached. The +To render a template, it must first be registered. The `Template` fairing +automatically registers all discoverable templates when attached. The [Fairings](../fairings) sections of the guide provides more information on fairings. To attach the template fairing, simply call `.attach(Template::fairing())` on an instance of `Rocket` as follows: diff --git a/site/guide/6-state.md b/site/guide/6-state.md index ac974ffd5f..fb70b8832e 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -21,7 +21,7 @@ The process for using managed state is simple: ! note: All managed state must be thread-safe. - Because Rocket automatically multithreads your application, handlers can + Because Rocket automatically parallelizes your application, handlers can concurrently access managed state. As a result, managed state must be thread-safe. Thanks to Rust, this condition is checked at compile-time by ensuring that the type of values you store in managed state implement `Send` + diff --git a/site/news/2017-02-06-version-0.2.md b/site/news/2017-02-06-version-0.2.md index b94aabeac9..f657f9d82e 100644 --- a/site/news/2017-02-06-version-0.2.md +++ b/site/news/2017-02-06-version-0.2.md @@ -17,7 +17,7 @@ contributors at the end of this article. ## About Rocket -Rocket is a web framework for Rust with a focus on ease of use, expressibility, +Rocket is a web framework for Rust with a focus on ease of use, expressiveness, and speed. Rocket makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. diff --git a/site/news/2017-07-14-version-0.3.md b/site/news/2017-07-14-version-0.3.md index 566fe64db0..e422878289 100644 --- a/site/news/2017-07-14-version-0.3.md +++ b/site/news/2017-07-14-version-0.3.md @@ -15,7 +15,7 @@ support: a sincere thank you to everyone involved! ## About Rocket -Rocket is a web framework for Rust with a focus on ease of use, expressibility, +Rocket is a web framework for Rust with a focus on ease of use, expressiveness, and speed. Rocket makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. diff --git a/site/news/2018-10-31-version-0.4-rc.md b/site/news/2018-10-31-version-0.4-rc.md index 5958bd3d65..56e3c9b66c 100644 --- a/site/news/2018-10-31-version-0.4-rc.md +++ b/site/news/2018-10-31-version-0.4-rc.md @@ -31,7 +31,7 @@ Friday, November 9th for the general release! ## About Rocket -Rocket is a web framework for Rust with a focus on ease of use, expressibility, +Rocket is a web framework for Rust with a focus on ease of use, expressiveness, and speed. Rocket makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. diff --git a/site/news/2018-11-30-version-0.4-rc-2.md b/site/news/2018-11-30-version-0.4-rc-2.md index 048a729b7f..5474e2a705 100644 --- a/site/news/2018-11-30-version-0.4-rc-2.md +++ b/site/news/2018-11-30-version-0.4-rc-2.md @@ -38,7 +38,7 @@ Wednesday, December 5th for the general release! ## About Rocket -Rocket is a web framework for Rust with a focus on ease of use, expressibility, +Rocket is a web framework for Rust with a focus on ease of use, expressiveness, and speed. Rocket makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. From e6985c50e87abf169636d23ed2af2e3fb09df7df Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 3 Nov 2023 12:45:45 -0500 Subject: [PATCH 028/178] Add 0.5.0 CHANGELOG entry. --- CHANGELOG.md | 682 +++++++++++++++++++-------------------------------- 1 file changed, 258 insertions(+), 424 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3012f566df..daba64de24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,320 +1,4 @@ -# Version 0.5.0-rc.4 (Nov 1, 2023) - -## Major Features and Improvements - - * Introduced [request connection upgrade APIs]. - - The APIs allow responders to handle upgrade requests by [registering] - [`IoHandler`]s for upgraded protocols. The [`IoHandler`] can perform raw - byte I/O with the client. This functionality is used to implement WebSocket - support by the newly introduced [`rocket_ws`] library. - - * Introduced WebSocket support: [`rocket_ws`]. - - The newly introduced [`rocket_ws`] contrib library -- entirely external to - Rocket itself -- provides complete, multi-modal support for interacting with - clients over WebSockets. Modalities include async streams: - - ```rust - #[get("/echo?compose")] - fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { - ws.stream(|io| io) - } - ``` - - And generator syntax, among others: - - ```rust - #[get("/echo?stream")] - fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::Stream! { ws => - for await message in ws { - yield message?; - } - } - } - ``` - -[request connection upgrade APIs]: https://api.rocket.rs/v0.5-rc/rocket/struct.Response.html#upgrading -[`rocket_ws`]: https://api.rocket.rs/v0.5-rc/rocket_ws/ -[registering]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Response.html#method.add_upgrade -[`IoHandler`]: https://api.rocket.rs/v0.5-rc/rocket/data/trait.IoHandler.html - -## Breaking Changes - - * The types of responders in [`response::status`] were unified to all be of - the form `Status(R)`. - - Previously, some responders were of the form `Status(Option)`. - - * [Custom form errors] must now specify an associated `Status`. - - Change errors created as `ErrorKind(error)` to `ErrorKind(status, error)`. - - * [Route `Forward` outcomes] are now associated with a `Status`. - - This allows guards to determine which catcher will be invoked when all - routes forward. All [`request::Outcome`] `Forward` values must be modified - to include a `Status` hint. - - * `Outcome::Failure` was renamed to [`Outcome::Error`]. - - This disambiguates the leading `F`s in `Failure` and `Forward`. In - particular, generics of `E` and `F` now clearly correspond to `Error` and - `Forward`, respectively. - - * The status codes used when built-in guards forward were changed. - - Previously, the `NotFound` status code was used to signal many kinds of - recoverable, forwarding errors. This included validation errors, incorrect - Content-Type errors, and more. This has been changed such that the status - code used more closely matches the issue encountered. - - The net effect is that different error catchers will be invoked when - built-in guards fail. The complete changes are: - - * Route parameter `FromParam` errors now forward as 422. - * Query paramater errors now forward as 422. - * Incorrect form content-type errors forwards as 413. - * `&Host`, `&Accept`, `&ContentType`, `IpAddr`, and `SocketAddr` all forward - with a 500. - - * [`IntoOutcome`] was overhauled to supplant methods now removed in `Outcome`. - - * `IntoOutcome::into_outcome()` is now `or_error()`. - * `IntoOutcome` is implemented for all `Outcome` type aliases. - * `Outcome::forward()` requires specifying a status code. - * `Outcome::from()` and `Outcome::from_or_forward()` were removed. - - Use `responder.respond_to(req)` followed by a chaining of `.or_error()` or - `.or_forward()`. - - * `Secure` cookie flags are set by default when serving over TLS. - - * Removal cookies now have `SameSite` set to `Lax`. - - * [`MediaType::JavaScript`] is now `text/javascript`. - -[`response::status`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/index.html -[Custom form errors]: https://api.rocket.rs/v0.5-rc/rocket/form/error/enum.ErrorKind.html#variant.Custom -[`request::Outcome`]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward -[Route `Forward` outcomes]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward -[`Outcome::Error`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/enum.Outcome.html#variant.Error -[`IntoOutcome`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/trait.IntoOutcome.html -[`MediaType::JavaScript`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html#associatedconstant.JavaScript - -## General Improvements - - * [`rocket_db_pools`] gained support for [`diesel-async`]. - * Added [`TempFile::open()`] to stream `TempFile` data. - * mTLS certificates can now be set on local requests via - [`LocalRequest::identity()`]. - * Added [`Error::pretty_print()`] for pretty-printing errors like Rocket. - * The TLS example now includes an HTTP to HTTPS redirection fairing. - * When data limits are reached, warnings are logged. - * A warning is emitted when `String` is used as a route parameter. - * Configuration provenance information is logged under the `debug` log level. - * `template_dir` as recognized by `rocket_dyn_templates` is more reliably - interpreted. - * Logging of `Outcome`s now includes the relevant status code. - -[`diesel-async`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#diesel-v2 -[`LocalRequest::identity()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.identity -[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open -[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print - -### Known Media Types - - * The `.mjs` extension is now recognized as JavaScript. - * '.exe', '.iso', '.dmg' are marked as known extensions. - -### Trait Implementations - - * Implemented `De(Serialize)` for `Status`. - -### Updated Dependencies - - * `rustls` to 0.21. - * `tokio-rustls` to 0.24. - * `state` to 0.21. - * `sqlx` to 0.7. - * `notify` to 6. - * `indexmap` to 2. - * `cookie` to 0.18. - -### Documentation - - * Driver versions in `db_pools` were fixed and updated. - * Added details on using `Status` as a field in a derived `Responder`. - * All built-in data guards are now properly documented. - * Further details on deriving `UriDisplay` were added. - * The guide now includes notes on enabling crate features when needed. - -## Infrastructure - - * The CI now frees disk space before proceeding to avoid out-of-disk errors. - * `Span::mixed_site()` is used in codegen to reduce errant `clippy` warnings. - * All workspaces now use `resolver = 2`. - -# Version 0.5.0-rc.3 (Mar 23, 2023) - -## Major Features and Improvements - - * Added a [`max_blocking`] configuration parameter. - - The parameter sets a limit on the number of threads used by blocking tasks. - - * Added an [`ip_header`] "real IP" header configuration parameter. - - The parameter allows modifying the header that Rocket attempts to use to retrieve the "real IP" - address of the client via `Request` methods like [`Request::client_ip()`]. Additionally, the - change allows disabling the use of any such header entirely. - - * A [`pool()`] method is emitted by [`rocket_sync_db_pools`] for code-generated pools. - - The method returns an opaque reference to a type that can be used to retrieve pooled connections - outside of a request handling context. - - * Raw binary form field data can be retrieved using the `&[u8]` form guard. - - * Data guards are now eligible [sentinels]. - -## General Improvements - - * Final launch messages are now _always_ logged, irrespective of profile. - * Only functions that return `Rocket` are now `#[must_use]`, not all `Rocket

`. - * Fixed mismatched form field names in errors under certain conditions in [`FromForm`] derive. - * The [`FromForm`] derive now collects _all_ errors that occur. - * Data pools are now gracefully shutdown in [`rocket_sync_db_pools`]. - * Added [`Metadata::render()`] in [`rocket_dyn_templates`] for direct template rendering. - * Rocket salvages more information from malformed requests for error catchers. - * The `cookie` `secure` feature is now properly conditionally enabled. - * Data before encapsulation boundaries in TLS keys is allowed and ignored. - * Support for TLS keys in SEC1 format was added. - * Rocket now warns when a known secret key is configured. - * A panic that could occur on shutdown in `rocket_sync_db_pools` was fixed. - -### Known Media Types - - - Added `MP3`: `audio/mpeg`. - - Added `CBZ`: `application/vnd.comicbook+zip`, extension `.cbz`. - - Added `CBR`: `application/vnd.comicbook-rar`, extension `.cbr`. - - Added `RAR`: `application/vnd.rar`, extension `.rar`. - - Added `EPUB`: `application/epub+zip`, extension `.epub`. - - Added `OPF`: `application/oebps-package+xml`, extension `.opf`. - - Added `XHTML`: `application/xhtml+xml`, extension `.xhtml`. - -### Trait Implementations - - * Implemented `Responder` for `Box`. - * Implemented `FromForm` for `Arc`. - * Implemented `Fairing` for `Arc`. - -### Updated Dependencies - - * Updated `syn` to `2`. - * Updated `diesel` to `2.0`. - * Updated `sqlx` to `0.6`. - * Updated `notify` to `5.0`. - * Updated `criterion` to `0.4`. - * Updated `deadpool-redis` to `0.11`. - * Updated `normpath` from to `1`. - * Updated `cookie` to `0.17`. - * Replaced `atty` with `is-terminal`. - -## Infrastructure - - * UI tests are now allowed to fail by the CI to avoid false negatives. - * Fixed many typos, errors, and broken links throughout docs and examples. - * The GitHub CI workflow was updated to use maintained actions. - -[`Metadata::render()`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Metadata.html#method.render -[`pool()`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/example/struct.ExampleDb.html#method.pool -[`Request::client_ip()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.client_ip -[`max_blocking`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.max_blocking -[`ip_header`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.ip_header - -# Version 0.5.0-rc.2 (May 09, 2022) - -## Major Features and Improvements - - * Introduced [`rocket_db_pools`] for asynchronous database pooling. - * Introduced support for [mutual TLS] and client [`Certificate`]s. - * Added a [`local_cache_once!`] macro for request-local storage. - * Added a [v0.4 to v0.5 migration guide] and [FAQ] to Rocket's website. - * Introduced [shutdown fairings]. - -## Breaking Changes - - * `Hash` `impl`s for `MediaType` and `ContentType` no longer consider media type parameters. - * TLS config values are only available when the `tls` feature is enabled. - * [`MediaType::with_params()`] and [`ContentType::with_params()`] are now builder methods. - * Content-Type [`content`] responder type names are now prefixed with `Raw`. - * The `content::Plain` responder is now called `content::RawText`. - * The `content::Custom` responder was removed in favor of [`(ContentType, T)`]. - * TLS config structs are now only available when the `tls` feature is enabled. - * Removed `CookieJar::get_private_pending()` in favor of [`CookieJar::get_pending()`]. - * The [`local_cache!`] macro accepts fewer types. Use [`local_cache_once!`] as appropriate. - * When requested, the `FromForm` implementations of `Vec` and `Map`s are now properly lenient. - * To concord with browsers, the `[` and `]` characters are now accepted in URI paths. - * The `[` and `]` characters are no longer encoded by [`uri!`]. - * [`Rocket::launch()`] allows `Rocket` recovery by returning the instance after shutdown. - * `ErrorKind::Runtime` was removed; [`ErrorKind::Shutdown`] was added. - -## General Improvements - - * [`Rocket`] is now `#[must_use]`. - * Support for HTTP/2 can be disabled by disabling the default `http2` crate feature. - * Added [`rocket::execute()`] for executing Rocket's `launch()` future. - * Added the [`context!`] macro to [`rocket_dyn_templates`] for ad-hoc template contexts. - * The `time` crate is re-exported from the crate root. - * The `FromForm`, `Responder`, and `UriDisplay` derives now fully support generics. - * Added helper functions to `serde` submodules. - * The [`Shield`] HSTS preload header now includes `includeSubdomains`. - * Logging ignores `write!` errors if `stdout` disappears, preventing panics. - * Added [`Client::terminate()`] to run graceful shutdown in testing. - * Shutdown now terminates the `async` runtime, never the process. - -### HTTP - - * Introduced [`Host`] and the [`&Host`] request guard. - * Added `Markdown` (`text/markdown`) as a known media type. - * Added [`RawStr::percent_encode_bytes()`]. - * `NODELAY` is now enabled on all connections by default. - * The TLS implementation handles handshakes off the main task, improving DoS resistance. - -### Request - - * Added [`Request::host()`] to retrieve the client-requested host. - -### Trait Implementations - - * `Arc`, `Box` where `T: Responder` now implement `Responder`. - * [`Method`] implements `Serialize` and `Deserialize`. - * [`MediaType`] and [`ContentType`] implement `Eq`. - -### Updated Dependencies - - * The `time` dependency was updated to `0.3`. - * The `handlebars` dependency was updated to `4.0`. - * The `memcache` dependency was updated to `0.16`. - * The `rustls` dependency was updated to `0.20`. - -## Infrastructure - - * Rocket now uses the 2021 edition of Rust. - -[`(ContentType, T)`]: https://api.rocket.rs/v0.5-rc/rocket/response/content/index.html#usage -[v0.4 to v0.5 migration guide]: https://rocket.rs/v0.5-rc/guide/upgrading/ -[FAQ]: https://rocket.rs/v0.5-rc/guide/faq/ -[`Rocket::launch()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.launch -[`ErrorKind::Shutdown`]: https://api.rocket.rs/v0.5-rc/rocket/error/enum.ErrorKind.html#variant.Shutdown -[shutdown fairings]: https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#shutdown -[`Client::terminate()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.Client.html#method.terminate -[`rocket::execute()`]: https://api.rocket.rs/v0.5-rc/rocket/fn.execute.html -[`CookieJar::get_pending()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html#method.get_pending - -# Version 0.5.0-rc.1 (Jun 09, 2021) +# Version 0.5.0 (Nov XX, 2023) ## Major Features and Improvements @@ -322,19 +6,24 @@ This release introduces the following major features and improvements: * Support for [compilation on Rust's stable] release channel. * A rewritten, fully asynchronous core with support for [`async`/`await`]. + * WebSocket support via [`rocket_ws`]. * [Feature-complete forms support] including multipart, collections, [ad-hoc validation], and [context](https://rocket.rs/v0.5-rc/guide/requests/#context). * [Sentinels]: automatic verification of application state at start-up to prevent runtime errors. - * [Graceful shutdown] with configurable signaling, grace periods, notification via [`Shutdown`]. + * [Graceful shutdown] with configurable signaling, grace periods, [notification], and + [shutdown fairings]. * An entirely new, flexible and robust [configuration system] based on [Figment]. * Typed [asynchronous streams] and [Server-Sent Events] with generator syntax. + * Asynchronous database pooling support via [`rocket_db_pools`]. + * Support for [mutual TLS] and client [`Certificate`]s. * Automatic support for HTTP/2 including `h2` ALPN. * Graduation of `json`, `msgpack`, and `uuid` `rocket_contrib` [features into core]. * An automatically enabled [`Shield`]: security and privacy headers for all responses. * Type-system enforced [incoming data limits] to mitigate memory-based DoS attacks. * Compile-time URI literals via a fully revamped [`uri!`] macro. + * [Request connection upgrade APIs] with support for raw I/O with the client. * Full support for [UTF-8 characters] in routes and catchers. - * Precise detection of unmanaged state and missing database, template fairings with [sentinels]. + * Precise detection of missing managed state, databases, and templating with [sentinels]. * Typed [build phases] with strict application-level guarantees. * [Ignorable segments]: wildcard route matching with no typing restrictions. * First-class [support for `serde`] for built-in guards and types. @@ -350,7 +39,7 @@ This release introduces the following major features and improvements: * Support for custom config profiles and [automatic typed config extraction]. * Rewritten, zero-copy, RFC compliant URI parsers with support for URI-[`Reference`]s. * Multi-segment parameters (``) which match _zero_ segments. - * A [`request::local_cache!`] macro for request-local storage of non-uniquely typed values. + * A [`local_cache!`] macro for request-local storage of non-uniquely typed values. * A [`CookieJar`] without "one-at-a-time" limitations. * [Singleton fairings] with replacement and guaranteed uniqueness. * [Data limit declaration in SI units]: "2 MiB", `2.mebibytes()`. @@ -366,9 +55,9 @@ This release introduces the following major features and improvements: ## Support for Rust Stable -As a result of support for Rust stable (Rust 2021 Edition and beyond), the -`#![feature(..)]` crate attribute is no longer required for Rocket applications. -The complete canonical example with a single `hello` route becomes: +As a result of support for Rust stable (Rust 2021 Edition and beyond), +`#![feature(..)]` crate attributes are no longer required to use Rocket. The +complete canonical example with a single `hello` route becomes: ```rust #[macro_use] extern crate rocket; @@ -411,7 +100,8 @@ fn rocket() -> _ { ## Breaking Changes -This release includes many breaking changes. The most significant changes are listed below. +This release includes many breaking changes. For a walkthrough guide on handling these changes, see +the [v0.4 to v0.5 migration guide]. The most significant changes are listed below. ### Silent Changes @@ -454,87 +144,111 @@ We **strongly** advise all application authors to review this list carefully. than the defaults. * [`CookieJar`] `get()`s do not return cookies added during request handling. See [`CookieJar`#pending]. + * `Hash` `impl`s for `MediaType` and `ContentType` no longer consider media type parameters. + * When requested, the `FromForm` implementations of `Vec` and `Map`s are now properly lenient. + * To agree with browsers, the `[` and `]` characters are now accepted in URI paths. + * The `[` and `]` characters are no longer encoded by [`uri!`]. + * The `Secure` cookie flag is set by default for all cookies when serving over TLS. + * Removal cookies have `SameSite` set to `Lax` by default. + * [`MediaType::JavaScript`] is now `text/javascript`. ### Contrib Graduation - * The `rocket_contrib` crate has been deprecated and should no longer be used. +The `rocket_contrib` crate is deprecated and the functionality moved to other `rocket` crates. The +[contrib deprecation upgrade guide] provides a walkthrough on migrating. The relevant changes are: + * Several features previously in `rocket_contrib` were merged into `rocket` itself: - * `json`, `msgpack`, and `uuid` are now [features of `rocket`]. - * Moved `rocket_contrib::json` to [`rocket::serde::json`]. - * Moved `rocket_contrib::msgpack` to [`rocket::serde::msgpack`]. - * Moved `rocket_contrib::uuid` to [`rocket::serde::uuid`]. - * Moved `rocket_contrib::helmet` to [`rocket::shield`]. [`Shield`] is enabled by default. - * Moved `rocket_contrib::serve` to [`rocket::fs`], `StaticFiles` to [`rocket::fs::FileServer`]. - * Removed the now unnecessary `Uuid` and `JsonValue` wrapper types. - * Removed headers in `Shield` that are no longer respected by browsers. + - `json`, `msgpack`, and `uuid` are now [features of `rocket`]. + - Moved `rocket_contrib::json` to [`rocket::serde::json`]. + - Moved `rocket_contrib::msgpack` to [`rocket::serde::msgpack`]. + - Moved `rocket_contrib::uuid` to [`rocket::serde::uuid`]. + - Moved `rocket_contrib::helmet` to [`rocket::shield`]. [`Shield`] is enabled by default. + - Moved `rocket_contrib::serve` to [`rocket::fs`], `StaticFiles` to [`rocket::fs::FileServer`]. + - Removed the now unnecessary `Uuid` and `JsonValue` wrapper types. + - Removed headers in `Shield` that are no longer respected by browsers. * The remaining features from `rocket_contrib` are now provided by separate crates: - * Replaced `rocket_contrib::templates` with [`rocket_dyn_templates`]. - * Replaced `rocket_contrib::databases` with [`rocket_sync_db_pools`] and [`rocket_db_pools`]. - * These crates are versioned and released independently of `rocket`. - * `rocket_contrib::databases::DbError` is now `rocket_sync_db_pools::Error`. - * Removed `redis`, `mongodb`, and `mysql` integrations which have upstream `async` drivers. - * The [`#[database]`](https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/attr.database.html) + - Replaced `rocket_contrib::templates` with [`rocket_dyn_templates`]. + - Replaced `rocket_contrib::databases` with [`rocket_sync_db_pools`] and [`rocket_db_pools`]. + - These crates are versioned and released independently of `rocket`. + - `rocket_contrib::databases::DbError` is now `rocket_sync_db_pools::Error`. + - Removed `redis`, `mongodb`, and `mysql` integrations which have upstream `async` drivers. + - The [`#[database]`](https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/attr.database.html) attribute generates an [`async run()`] method instead of `Deref` implementations. ### General +The following breaking changes apply broadly and are likely to cause compile-time errors. + * [`Rocket`] is now generic over a [phase] marker: - * APIs operate on `Rocket`, `Rocket`, `Rocket`, or `Rocket` as + - APIs operate on `Rocket`, `Rocket`, `Rocket`, or `Rocket` as needed. - * The phase marker statically enforces state transitions in `Build`, `Ignite`, `Orbit` order. - * `rocket::ignite()` is now [`rocket::build()`], returns a `Rocket`. - * [`Rocket::ignite()`] transitions to the `Ignite` phase. This is run automatically on launch as + - The phase marker statically enforces state transitions in `Build`, `Ignite`, `Orbit` order. + - `rocket::ignite()` is now [`rocket::build()`] and returns a `Rocket`. + - [`Rocket::ignite()`] transitions to the `Ignite` phase. This is run automatically on launch as needed. - * Ignition finalizes configuration, runs `ignite` fairings, and verifies [sentinels]. - * [`Rocket::launch()`] transitions into the `Orbit` phase and starts the server. - * Methods like [`Request::rocket()`] that refer to a live Rocket instance return an + - Ignition finalizes configuration, runs `ignite` fairings, and verifies [sentinels]. + - [`Rocket::launch()`] transitions into the `Orbit` phase and starts the server. + - Methods like [`Request::rocket()`] that refer to a live Rocket instance return an `&Rocket`. * [Fairings] have been reorganized and restructured for `async`: - * Replaced `attach` fairings with `ignite` fairings. Unlike `attach` fairings, which ran + - Replaced `attach` fairings with `ignite` fairings. Unlike `attach` fairings, which ran immediately at the time of attachment, `ignite` fairings are run when transitioning into the `Ignite` phase. - * Replaced `launch` fairings with `liftoff` fairings. `liftoff` fairings are always run, even in + - Replaced `launch` fairings with `liftoff` fairings. `liftoff` fairings are always run, even in local clients, after the server begins listening and the concrete port is known. * Introduced a new [configuration system] based on [Figment]: - * The concept of "environments" is replaced with "profiles". - * `ROCKET_ENV` is superseded by `ROCKET_PROFILE`. - * `ROCKET_LOG` is superseded by `ROCKET_LOG_LEVEL`. - * Profile names can now be arbitrarily chosen. The `dev`, `stage`, and `prod` profiles carry no + - The concept of "environments" is replaced with "profiles". + - `ROCKET_ENV` is superseded by `ROCKET_PROFILE`. + - `ROCKET_LOG` is superseded by `ROCKET_LOG_LEVEL`. + - Profile names can now be arbitrarily chosen. The `dev`, `stage`, and `prod` profiles carry no special meaning. - * The `debug` and `release` profiles are the default profiles for the debug and release + - The `debug` and `release` profiles are the default profiles for the debug and release compilation profiles. - * A new specially recognized `default` profile specifies defaults for all profiles. - * The `global` profile has highest precedence, followed by the selected profile, followed by + - A new specially recognized `default` profile specifies defaults for all profiles. + - The `global` profile has highest precedence, followed by the selected profile, followed by `default`. - * Added support for limits specified in SI units: "1 MiB". - * Renamed `LoggingLevel` to [`LogLevel`]. - * Inlined error variants into the [`Error`] structure. - * Changed the type of `workers` to `usize` from `u16`. - * Changed accepted values for `keep_alive`: it is disabled with `0`, not `false` or `off`. - * Disabled the `secrets` feature (for private cookies) by default. - * Removed APIs related to "extras". Typed values can be extracted from the configured `Figment`. - * Removed `ConfigBuilder`: all fields of [`Config`] are public with constructors for each field + - Added support for limits specified in SI units: "1 MiB". + - Renamed `LoggingLevel` to [`LogLevel`]. + - Inlined error variants into the [`Error`] structure. + - Changed the type of `workers` to `usize` from `u16`. + - Changed accepted values for `keep_alive`: it is disabled with `0`, not `false` or `off`. + - Disabled the `secrets` feature (for private cookies) by default. + - Removed APIs related to "extras". Typed values can be extracted from the configured `Figment`. + - Removed `ConfigBuilder`: all fields of [`Config`] are public with constructors for each field type. * Many functions, traits, and trait bounds have been modified for `async`: - * [`FromRequest`], [`Fairing`], [`catcher::Handler`], [`route::Handler`], and [`FromData`] use + - [`FromRequest`], [`Fairing`], [`catcher::Handler`], [`route::Handler`], and [`FromData`] use `#[async_trait]`. - * [`NamedFile::open`] is now an `async` function. - * Added [`Request::local_cache_async()`] for use in async request guards. - * Unsized `Response` bodies must be [`AsyncRead`] instead of `Read`. - * Automatically sized `Response` bodies must be [`AsyncSeek`] instead of `Seek`. - * The `local` module is split into two: [`rocket::local::asynchronous`] and + - [`NamedFile::open`] is now an `async` function. + - Added [`Request::local_cache_async()`] for use in async request guards. + - Unsized `Response` bodies must be [`AsyncRead`] instead of `Read`. + - Automatically sized `Response` bodies must be [`AsyncSeek`] instead of `Seek`. + - The `local` module is split into two: [`rocket::local::asynchronous`] and [`rocket::local::blocking`]. * Functionality and features requiring Rust nightly were removed: - * Removed the `Try` implementation on [`Outcome`] which allowed using `?` with `Outcome`s. The + - Removed the `Try` implementation on [`Outcome`] which allowed using `?` with `Outcome`s. The recommended replacement is the [`rocket::outcome::try_outcome!`] macro or the various combinator functions on `Outcome`. - * [`Result` implements `Responder`] only when both `T` and `E` implement `Responder`. The + - [`Result` implements `Responder`] only when both `T` and `E` implement `Responder`. The new [`Debug`] wrapping responder replaces `Result`. - * APIs which used the `!` type to now use [`std::convert::Infallible`]. + - APIs which used the `!` type to now use [`std::convert::Infallible`]. + * [`IntoOutcome`] was overhauled to supplant methods now removed in `Outcome`. + - `IntoOutcome::into_outcome()` is now `or_error()`. + - `IntoOutcome` is implemented for all `Outcome` type aliases. + - `Outcome::forward()` requires specifying a status code. + - `Outcome::from()` and `Outcome::from_or_forward()` were removed. * [`Rocket::register()`] now takes a base path to scope catchers under as its first argument. * `ErrorKind::Collision` has been renamed to [`ErrorKind::Collisions`]. - -[phase]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#phases + * TLS config values are only available when the `tls` feature is enabled. + * [`MediaType::with_params()`] and [`ContentType::with_params()`] are now builder methods. + * Content-Type [`content`] responder type names are now prefixed with `Raw`. + * The `content::Plain` responder is now called `content::RawText`. + * The `content::Custom` responder was removed in favor of [`(ContentType, T)`]. + * Removed `CookieJar::get_private_pending()` in favor of [`CookieJar::get_pending()`]. + * The [`local_cache!`] macro accepts fewer types. Use [`local_cache_once!`] as appropriate. + * [`Rocket::launch()`] allows `Rocket` recovery by returning the instance after shutdown. + * `ErrorKind::Runtime` was removed; [`ErrorKind::Shutdown`] was added. + * `Outcome::Failure` was renamed to [`Outcome::Error`]. ### Routing and URIs @@ -555,14 +269,19 @@ We **strongly** advise all application authors to review this list carefully. * `Origin::path()` and `Origin::query()` return `&RawStr` instead of `&str`. * The type of `Route::name` is now `Option>`. * `Route::set_uri` was replaced with [`Route::map_base()`]. - * `Route::uri()` returns a new [`RouteUri`] type. + * The `Route::uri` field is now of type [`RouteUri`]. * `Route::base` was removed in favor of `Route.uri().base()`. - -[`RouteUri`]: https://api.rocket.rs/v0.5-rc/rocket/route/struct.RouteUri.html + * [Route `Forward` outcomes] are now associated with a `Status`. + * The status codes used when built-in guards forward were changed: + - Route parameter `FromParam` errors now forward as 422. + - Query parameter errors now forward as 422. + - Incorrect form content-type errors forwards as 413. + - `&Host`, `&Accept`, `&ContentType`, `IpAddr`, and `SocketAddr` all forward + with a 500. ### Data and Forms - * `Data` now has a lifetime: `Data<'r>`. + * `Data` now has a lifetime generic: `Data<'r>`. * [`Data::open()`] indelibly requires a data limit. * Removed `FromDataSimple`. Use [`FromData`] and [`local_cache!`] or [`local_cache_once!`]. * All [`DataStream`] APIs require limits and return [`Capped`] types. @@ -571,6 +290,7 @@ We **strongly** advise all application authors to review this list carefully. * Replaced `FromFormValue` with [`FromFormField`]. All `T: FromFormField` implement `FromForm`. * Form field values are percent-decoded before calling [`FromFormField`] implementations. * Renamed the `#[form(field = ...)]` attribute to `#[field(name = ...)]`. + * [Custom form errors] must now specify an associated `Status`. ### Request Guards @@ -581,7 +301,7 @@ We **strongly** advise all application authors to review this list carefully. * Replaced `Request::get_query_value()` with `Request::query_value()`. * Replaced `Segments::into_path_buf()` with `Segments::to_path_buf()`. * Replaced `Segments` and `QuerySegments` with [`Segments` and `Segments`]. - * [`Flash`] constructors to take `Into` instead of `AsRef`. + * [`Flash`] constructors now take `Into` instead of `AsRef`. * The `State<'_, T>` request guard is now `&State`. * Removed a lifetime from [`FromRequest`]: `FromRequest<'r>`. * Removed a lifetime from [`FlashMessage`]: `FlashMessage<'_>`. @@ -597,10 +317,12 @@ We **strongly** advise all application authors to review this list carefully. * Removed inaccurate "chunked body" types and variants. * Removed `Responder` `impl` for `Response`. Prefer custom responders with `#[derive(Responder)]`. * Removed the unused reason phrase from `Status`. + * The types of responders in [`response::status`] were unified to all be of + the form `Status(R)`. ## General Improvements -In addition to new features and major improvements, Rocket saw the following improvements: +In addition to new features and changes, Rocket saw the following improvements: ### General @@ -632,19 +354,73 @@ In addition to new features and major improvements, Rocket saw the following imp * Added support to [`UriDisplayQuery`] for C-like enums. * The [`UriDisplayQuery`] derive now recognizes the `#[field]` attribute for field renaming. * `Client` method builders accept `TryInto` allowing a `uri!()` to be used directly. - * [`Redirect`] now accepts a `TryFrom`, allowing fragment parts. + * [`Rocket`] is now `#[must_use]`. + * Support for HTTP/2 can be disabled by disabling the default `http2` crate feature. + * Added [`rocket::execute()`] for executing Rocket's `launch()` future. + * Added the [`context!`] macro to [`rocket_dyn_templates`] for ad-hoc template contexts. + * The `time` crate is re-exported from the crate root. + * The `FromForm`, `Responder`, and `UriDisplay` derives now fully support generics. + * Added helper functions to `serde` submodules. + * The [`Shield`] HSTS preload header now includes `includeSubdomains`. + * Logging ignores `write!` errors if `stdout` disappears, preventing panics. + * Added [`Client::terminate()`] to run graceful shutdown in testing. + * Shutdown now terminates the `async` runtime, never the process. + * Added a [`local_cache_once!`] macro for request-local storage. + * Final launch messages are now _always_ logged, irrespective of profile. + * Only functions that return `Rocket` are now `#[must_use]`, not all `Rocket

`. + * Fixed mismatched form field names in errors under certain conditions in [`FromForm`] derive. + * The [`FromForm`] derive now collects _all_ errors that occur. + * Data pools are now gracefully shutdown in [`rocket_sync_db_pools`]. + * Added [`Metadata::render()`] in [`rocket_dyn_templates`] for direct template rendering. + * Rocket salvages more information from malformed requests for error catchers. + * The `cookie` `secure` feature is now properly conditionally enabled. + * Data before encapsulation boundaries in TLS keys is allowed and ignored. + * Support for TLS keys in SEC1 format was added. + * Rocket now warns when a known secret key is configured. + * A panic that could occur on shutdown in `rocket_sync_db_pools` was fixed. + * Added a [`max_blocking`] configuration parameter to control number of blocking threads. + * Added an [`ip_header`] "real IP" header configuration parameter. + * A [`pool()`] method is emitted by [`rocket_sync_db_pools`] for code-generated pools. + * Data guards are now eligible [sentinels]. + * Raw binary form field data can be retrieved using the `&[u8]` form guard. + * Added [`TempFile::open()`] to stream `TempFile` data. + * mTLS certificates can be set on local requests with [`LocalRequest::identity()`]. + * Added [`Error::pretty_print()`] for pretty-printing errors like Rocket. + * Warnings are logged when data limits are reached. + * A warning is emitted when `String` is used as a route parameter. + * Configuration provenance information is logged under the `debug` log level. + * Logging of `Outcome`s now includes the relevant status code. + * `Span::mixed_site()` is used in codegen to reduce errant `clippy` warnings. ### HTTP * Added support for HTTP/2, enabled by default via the `http2` crate feature. - * Added AVIF (`image/avif`) as a known media type. - * Added `EventStream` (`text/event-stream`) as a known media type. * Added a `const` constructor for `MediaType`. - * Added aliases `Text`, `Bytes` for the `Plain`, `Binary` media types, respectively. * Introduced [`RawStrBuf`], an owned `RawStr`. * Added many new "pattern" methods to [`RawStr`]. * Added [`RawStr::percent_encode()`] and [`RawStr::strip()`]. * Added support for unencoded query characters in URIs that are frequently sent by browsers. + * Introduced [`Host`] and [`&Host`] request guards. + * Added [`RawStr::percent_encode_bytes()`]. + * `NODELAY` is now enabled on all connections by default. + * The TLS implementation handles handshakes off the main task, improving DoS resistance. + +### Known Media Types + + * Added AVIF: `image/avif`. + * Added `EventStream`: `text/event-stream`. + * Added `Markdown`: `text/markdown`. + * Added `MP3`: `audio/mpeg`. + * Added `CBZ`: `application/vnd.comicbook+zip`, extension `.cbz`. + * Added `CBR`: `application/vnd.comicbook-rar`, extension `.cbr`. + * Added `RAR`: `application/vnd.rar`, extension `.rar`. + * Added `EPUB`: `application/epub+zip`, extension `.epub`. + * Added `OPF`: `application/oebps-package+xml`, extension `.opf`. + * Added `XHTML`: `application/xhtml+xml`, extension `.xhtml`. + * Added `Text` as an alias for the `Plain` media type. + * Added `Bytes` as an alias for the `Binary` media type. + * Added `.mjs` as known JavaScript extension. + * Added '.exe', '.iso', '.dmg' as known extensions. ### Request @@ -656,6 +432,7 @@ In addition to new features and major improvements, Rocket saw the following imp * `Json`, `MsgPack` accept `T: Deserialize`, not only `T: DeserializeOwned`. * Diesel SQLite connections in `rocket_sync_db_pools` use better defaults. * The default number of workers for synchronous database pools is now `workers * 4`. + * Added [`Request::host()`] to retrieve the client-requested host. ### Response @@ -667,6 +444,7 @@ In addition to new features and major improvements, Rocket saw the following imp [`#[derive(Responder)]`](https://api.rocket.rs/v0.5-rc/rocket/derive.Responder.html). * The `Server` header is only set if one isn't already set. * Accurate `Content-Length` headers are sent even for partially read `Body`s. + * [`Redirect`] now accepts a `TryFrom`, allowing fragment parts. ### Trait Implementations @@ -698,46 +476,66 @@ In addition to new features and major improvements, Rocket saw the following imp * Implemented `Serialize`, `Deserialize`, `UriDisplay` and `FromUriParam` for `uuid::Uuid` * Implemented `Serialize`, `Deserialize` for `RawStr`. * Implemented `Serialize`, `Deserialize` for all URI types. - -### Updated Dependencies - - * The `serde` dependency was introduced (`1.0`). - * The `futures` dependency was introduced (`0.3`). - * The `state` dependency was updated to `0.5`. - * The `time` dependency was updated to `0.2`. - * The `binascii` dependency was introduced (`0.1`). - * The `ref-cast` dependency was introduced (`1.0`). - * The `atomic` dependency was introduced (`0.5`). - * The `parking_lot` dependency was introduced (`0.11`). - * The `ubtye` dependency was introduced (`0.10`). - * The `figment` dependency was introduced (`0.10`). - * The `rand` dependency was introduced (`0.8`). - * The `either` dependency was introduced (`1.0`). - * The `pin-project-lite` dependency was introduced (`0.2`). - * The `indexmap` dependency was introduced (`1.0`). - * The `tempfile` dependency was introduced (`3.0`). - * The `async-trait` dependency was introduced (`0.1`). - * The `async-stream` dependency was introduced (`0.3`). - * The `multer` dependency was introduced (`2.0`). - * The `tokio` dependency was introduced (`1.6.1`). - * The `tokio-util` dependency was introduced (`0.6`). - * The `tokio-stream` dependency was introduced (`0.1.6`). - * The `bytes` dependency was introduced (`1.0`). - * The `rmp-serde` dependency was updated to `0.15`. - * The `uuid` dependency was updated to `0.8`. - * The `tera` dependency was updated to `1.10`. - * The `handlebars` dependency was updated to `3.0`. - * The `normpath` dependency was introduced (`0.3`). - * The `postgres` dependency was updated to `0.19`. - * The `rusqlite` dependency was updated to `0.25`. - * The `r2d2_sqlite` dependency was updated to `0.18`. - * The `memcache` dependency was updated to `0.15`. + * Implemented `Responder` for `Arc`, `Box` where `T: Responder`. + * Implemented `Serialize` and `Deserialize` for [`Method`]. + * Implemented `Eq` for [`MediaType`] and [`ContentType`]. + * Implemented `Responder` for `Box`. + * Implemented `FromForm` for `Arc`. + * Implemented `Fairing` for `Arc`. + * Implemented `Serialize` and `Deserialize` for `Status`. + +### Dependency Changes + + * `serde` was introduced (`1.0`). + * `futures` was introduced (`0.3`). + * `binascii` was introduced (`0.1`). + * `ref-cast` was introduced (`1.0`). + * `atomic` was introduced (`0.5`). + * `parking_lot` was introduced (`0.11`). + * `ubtye` was introduced (`0.10`). + * `figment` was introduced (`0.10`). + * `rand` was introduced (`0.8`). + * `either` was introduced (`1.0`). + * `pin-project-lite` was introduced (`0.2`). + * `indexmap` was introduced (`2.0`). + * `tempfile` was introduced (`3.0`). + * `async-trait` was introduced (`0.1`). + * `async-stream` was introduced (`0.3`). + * `multer` was introduced (`2.0`). + * `tokio` was introduced (`1.6.1`). + * `tokio-util` was introduced (`0.6`). + * `tokio-stream` was introduced (`0.1.6`). + * `bytes` was introduced (`1.0`). + * `normpath` was introduced (`1`). + * `state` was updated to `0.6`. + * `rmp-serde` was updated to `0.15`. + * `uuid` was updated to `0.8`. + * `tera` was updated to `1.10`. + * `postgres` was updated to `0.19`. + * `rusqlite` was updated to `0.25`. + * `r2d2_sqlite` was updated to `0.18`. + * `time` was updated to `0.3`. + * `handlebars` was updated to `4.0`. + * `memcache` was updated to `0.16`. + * `rustls` was updated to `0.21`. + * `tokio-rustls` was updated to `0.24`. + * `syn` was updated to `2`. + * `diesel` was updated to `2.0`. + * `sqlx` was updated to `0.7`. + * `notify` was updated to `6`. + * `criterion` was updated to `0.4`. + * `cookie` was updated to `0.18`. + * `yansi` was updated to `1.0`. + * `atty` was removed. ## Infrastructure - * Rocket now uses the 2018 edition of Rust. +The following changes were made to the project's infrastructure: + + * Rocket now uses the 2021 edition of Rust. + * Added a [v0.4 to v0.5 migration guide] and [FAQ] to Rocket's website. * Added visible `use` statements to examples in the guide. - * Split examples into a separate workspace from the non-example crates. + * Split examples into a separate workspace for easier testing. * Updated documentation for all changes. * Fixed many typos, errors, and broken links throughout documentation and examples. * Improved the general robustness of macros, and the quality and frequency of error messages. @@ -745,8 +543,12 @@ In addition to new features and major improvements, Rocket saw the following imp * Fixed the SPDX license expressions in `Cargo.toml` files. * Added support to `test.sh` for a `+` flag (e.g. `+stable`) to pass to `cargo`. * Added support to `test.sh` for extra flags to be passed on to `cargo`. - * Migrated CI to Github Actions. + * UI tests are now allowed to fail by the CI to avoid false negatives. + * The GitHub CI workflow was updated to use maintained actions. + * The CI now frees disk space before proceeding to avoid out-of-disk errors. + * All workspaces now use `resolver = 2`. +[phase]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#phases [`async`/`await`]: https://rocket.rs/v0.5-rc/guide/overview/#async-routes [compilation on Rust's stable]: https://rocket.rs/v0.5-rc/guide/getting-started/#installing-rust [Feature-complete forms support]: https://rocket.rs/v0.5-rc/guide/requests/#forms @@ -804,7 +606,7 @@ In addition to new features and major improvements, Rocket saw the following imp [asynchronous streams]: https://rocket.rs/v0.5-rc/guide/responses/#async-streams [Server-Sent Events]: https://api.rocket.rs/v0.5-rc/rocket/response/stream/struct.EventStream.html [`fs::relative!`]: https://api.rocket.rs/v0.5-rc/rocket/fs/macro.relative.html -[`Shutdown`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Shutdown.html +[notification]: https://api.rocket.rs/v0.5-rc/rocket/struct.Shutdown.html [`Rocket`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html [`rocket::build()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.build [`Rocket::ignite()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.ignite @@ -886,13 +688,45 @@ In addition to new features and major improvements, Rocket saw the following imp [`MediaType`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html [`ContentType`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.ContentType.html [`Method`]: https://api.rocket.rs/v0.5-rc/rocket/http/enum.Method.html +[`(ContentType, T)`]: https://api.rocket.rs/v0.5-rc/rocket/response/content/index.html#usage +[v0.4 to v0.5 migration guide]: https://rocket.rs/v0.5-rc/guide/upgrading/ +[contrib deprecation upgrade guide]: https://rocket.rs/v0.5-rc/guide/upgrading/#contrib-deprecation +[FAQ]: https://rocket.rs/v0.5-rc/guide/faq/ +[`Rocket::launch()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.launch +[`ErrorKind::Shutdown`]: https://api.rocket.rs/v0.5-rc/rocket/error/enum.ErrorKind.html#variant.Shutdown +[shutdown fairings]: https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#shutdown +[`Client::terminate()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.Client.html#method.terminate +[`rocket::execute()`]: https://api.rocket.rs/v0.5-rc/rocket/fn.execute.html +[`CookieJar::get_pending()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html#method.get_pending +[`Metadata::render()`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Metadata.html#method.render +[`pool()`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/example/struct.ExampleDb.html#method.pool +[`Request::client_ip()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.client_ip +[`max_blocking`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.max_blocking +[`ip_header`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.ip_header +[`LocalRequest::identity()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.identity +[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open +[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print +[Request connection upgrade APIs]: https://api.rocket.rs/v0.5-rc/rocket/struct.Response.html#upgrading +[`rocket_ws`]: https://api.rocket.rs/v0.5-rc/rocket_ws/ +[registering]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Response.html#method.add_upgrade +[`IoHandler`]: https://api.rocket.rs/v0.5-rc/rocket/data/trait.IoHandler.html +[`response::status`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/index.html +[Custom form errors]: https://api.rocket.rs/v0.5-rc/rocket/form/error/enum.ErrorKind.html#variant.Custom +[`request::Outcome`]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward +[Route `Forward` outcomes]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward +[`Outcome::Error`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/enum.Outcome.html#variant.Error +[`IntoOutcome`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/trait.IntoOutcome.html +[`MediaType::JavaScript`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html#associatedconstant.JavaScript +[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open +[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print +[`RouteUri`]: https://api.rocket.rs/v0.5-rc/rocket/route/struct.RouteUri.html # Version 0.4.10 (May 21, 2021) ## Core - * [[`3276b8`]] Removed used of `unsafe` in `Origin::parse_owned()`, fixing a - soundness issue. + * [[`3276b8`]] Removed `unsafe` in `Origin::parse_owned()`, fixing a soundness + issue. [`3276b8`]: https://github.com/SergioBenitez/Rocket/commit/3276b8 From f25954fef011eb8fd5a1563ac57525bf007da34b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 12 Nov 2023 17:27:29 -0800 Subject: [PATCH 029/178] Add WebSocket section to upgrading guide. --- site/guide/01-upgrading.md | 43 ++++++++++++++++++++++++++++++++++++++ site/tests/Cargo.toml | 3 +++ 2 files changed, 46 insertions(+) diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index 65f23013d9..cdee2af4e9 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -809,6 +809,49 @@ fn stream(n: Option) -> EventStream![] { [async streams]: ../responses/#async-streams [chat example]: @example/chat +### WebSockets + +Rocket v0.5 introduces support for HTTP connection upgrades via a new [upgrade +API]. The API allows responders to take over an HTTP connection and perform raw +I/O with the client. In other words, an HTTP connection can be _upgraded_ to any +protocol, including HTTP WebSockets! + +The newly introduced [`rocket_ws`] library takes advantage of the new API to +implement first-class support for WebSockets entirely outside of Rocket's core. +The simplest use of the library, implementing an echo server and showcasing that +the incoming message stream is `async`, looks like this: + +```rust +# use rocket::get; +# use rocket_ws as ws; + +#[get("/echo")] +fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { + ws.stream(|io| io) +} +``` + +The simplified [async streams] generator syntax can also be used: + +```rust +# use rocket::get; +# use rocket_ws as ws; + +#[get("/echo")] +fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::Stream! { ws => + for await message in ws { + yield message?; + } + } +} +``` + +For complete usage details, see the [`rocket_ws`] documentation. + +[upgrade API]: @api/rocket/response/struct.Response.html#upgrading +[`rocket_ws`]: @api/rocket_ws + ## Getting Help If you run into any issues upgrading, we encourage you to ask questions via diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index 561c9a54ba..11269a589e 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -20,3 +20,6 @@ features = ["tera"] [dev-dependencies.rocket_db_pools] path = "../../contrib/db_pools/lib" features = ["sqlx_sqlite"] + +[dev-dependencies.rocket_ws] +path = "../../contrib/ws/lib" From 6bcab27101599b5eb9bab8c4fa956ab8730196fe Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 12 Nov 2023 17:27:08 -0800 Subject: [PATCH 030/178] Initial news article for v0.5 release. --- site/news/2023-05-26-version-0.5.md | 325 ++++++++++++++++++++++++++++ site/news/index.toml | 16 ++ 2 files changed, 341 insertions(+) create mode 100644 site/news/2023-05-26-version-0.5.md diff --git a/site/news/2023-05-26-version-0.5.md b/site/news/2023-05-26-version-0.5.md new file mode 100644 index 0000000000..8249b1f37a --- /dev/null +++ b/site/news/2023-05-26-version-0.5.md @@ -0,0 +1,325 @@ +# Rocket v0.5: Stable, Async, Sentinels, Figment, Shield, Streams, SSE, WebSockets, & More! + +

+ +Four years, almost a thousand commits, and over a thousand issues, discussions, +and PRs later, I am ~~relieved~~ thrilled to announce the general availability +of Rocket v0.5. + +> **Rocket** is a backend web framework for Rust with a focus on usability, +> security, extensibility, and speed. Rocket makes it simple to write secure web +> applications without sacrificing usability or performance. + +We encourage all users to upgrade. For a guided upgrade from Rocket v0.4 to +Rocket v0.5, please consult the newly available [upgrading guide]. Rocket v0.4 +will continue to be supported and receive security updates until the next major +release. + +[upgrading guide]: ../../guide/upgrading + +## What's New? + +Almost every aspect has been reevaluated with a focus on usability, security, +and consistency across the library and [broader ecosystem]. The changes are +numerous, so we focus on the most impactful changes here and encourage everyone +to read the [CHANGELOG] for a complete list. For answers to frequently asked +questions, see the new [FAQ]. + +[broader ecosystem]: ../../guide/faq/#releases +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0/CHANGELOG.md +[FAQ]: ../../guide/faq + +### โš“ Support for Stable `rustc` + +Rocket v0.5 compiles and builds on Rust stable with an entirely asynchronous +core. This means that you can compile Rocket application with `rustc` from the +stable release channel. + +Using the stable release channel ensures that _no_ breakages will occur when +upgrading your Rust compiler or Rocket. That being said, Rocket continues to +take advantage of features only present in the nightly channel. + +### ๐Ÿ“ฅ Async I/O + +The new asynchronous core requires an async runtime to run. The new +[`launch`] and [`main`] attributes simplify starting a runtime suitable for +running Rocket applications. You should use [`launch`] whenever possible. + +Additionally, the `rocket::ignite()` function has been renamed to +[`rocket::build()`]; calls to the function or method should be replaced +accordingly. Together, these two changes result in the following diff to what +was previously the `main` function: + +```diff +- fn main() { +- rocket::ignite().mount("/hello", routes![hello]).launch(); +- } ++ #[launch] ++ fn rocket() -> _ { ++ rocket::build().mount("/hello", routes![hello]) ++ } +``` + +### ๐Ÿ’‚ Sentinels + +Rocket v0.5 introduces [sentinels]. Entirely unique to Rocket, sentinels offer +an automatic last line of defense against runtime errors by enabling any type +that appears in a route to abort application launch if invalid conditions are +detected. For example, the [`&State`] guard in v0.5 is a [`Sentinel`] that +aborts launch if the type `T` is not in managed state, thus preventing +associated runtime errors. + +You should consider implementing `Sentinel` for your types if you have guards +(request, data, form, etc.) or responders that depend on `Rocket` state to +function properly. For example, consider a `MyResponder` that expects: + + * An error catcher to be registered for the `400` status code. + * A specific type `T` to be in managed state. + +Making `MyResponder` a sentinel that guards against these conditions is as +simple as: + +```rust +use rocket::{Rocket, Ignite, Sentinel}; +# struct MyResponder; +# struct T; + +impl Sentinel for MyResponder { + fn abort(r: &Rocket) -> bool { + !r.catchers().any(|c| c.code == Some(400)) || r.state::().is_none() + } +} +``` + +[sentinels]: @api/rocket/trait.Sentinel.html +[`Sentinel`]: @api/rocket/trait.Sentinel.html +[`&State`]: @api/rocket/struct.State.html + + +### ๐Ÿ›ก๏ธ Shield + +### ๐ŸŒŠ Streams and SSE + +Rocket v0.5 introduces real-time, typed, `async` [streams]. The new [async +streams] section of the guide contains further details, and we encourage all +interested parties to see the new real-time, multi-room [chat example]. + +As a taste of what's possible, the following `stream` route emits a `"ping"` +Server-Sent Event every `n` seconds, defaulting to `1`: + +```rust +# use rocket::*; +use rocket::response::stream::{Event, EventStream};; +use rocket::tokio::time::{interval, Duration}; + +#[get("/ping?")] +fn stream(n: Option) -> EventStream![] { + EventStream! { + let mut timer = interval(Duration::from_secs(n.unwrap_or(1))); + loop { + yield Event::data("ping"); + timer.tick().await; + } + } +} +``` + +[streams]: @api/rocket/response/stream/index.html +[async streams]: @guide/responses/#async-streams +[chat example]: @example/chat + +### ๐Ÿ”Œ WebSockets + +Rocket v0.5 introduces support for HTTP connection upgrades via a new [upgrade +API]. The API allows responders to take over an HTTP connection and perform raw +I/O with the client. In other words, an HTTP connection can be _upgraded_ to any +protocol, including HTTP WebSockets! + +The newly introduced [`rocket_ws`] library takes advantage of the new API to +implement first-class support for WebSockets entirely outside of Rocket's core. +The simplest use of the library, implementing an echo server and showcasing that +the incoming message stream is `async`, looks like this: + +```rust +# use rocket::get; +# use rocket_ws as ws; + +#[get("/echo")] +fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { + ws.stream(|io| io) +} +``` + +The simplified [async streams] generator syntax can also be used: + +```rust +# use rocket::get; +# use rocket_ws as ws; + +#[get("/echo")] +fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::Stream! { ws => + for await message in ws { + yield message?; + } + } +} +``` + +For complete usage details, see the [`rocket_ws`] documentation. + +[upgrade API]: @api/rocket/response/struct.Response.html#upgrading +[`rocket_ws`]: @api/rocket_ws + +[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[migration guide]: ../../guide/upgrading +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/CHANGELOG.md#version-050-rc2-may-9-2022 + +## Thank You + +
    +
  • Aaron Leopold
  • +
  • Abdullah Alyan
  • +
  • Aditya
  • +
  • Alex Macleod
  • +
  • Alex Sears
  • +
  • Alexander van Ratingen
  • +
  • ami-GS
  • +
  • Antoine Martin
  • +
  • arctic-alpaca
  • +
  • arlecchino
  • +
  • Arthur Woimbรฉe
  • +
  • atouchet
  • +
  • Aurora
  • +
  • badoken
  • +
  • Beep LIN
  • +
  • Ben Sully
  • +
  • Benedikt Weber
  • +
  • BlackDex
  • +
  • Bonex
  • +
  • Brenden Matthews
  • +
  • Brendon Federko
  • +
  • Brett Buford
  • +
  • Cedric Hutchings
  • +
  • Cezar Halmagean
  • +
  • Charles-Axel Dein
  • +
  • Compro Prasad
  • +
  • Daniel Wiesenberg
  • +
  • David Venhoek
  • +
  • Dimitri Sabadie
  • +
  • Dinu Blanovschi
  • +
  • Dominik Boehi
  • +
  • Doni Rubiagatra
  • +
  • Edgar Onghena
  • +
  • Edwin Svensson
  • +
  • est31
  • +
  • Felix Suominen
  • +
  • Filip Gospodinov
  • +
  • Flying-Toast
  • +
  • Follpvosten
  • +
  • Francois Stephany
  • +
  • Gabriel Fontes
  • +
  • gcarq
  • +
  • George Cheng
  • +
  • Giles Cope
  • +
  • Gonรงalo Ribeiro
  • +
  • hiyoko3m
  • +
  • Howard Su
  • +
  • hpodhaisky
  • +
  • Ian Jackson
  • +
  • IFcoltransG
  • +
  • Indosaram
  • +
  • inyourface34456
  • +
  • J. Cohen
  • +
  • Jacob Pratt
  • +
  • Jacob Sharf
  • +
  • Jacob Simpson
  • +
  • Jakub Dฤ…bek
  • +
  • Jakub Wieczorek
  • +
  • James Tai
  • +
  • Jason Hinch
  • +
  • Jeb Rosen
  • +
  • Jeremy Kaplan
  • +
  • Joakim Soderlund
  • +
  • Johannes Liebermann
  • +
  • John-John Tedro
  • +
  • Jonah Brรผchert
  • +
  • Jonas Mรธller
  • +
  • Jonathan Dickinson
  • +
  • Jonty
  • +
  • Joscha
  • +
  • Joshua Nitschke
  • +
  • JR Heard
  • +
  • Juhasz Sandor
  • +
  • Julian Bรผttner
  • +
  • Juraj Fiala
  • +
  • Kenneth Allen
  • +
  • Kevin Wang
  • +
  • Kian-Meng Ang
  • +
  • Konrad Borowski
  • +
  • Leonora Tindall
  • +
  • lewis
  • +
  • Lionel G
  • +
  • Lucille Blumire
  • +
  • Mai-Lapyst
  • +
  • Manuel
  • +
  • Marc Schreiber
  • +
  • Marc-Stefan Cassola
  • +
  • Marshall Bowers
  • +
  • Martin1887
  • +
  • Martinez
  • +
  • Matthew Pomes
  • +
  • Maxime Guerreiro
  • +
  • meltinglava
  • +
  • Michael Howell
  • +
  • Mikail Bagishov
  • +
  • mixio
  • +
  • multisn8
  • +
  • Necmettin Karakaya
  • +
  • Ning Sun
  • +
  • Nya
  • +
  • Paolo Barbolini
  • +
  • Paul Smith
  • +
  • Paul van Tilburg
  • +
  • Paul Weaver
  • +
  • pennae
  • +
  • Petr Portnov
  • +
  • philipp
  • +
  • Pieter Frenssen
  • +
  • PROgrm_JARvis
  • +
  • Razican
  • +
  • Redrield
  • +
  • Rรฉmi Lauzier
  • +
  • Riley Patterson
  • +
  • Rodolphe Brรฉard
  • +
  • Roger Mo
  • +
  • RotesWasser
  • +
  • rotoclone
  • +
  • Rudi Floren
  • +
  • Samuele Esposito
  • +
  • Scott McMurray
  • +
  • Sergio Benitez
  • +
  • Silas Sewell
  • +
  • Soham Roy
  • +
  • Stuart Hinson
  • +
  • Thibaud Martinez
  • +
  • Thomas Eckert
  • +
  • ThouCheese
  • +
  • Tilen Pintariฤ
  • +
  • timando
  • +
  • timokoesters
  • +
  • toshokan
  • +
  • TotalKrill
  • +
  • Vasili
  • +
  • Vladimir Ignatev
  • +
  • Wesley Norris
  • +
  • xelivous
  • +
  • YetAnotherMinion
  • +
  • Yohannes Kifle
  • +
  • Yusuke Kominami
  • +
+ +## What's Next? diff --git a/site/news/index.toml b/site/news/index.toml index 75f140ab70..3796240308 100644 --- a/site/news/index.toml +++ b/site/news/index.toml @@ -1,3 +1,19 @@ +[[articles]] +title = """ +Rocket v0.5: Stable, Async, Featureful +""" +slug = "2023-05-26-version-0.5" +author = "Sergio Benitez" +author_url = "https://sergio.bz" +date = "May 26, 2023" +snippet = """ +I am _elated_ to announce that Rocket v0.5 is generally available. A step +forward in every direction, it is **packed** with features and improvements that +increase developer productivity, improve application security and robustness, +provide new opportunities for extensibility, and deliver a renewed degree of +toolchain stability. +""" + [[articles]] title = """ Rocket's 4th v0.5 Release Candidate From 6d467564d6ef5d3534adc1b107907470307b1c74 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 3 Nov 2023 18:26:07 -0500 Subject: [PATCH 031/178] Update version numbers for 0.5.0. --- CHANGELOG.md | 342 +++++++++++------------ contrib/db_pools/README.md | 14 +- contrib/db_pools/codegen/Cargo.toml | 2 +- contrib/db_pools/lib/Cargo.toml | 6 +- contrib/db_pools/lib/src/diesel.rs | 4 +- contrib/db_pools/lib/src/lib.rs | 4 +- contrib/dyn_templates/Cargo.toml | 6 +- contrib/dyn_templates/README.md | 4 +- contrib/dyn_templates/src/lib.rs | 6 +- contrib/sync_db_pools/README.md | 4 +- contrib/sync_db_pools/codegen/Cargo.toml | 2 +- contrib/sync_db_pools/lib/Cargo.toml | 8 +- contrib/sync_db_pools/lib/src/lib.rs | 6 +- contrib/ws/Cargo.toml | 6 +- contrib/ws/README.md | 4 +- contrib/ws/src/lib.rs | 4 +- core/codegen/Cargo.toml | 6 +- core/codegen/src/lib.rs | 6 +- core/http/Cargo.toml | 4 +- core/lib/Cargo.toml | 8 +- core/lib/src/config/config.rs | 2 +- core/lib/src/config/mod.rs | 2 +- core/lib/src/config/secret_key.rs | 4 +- core/lib/src/fairing/mod.rs | 2 +- core/lib/src/form/context.rs | 2 +- core/lib/src/form/form.rs | 2 +- core/lib/src/form/from_form.rs | 2 +- core/lib/src/form/mod.rs | 2 +- core/lib/src/lib.rs | 26 +- core/lib/src/local/mod.rs | 2 +- core/lib/src/mtls.rs | 2 +- core/lib/src/request/from_request.rs | 2 +- core/lib/src/serde/json.rs | 2 +- core/lib/src/serde/msgpack.rs | 2 +- core/lib/src/serde/uuid.rs | 2 +- core/lib/src/shield/mod.rs | 2 +- scripts/config.sh | 4 +- site/README.md | 2 +- site/guide/01-upgrading.md | 6 +- site/guide/1-quickstart.md | 2 +- site/guide/10-pastebin-tutorial.md | 2 +- site/guide/12-faq.md | 4 +- site/guide/2-getting-started.md | 2 +- site/guide/4-requests.md | 4 +- site/guide/6-state.md | 4 +- site/guide/9-configuration.md | 4 +- site/index.toml | 4 +- site/news/2021-06-09-version-0.5-rc.1.md | 2 +- site/news/2022-05-09-version-0.5-rc.2.md | 2 +- site/news/2023-03-23-version-0.5-rc.3.md | 2 +- site/news/2023-05-26-version-0.5.md | 2 +- site/tests/Cargo.toml | 4 +- 52 files changed, 277 insertions(+), 277 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daba64de24..b49b57075e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This release introduces the following major features and improvements: * A rewritten, fully asynchronous core with support for [`async`/`await`]. * WebSocket support via [`rocket_ws`]. * [Feature-complete forms support] including multipart, collections, [ad-hoc validation], and - [context](https://rocket.rs/v0.5-rc/guide/requests/#context). + [context](https://rocket.rs/v0.5/guide/requests/#context). * [Sentinels]: automatic verification of application state at start-up to prevent runtime errors. * [Graceful shutdown] with configurable signaling, grace periods, [notification], and [shutdown fairings]. @@ -28,8 +28,8 @@ This release introduces the following major features and improvements: * [Ignorable segments]: wildcard route matching with no typing restrictions. * First-class [support for `serde`] for built-in guards and types. * New application launch attributes: - [`#[launch]`](https://api.rocket.rs/v0.5-rc/rocket/attr.launch.html) and - [`#[rocket::main]`](https://api.rocket.rs/v0.5-rc/rocket/attr.main.html). + [`#[launch]`](https://api.rocket.rs/v0.5/rocket/attr.launch.html) and + [`#[rocket::main]`](https://api.rocket.rs/v0.5/rocket/attr.main.html). * [Default catchers] via `#[catch(default)]`, which handle _any_ status code. * [Catcher scoping] to narrow the scope of a catcher to a URI prefix. * Built-in libraries and support for [asynchronous testing]. @@ -48,7 +48,7 @@ This release introduces the following major features and improvements: * Borrowed byte slices as data and form guards. * Fail-fast behavior for [misconfigured secrets], file serving paths. * Support for generics and custom generic bounds in - [`#[derive(Responder)]`](https://api.rocket.rs/v0.5-rc/rocket/derive.Responder.html). + [`#[derive(Responder)]`](https://api.rocket.rs/v0.5/rocket/derive.Responder.html). * [Default ranking colors], which prevent more routing collisions automatically. * Improved error logging with suggestions when common errors are detected. * Completely rewritten examples including a new real-time [`chat`] application. @@ -172,7 +172,7 @@ The `rocket_contrib` crate is deprecated and the functionality moved to other `r - These crates are versioned and released independently of `rocket`. - `rocket_contrib::databases::DbError` is now `rocket_sync_db_pools::Error`. - Removed `redis`, `mongodb`, and `mysql` integrations which have upstream `async` drivers. - - The [`#[database]`](https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/attr.database.html) + - The [`#[database]`](https://api.rocket.rs/v0.5/rocket_sync_db_pools/attr.database.html) attribute generates an [`async run()`] method instead of `Deref` implementations. ### General @@ -441,7 +441,7 @@ In addition to new features and changes, Rocket saw the following improvements: * Added support for the `X-DNS-Prefetch-Control` header to `Shield`. * Added support for manually-set `expires` values for private cookies. * Added support for type generics and custom generic bounds to - [`#[derive(Responder)]`](https://api.rocket.rs/v0.5-rc/rocket/derive.Responder.html). + [`#[derive(Responder)]`](https://api.rocket.rs/v0.5/rocket/derive.Responder.html). * The `Server` header is only set if one isn't already set. * Accurate `Content-Length` headers are sent even for partially read `Body`s. * [`Redirect`] now accepts a `TryFrom`, allowing fragment parts. @@ -548,178 +548,178 @@ The following changes were made to the project's infrastructure: * The CI now frees disk space before proceeding to avoid out-of-disk errors. * All workspaces now use `resolver = 2`. -[phase]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#phases -[`async`/`await`]: https://rocket.rs/v0.5-rc/guide/overview/#async-routes -[compilation on Rust's stable]: https://rocket.rs/v0.5-rc/guide/getting-started/#installing-rust -[Feature-complete forms support]: https://rocket.rs/v0.5-rc/guide/requests/#forms -[configuration system]: https://rocket.rs/v0.5-rc/guide/configuration/#configuration -[graceful shutdown]: https://api.rocket.rs/v0.5-rc/rocket/config/struct.Shutdown.html#summary -[asynchronous testing]: https://rocket.rs/v0.5-rc/guide/testing/#asynchronous-testing -[UTF-8 characters]: https://rocket.rs/v0.5-rc/guide/requests/#static-parameters -[ignorable segments]: https://rocket.rs/v0.5-rc/guide/requests/#ignored-segments -[Catcher scoping]: https://rocket.rs/v0.5-rc/guide/requests/#scoping -[ad-hoc validation]: https://rocket.rs/v0.5-rc/guide/requests#ad-hoc-validation -[incoming data limits]: https://rocket.rs/v0.5-rc/guide/requests/#streaming -[build phases]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#phases -[Singleton fairings]: https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#singletons -[features into core]: https://api.rocket.rs/v0.5-rc/rocket/index.html#features -[features of `rocket`]: https://api.rocket.rs/v0.5-rc/rocket/index.html#features -[Data limit declaration in SI units]: https://api.rocket.rs/v0.5-rc/rocket/data/struct.ByteUnit.html -[support for `serde`]: https://api.rocket.rs/v0.5-rc/rocket/serde/index.html -[automatic typed config extraction]: https://api.rocket.rs/v0.5-rc/rocket/fairing/struct.AdHoc.html#method.config -[misconfigured secrets]: https://api.rocket.rs/v0.5-rc/rocket/config/struct.SecretKey.html -[default ranking colors]: https://rocket.rs/v0.5-rc/guide/requests/#default-ranking -[`chat`]: https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/chat -[`Form` guard]: https://api.rocket.rs/v0.5-rc/rocket/form/struct.Form.html -[`Strict`]: https://api.rocket.rs/v0.5-rc/rocket/form/struct.Strict.html -[`CookieJar`#pending]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html#pending -[`rocket::serde::json`]: https://api.rocket.rs/v0.5-rc/rocket/serde/json/index.html -[`rocket::serde::msgpack`]: https://api.rocket.rs/v0.5-rc/rocket/serde/msgpack/index.html -[`rocket::serde::uuid`]: https://api.rocket.rs/v0.5-rc/rocket/serde/uuid/index.html -[`rocket::shield`]: https://api.rocket.rs/v0.5-rc/rocket/shield/index.html -[`rocket::fs`]: https://api.rocket.rs/v0.5-rc/rocket/fs/index.html -[`async run()`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/index.html#handlers -[`LocalRequest::json()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.json -[`LocalRequest::msgpack()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.msgpack -[`LocalResponse::json()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalResponse.html#method.json -[`LocalResponse::msgpack()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalResponse.html#method.msgpack -[hierarchical data limits]: https://api.rocket.rs/v0.5-rc/rocket/data/struct.Limits.html#hierarchy -[default form field values]: https://rocket.rs/v0.5-rc/guide/requests/#defaults +[phase]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#phases +[`async`/`await`]: https://rocket.rs/v0.5/guide/overview/#async-routes +[compilation on Rust's stable]: https://rocket.rs/v0.5/guide/getting-started/#installing-rust +[Feature-complete forms support]: https://rocket.rs/v0.5/guide/requests/#forms +[configuration system]: https://rocket.rs/v0.5/guide/configuration/#configuration +[graceful shutdown]: https://api.rocket.rs/v0.5/rocket/config/struct.Shutdown.html#summary +[asynchronous testing]: https://rocket.rs/v0.5/guide/testing/#asynchronous-testing +[UTF-8 characters]: https://rocket.rs/v0.5/guide/requests/#static-parameters +[ignorable segments]: https://rocket.rs/v0.5/guide/requests/#ignored-segments +[Catcher scoping]: https://rocket.rs/v0.5/guide/requests/#scoping +[ad-hoc validation]: https://rocket.rs/v0.5/guide/requests#ad-hoc-validation +[incoming data limits]: https://rocket.rs/v0.5/guide/requests/#streaming +[build phases]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#phases +[Singleton fairings]: https://api.rocket.rs/v0.5/rocket/fairing/trait.Fairing.html#singletons +[features into core]: https://api.rocket.rs/v0.5/rocket/index.html#features +[features of `rocket`]: https://api.rocket.rs/v0.5/rocket/index.html#features +[Data limit declaration in SI units]: https://api.rocket.rs/v0.5/rocket/data/struct.ByteUnit.html +[support for `serde`]: https://api.rocket.rs/v0.5/rocket/serde/index.html +[automatic typed config extraction]: https://api.rocket.rs/v0.5/rocket/fairing/struct.AdHoc.html#method.config +[misconfigured secrets]: https://api.rocket.rs/v0.5/rocket/config/struct.SecretKey.html +[default ranking colors]: https://rocket.rs/v0.5/guide/requests/#default-ranking +[`chat`]: https://github.com/SergioBenitez/Rocket/tree/v0.5/examples/chat +[`Form` guard]: https://api.rocket.rs/v0.5/rocket/form/struct.Form.html +[`Strict`]: https://api.rocket.rs/v0.5/rocket/form/struct.Strict.html +[`CookieJar`#pending]: https://api.rocket.rs/v0.5/rocket/http/struct.CookieJar.html#pending +[`rocket::serde::json`]: https://api.rocket.rs/v0.5/rocket/serde/json/index.html +[`rocket::serde::msgpack`]: https://api.rocket.rs/v0.5/rocket/serde/msgpack/index.html +[`rocket::serde::uuid`]: https://api.rocket.rs/v0.5/rocket/serde/uuid/index.html +[`rocket::shield`]: https://api.rocket.rs/v0.5/rocket/shield/index.html +[`rocket::fs`]: https://api.rocket.rs/v0.5/rocket/fs/index.html +[`async run()`]: https://api.rocket.rs/v0.5/rocket_sync_db_pools/index.html#handlers +[`LocalRequest::json()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalRequest.html#method.json +[`LocalRequest::msgpack()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalRequest.html#method.msgpack +[`LocalResponse::json()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalResponse.html#method.json +[`LocalResponse::msgpack()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalResponse.html#method.msgpack +[hierarchical data limits]: https://api.rocket.rs/v0.5/rocket/data/struct.Limits.html#hierarchy +[default form field values]: https://rocket.rs/v0.5/guide/requests/#defaults [`Config::ident`]: https://api.rocket.rs/rocket/struct.Config.html#structfield.ident [`tokio`]: https://tokio.rs/ [Figment]: https://docs.rs/figment/0.10/figment/ -[`TempFile`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html -[`Contextual`]: https://rocket.rs/v0.5-rc/guide/requests/#context -[`Capped`]: https://api.rocket.rs/v0.5-rc/rocket/data/struct.Capped.html -[default catchers]: https://rocket.rs/v0.5-rc/guide/requests/#default-catchers -[URI types]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/index.html -[`uri!`]: https://api.rocket.rs/v0.5-rc/rocket/macro.uri.html -[`Reference`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Reference.html -[`Asterisk`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Asterisk.html -[`Redirect`]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Redirect.html -[`UriDisplayQuery`]: https://api.rocket.rs/v0.5-rc/rocket/derive.UriDisplayQuery.html -[`Shield`]: https://api.rocket.rs/v0.5-rc/rocket/shield/struct.Shield.html -[Sentinels]: https://api.rocket.rs/v0.5-rc/rocket/trait.Sentinel.html -[`local_cache!`]: https://api.rocket.rs/v0.5-rc/rocket/request/macro.local_cache.html -[`local_cache_once!`]: https://api.rocket.rs/v0.5-rc/rocket/request/macro.local_cache_once.html -[`CookieJar`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html -[asynchronous streams]: https://rocket.rs/v0.5-rc/guide/responses/#async-streams -[Server-Sent Events]: https://api.rocket.rs/v0.5-rc/rocket/response/stream/struct.EventStream.html -[`fs::relative!`]: https://api.rocket.rs/v0.5-rc/rocket/fs/macro.relative.html -[notification]: https://api.rocket.rs/v0.5-rc/rocket/struct.Shutdown.html -[`Rocket`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html -[`rocket::build()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.build -[`Rocket::ignite()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.ignite -[`Rocket::launch()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.launch -[`Request::rocket()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.rocket -[Fairings]: https://rocket.rs/v0.5-rc/guide/fairings/ -[configuration system]: https://rocket.rs/v0.5-rc/guide/configuration/ -[`Poolable`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/trait.Poolable.html -[`Config`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html -[`Error`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html -[`LogLevel`]: https://api.rocket.rs/v0.5-rc/rocket/config/enum.LogLevel.html -[`Rocket::register()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.register -[`NamedFile::open`]: https://api.rocket.rs/v0.5-rc/rocket/fs/struct.NamedFile.html#method.open -[`Request::local_cache_async()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.local_cache_async -[`FromRequest`]: https://api.rocket.rs/v0.5-rc/rocket/request/trait.FromRequest.html -[`Fairing`]: https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html -[`catcher::Handler`]: https://api.rocket.rs/v0.5-rc/rocket/catcher/trait.Handler.html -[`route::Handler`]: https://api.rocket.rs/v0.5-rc/rocket/route/trait.Handler.html -[`FromData`]: https://api.rocket.rs/v0.5-rc/rocket/data/trait.FromData.html +[`TempFile`]: https://api.rocket.rs/v0.5/rocket/fs/enum.TempFile.html +[`Contextual`]: https://rocket.rs/v0.5/guide/requests/#context +[`Capped`]: https://api.rocket.rs/v0.5/rocket/data/struct.Capped.html +[default catchers]: https://rocket.rs/v0.5/guide/requests/#default-catchers +[URI types]: https://api.rocket.rs/v0.5/rocket/http/uri/index.html +[`uri!`]: https://api.rocket.rs/v0.5/rocket/macro.uri.html +[`Reference`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Reference.html +[`Asterisk`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Asterisk.html +[`Redirect`]: https://api.rocket.rs/v0.5/rocket/response/struct.Redirect.html +[`UriDisplayQuery`]: https://api.rocket.rs/v0.5/rocket/derive.UriDisplayQuery.html +[`Shield`]: https://api.rocket.rs/v0.5/rocket/shield/struct.Shield.html +[Sentinels]: https://api.rocket.rs/v0.5/rocket/trait.Sentinel.html +[`local_cache!`]: https://api.rocket.rs/v0.5/rocket/request/macro.local_cache.html +[`local_cache_once!`]: https://api.rocket.rs/v0.5/rocket/request/macro.local_cache_once.html +[`CookieJar`]: https://api.rocket.rs/v0.5/rocket/http/struct.CookieJar.html +[asynchronous streams]: https://rocket.rs/v0.5/guide/responses/#async-streams +[Server-Sent Events]: https://api.rocket.rs/v0.5/rocket/response/stream/struct.EventStream.html +[`fs::relative!`]: https://api.rocket.rs/v0.5/rocket/fs/macro.relative.html +[notification]: https://api.rocket.rs/v0.5/rocket/struct.Shutdown.html +[`Rocket`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html +[`rocket::build()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.build +[`Rocket::ignite()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.ignite +[`Rocket::launch()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.launch +[`Request::rocket()`]: https://api.rocket.rs/v0.5/rocket/request/struct.Request.html#method.rocket +[Fairings]: https://rocket.rs/v0.5/guide/fairings/ +[configuration system]: https://rocket.rs/v0.5/guide/configuration/ +[`Poolable`]: https://api.rocket.rs/v0.5/rocket_sync_db_pools/trait.Poolable.html +[`Config`]: https://api.rocket.rs/v0.5/rocket/struct.Config.html +[`Error`]: https://api.rocket.rs/v0.5/rocket/struct.Error.html +[`LogLevel`]: https://api.rocket.rs/v0.5/rocket/config/enum.LogLevel.html +[`Rocket::register()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.register +[`NamedFile::open`]: https://api.rocket.rs/v0.5/rocket/fs/struct.NamedFile.html#method.open +[`Request::local_cache_async()`]: https://api.rocket.rs/v0.5/rocket/request/struct.Request.html#method.local_cache_async +[`FromRequest`]: https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest.html +[`Fairing`]: https://api.rocket.rs/v0.5/rocket/fairing/trait.Fairing.html +[`catcher::Handler`]: https://api.rocket.rs/v0.5/rocket/catcher/trait.Handler.html +[`route::Handler`]: https://api.rocket.rs/v0.5/rocket/route/trait.Handler.html +[`FromData`]: https://api.rocket.rs/v0.5/rocket/data/trait.FromData.html [`AsyncRead`]: https://docs.rs/tokio/1/tokio/io/trait.AsyncRead.html [`AsyncSeek`]: https://docs.rs/tokio/1/tokio/io/trait.AsyncSeek.html -[`rocket::local::asynchronous`]: https://api.rocket.rs/v0.5-rc/rocket/local/asynchronous/index.html -[`rocket::local::blocking`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/index.html -[`Outcome`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/enum.Outcome.html -[`rocket::outcome::try_outcome!`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/macro.try_outcome.html -[`Result` implements `Responder`]: https://api.rocket.rs/v0.5-rc/rocket/response/trait.Responder.html#provided-implementations -[`Debug`]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Debug.html +[`rocket::local::asynchronous`]: https://api.rocket.rs/v0.5/rocket/local/asynchronous/index.html +[`rocket::local::blocking`]: https://api.rocket.rs/v0.5/rocket/local/blocking/index.html +[`Outcome`]: https://api.rocket.rs/v0.5/rocket/outcome/enum.Outcome.html +[`rocket::outcome::try_outcome!`]: https://api.rocket.rs/v0.5/rocket/outcome/macro.try_outcome.html +[`Result` implements `Responder`]: https://api.rocket.rs/v0.5/rocket/response/trait.Responder.html#provided-implementations +[`Debug`]: https://api.rocket.rs/v0.5/rocket/response/struct.Debug.html [`std::convert::Infallible`]: https://doc.rust-lang.org/stable/std/convert/enum.Infallible.html -[`ErrorKind::Collisions`]: https://api.rocket.rs/v0.5-rc/rocket/error/enum.ErrorKind.html#variant.Collisions -[`rocket::http::uri::fmt`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/fmt/index.html -[`Data::open()`]: https://api.rocket.rs/v0.5-rc/rocket/data/struct.Data.html#method.open -[`DataStream`]: https://api.rocket.rs/v0.5-rc/rocket/data/struct.DataStream.html -[`rocket::form`]: https://api.rocket.rs/v0.5-rc/rocket/form/index.html -[`FromFormField`]: https://api.rocket.rs/v0.5-rc/rocket/form/trait.FromFormField.html -[`FromForm`]: https://api.rocket.rs/v0.5-rc/rocket/form/trait.FromForm.html -[`FlashMessage`]: https://api.rocket.rs/v0.5-rc/rocket/request/type.FlashMessage.html -[`Flash`]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Flash.html -[`rocket::State`]: https://api.rocket.rs/v0.5-rc/rocket/struct.State.html -[`Segments` and `Segments`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Segments.html -[`Route::map_base()`]: https://api.rocket.rs/v0.5-rc/rocket/route/struct.Route.html#method.map_base -[`uuid` support]: https://api.rocket.rs/v0.5-rc/rocket/serde/uuid/index.html -[`json`]: https://api.rocket.rs/v0.5-rc/rocket/serde/json/index.html -[`msgpack`]: https://api.rocket.rs/v0.5-rc/rocket/serde/msgpack/index.html -[`rocket::serde::json::json!`]: https://api.rocket.rs/v0.5-rc/rocket/serde/json/macro.json.html -[`rocket::shield::Shield`]: https://api.rocket.rs/v0.5-rc/rocket/shield/struct.Shield.html -[`rocket::fs::FileServer`]: https://api.rocket.rs/v0.5-rc/rocket/fs/struct.FileServer.html -[`rocket_dyn_templates`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/index.html -[`rocket_sync_db_pools`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/index.html -[multitasking]: https://rocket.rs/v0.5-rc/guide/overview/#multitasking -[`Created`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/struct.Created.html -[`Created::tagged_body`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/struct.Created.html#method.tagged_body +[`ErrorKind::Collisions`]: https://api.rocket.rs/v0.5/rocket/error/enum.ErrorKind.html#variant.Collisions +[`rocket::http::uri::fmt`]: https://api.rocket.rs/v0.5/rocket/http/uri/fmt/index.html +[`Data::open()`]: https://api.rocket.rs/v0.5/rocket/data/struct.Data.html#method.open +[`DataStream`]: https://api.rocket.rs/v0.5/rocket/data/struct.DataStream.html +[`rocket::form`]: https://api.rocket.rs/v0.5/rocket/form/index.html +[`FromFormField`]: https://api.rocket.rs/v0.5/rocket/form/trait.FromFormField.html +[`FromForm`]: https://api.rocket.rs/v0.5/rocket/form/trait.FromForm.html +[`FlashMessage`]: https://api.rocket.rs/v0.5/rocket/request/type.FlashMessage.html +[`Flash`]: https://api.rocket.rs/v0.5/rocket/response/struct.Flash.html +[`rocket::State`]: https://api.rocket.rs/v0.5/rocket/struct.State.html +[`Segments` and `Segments`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Segments.html +[`Route::map_base()`]: https://api.rocket.rs/v0.5/rocket/route/struct.Route.html#method.map_base +[`uuid` support]: https://api.rocket.rs/v0.5/rocket/serde/uuid/index.html +[`json`]: https://api.rocket.rs/v0.5/rocket/serde/json/index.html +[`msgpack`]: https://api.rocket.rs/v0.5/rocket/serde/msgpack/index.html +[`rocket::serde::json::json!`]: https://api.rocket.rs/v0.5/rocket/serde/json/macro.json.html +[`rocket::shield::Shield`]: https://api.rocket.rs/v0.5/rocket/shield/struct.Shield.html +[`rocket::fs::FileServer`]: https://api.rocket.rs/v0.5/rocket/fs/struct.FileServer.html +[`rocket_dyn_templates`]: https://api.rocket.rs/v0.5/rocket_dyn_templates/index.html +[`rocket_sync_db_pools`]: https://api.rocket.rs/v0.5/rocket_sync_db_pools/index.html +[multitasking]: https://rocket.rs/v0.5/guide/overview/#multitasking +[`Created`]: https://api.rocket.rs/v0.5/rocket/response/status/struct.Created.html +[`Created::tagged_body`]: https://api.rocket.rs/v0.5/rocket/response/status/struct.Created.html#method.tagged_body [raw identifiers]: https://doc.rust-lang.org/1.51.0/book/appendix-01-keywords.html#raw-identifiers -[`Rocket::config()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.config -[`Rocket::figment()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.figment -[`Rocket::state()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.state -[`Rocket::catchers()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.catchers -[`LocalRequest::inner_mut()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.inner_mut -[`RawStrBuf`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.RawStrBuf.html -[`RawStr`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.RawStr.html -[`RawStr::percent_encode()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.RawStr.html#method.percent_encode -[`RawStr::percent_encode_bytes()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.RawStr.html#method.percent_encode_bytes -[`RawStr::strip()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.RawStr.html#method.strip_prefix -[`rocket::catcher`]: https://api.rocket.rs/v0.5-rc/rocket/catcher/index.html -[`rocket::route`]: https://api.rocket.rs/v0.5-rc/rocket/route/index.html -[`Segments::prefix_of()`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Segments.html#method.prefix_of -[`Template::try_custom()`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Template.html#method.try_custom -[`Template::custom`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Template.html#method.custom -[`FileServer::new()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/struct.FileServer.html#method.new -[`content`]: https://api.rocket.rs/v0.5-rc/rocket/response/content/index.html -[`rocket_db_pools`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html -[mutual TLS]: https://rocket.rs/v0.5-rc/guide/configuration/#mutual-tls -[`Certificate`]: https://api.rocket.rs/v0.5-rc/rocket/mtls/struct.Certificate.html -[`MediaType::with_params()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html#method.with_params -[`ContentType::with_params()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.ContentType.html#method.with_params -[`Host`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Host.html -[`&Host`]: https://api.rocket.rs/v0.5-rc/rocket/http/uri/struct.Host.html -[`Request::host()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.host -[`context!`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/macro.context.html -[`MediaType`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html -[`ContentType`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.ContentType.html -[`Method`]: https://api.rocket.rs/v0.5-rc/rocket/http/enum.Method.html -[`(ContentType, T)`]: https://api.rocket.rs/v0.5-rc/rocket/response/content/index.html#usage -[v0.4 to v0.5 migration guide]: https://rocket.rs/v0.5-rc/guide/upgrading/ -[contrib deprecation upgrade guide]: https://rocket.rs/v0.5-rc/guide/upgrading/#contrib-deprecation -[FAQ]: https://rocket.rs/v0.5-rc/guide/faq/ -[`Rocket::launch()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Rocket.html#method.launch -[`ErrorKind::Shutdown`]: https://api.rocket.rs/v0.5-rc/rocket/error/enum.ErrorKind.html#variant.Shutdown -[shutdown fairings]: https://api.rocket.rs/v0.5-rc/rocket/fairing/trait.Fairing.html#shutdown -[`Client::terminate()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.Client.html#method.terminate -[`rocket::execute()`]: https://api.rocket.rs/v0.5-rc/rocket/fn.execute.html -[`CookieJar::get_pending()`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.CookieJar.html#method.get_pending -[`Metadata::render()`]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/struct.Metadata.html#method.render -[`pool()`]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools/example/struct.ExampleDb.html#method.pool -[`Request::client_ip()`]: https://api.rocket.rs/v0.5-rc/rocket/request/struct.Request.html#method.client_ip -[`max_blocking`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.max_blocking -[`ip_header`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Config.html#structfield.ip_header -[`LocalRequest::identity()`]: https://api.rocket.rs/v0.5-rc/rocket/local/blocking/struct.LocalRequest.html#method.identity -[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open -[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print -[Request connection upgrade APIs]: https://api.rocket.rs/v0.5-rc/rocket/struct.Response.html#upgrading -[`rocket_ws`]: https://api.rocket.rs/v0.5-rc/rocket_ws/ -[registering]: https://api.rocket.rs/v0.5-rc/rocket/response/struct.Response.html#method.add_upgrade -[`IoHandler`]: https://api.rocket.rs/v0.5-rc/rocket/data/trait.IoHandler.html -[`response::status`]: https://api.rocket.rs/v0.5-rc/rocket/response/status/index.html -[Custom form errors]: https://api.rocket.rs/v0.5-rc/rocket/form/error/enum.ErrorKind.html#variant.Custom -[`request::Outcome`]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward -[Route `Forward` outcomes]: https://api.rocket.rs/v0.5-rc/rocket/request/type.Outcome.html#variant.Forward -[`Outcome::Error`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/enum.Outcome.html#variant.Error -[`IntoOutcome`]: https://api.rocket.rs/v0.5-rc/rocket/outcome/trait.IntoOutcome.html -[`MediaType::JavaScript`]: https://api.rocket.rs/v0.5-rc/rocket/http/struct.MediaType.html#associatedconstant.JavaScript -[`TempFile::open()`]: https://api.rocket.rs/v0.5-rc/rocket/fs/enum.TempFile.html#method.open -[`Error::pretty_print()`]: https://api.rocket.rs/v0.5-rc/rocket/struct.Error.html#method.pretty_print -[`RouteUri`]: https://api.rocket.rs/v0.5-rc/rocket/route/struct.RouteUri.html +[`Rocket::config()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.config +[`Rocket::figment()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.figment +[`Rocket::state()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.state +[`Rocket::catchers()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.catchers +[`LocalRequest::inner_mut()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalRequest.html#method.inner_mut +[`RawStrBuf`]: https://api.rocket.rs/v0.5/rocket/http/struct.RawStrBuf.html +[`RawStr`]: https://api.rocket.rs/v0.5/rocket/http/struct.RawStr.html +[`RawStr::percent_encode()`]: https://api.rocket.rs/v0.5/rocket/http/struct.RawStr.html#method.percent_encode +[`RawStr::percent_encode_bytes()`]: https://api.rocket.rs/v0.5/rocket/http/struct.RawStr.html#method.percent_encode_bytes +[`RawStr::strip()`]: https://api.rocket.rs/v0.5/rocket/http/struct.RawStr.html#method.strip_prefix +[`rocket::catcher`]: https://api.rocket.rs/v0.5/rocket/catcher/index.html +[`rocket::route`]: https://api.rocket.rs/v0.5/rocket/route/index.html +[`Segments::prefix_of()`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Segments.html#method.prefix_of +[`Template::try_custom()`]: https://api.rocket.rs/v0.5/rocket_dyn_templates/struct.Template.html#method.try_custom +[`Template::custom`]: https://api.rocket.rs/v0.5/rocket_dyn_templates/struct.Template.html#method.custom +[`FileServer::new()`]: https://api.rocket.rs/v0.5/rocket/fs/struct.FileServer.html#method.new +[`content`]: https://api.rocket.rs/v0.5/rocket/response/content/index.html +[`rocket_db_pools`]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html +[mutual TLS]: https://rocket.rs/v0.5/guide/configuration/#mutual-tls +[`Certificate`]: https://api.rocket.rs/v0.5/rocket/mtls/struct.Certificate.html +[`MediaType::with_params()`]: https://api.rocket.rs/v0.5/rocket/http/struct.MediaType.html#method.with_params +[`ContentType::with_params()`]: https://api.rocket.rs/v0.5/rocket/http/struct.ContentType.html#method.with_params +[`Host`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Host.html +[`&Host`]: https://api.rocket.rs/v0.5/rocket/http/uri/struct.Host.html +[`Request::host()`]: https://api.rocket.rs/v0.5/rocket/request/struct.Request.html#method.host +[`context!`]: https://api.rocket.rs/v0.5/rocket_dyn_templates/macro.context.html +[`MediaType`]: https://api.rocket.rs/v0.5/rocket/http/struct.MediaType.html +[`ContentType`]: https://api.rocket.rs/v0.5/rocket/http/struct.ContentType.html +[`Method`]: https://api.rocket.rs/v0.5/rocket/http/enum.Method.html +[`(ContentType, T)`]: https://api.rocket.rs/v0.5/rocket/response/content/index.html#usage +[v0.4 to v0.5 migration guide]: https://rocket.rs/v0.5/guide/upgrading/ +[contrib deprecation upgrade guide]: https://rocket.rs/v0.5/guide/upgrading/#contrib-deprecation +[FAQ]: https://rocket.rs/v0.5/guide/faq/ +[`Rocket::launch()`]: https://api.rocket.rs/v0.5/rocket/struct.Rocket.html#method.launch +[`ErrorKind::Shutdown`]: https://api.rocket.rs/v0.5/rocket/error/enum.ErrorKind.html#variant.Shutdown +[shutdown fairings]: https://api.rocket.rs/v0.5/rocket/fairing/trait.Fairing.html#shutdown +[`Client::terminate()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.Client.html#method.terminate +[`rocket::execute()`]: https://api.rocket.rs/v0.5/rocket/fn.execute.html +[`CookieJar::get_pending()`]: https://api.rocket.rs/v0.5/rocket/http/struct.CookieJar.html#method.get_pending +[`Metadata::render()`]: https://api.rocket.rs/v0.5/rocket_dyn_templates/struct.Metadata.html#method.render +[`pool()`]: https://api.rocket.rs/v0.5/rocket_sync_db_pools/example/struct.ExampleDb.html#method.pool +[`Request::client_ip()`]: https://api.rocket.rs/v0.5/rocket/request/struct.Request.html#method.client_ip +[`max_blocking`]: https://api.rocket.rs/v0.5/rocket/struct.Config.html#structfield.max_blocking +[`ip_header`]: https://api.rocket.rs/v0.5/rocket/struct.Config.html#structfield.ip_header +[`LocalRequest::identity()`]: https://api.rocket.rs/v0.5/rocket/local/blocking/struct.LocalRequest.html#method.identity +[`TempFile::open()`]: https://api.rocket.rs/v0.5/rocket/fs/enum.TempFile.html#method.open +[`Error::pretty_print()`]: https://api.rocket.rs/v0.5/rocket/struct.Error.html#method.pretty_print +[Request connection upgrade APIs]: https://api.rocket.rs/v0.5/rocket/struct.Response.html#upgrading +[`rocket_ws`]: https://api.rocket.rs/v0.5/rocket_ws/ +[registering]: https://api.rocket.rs/v0.5/rocket/response/struct.Response.html#method.add_upgrade +[`IoHandler`]: https://api.rocket.rs/v0.5/rocket/data/trait.IoHandler.html +[`response::status`]: https://api.rocket.rs/v0.5/rocket/response/status/index.html +[Custom form errors]: https://api.rocket.rs/v0.5/rocket/form/error/enum.ErrorKind.html#variant.Custom +[`request::Outcome`]: https://api.rocket.rs/v0.5/rocket/request/type.Outcome.html#variant.Forward +[Route `Forward` outcomes]: https://api.rocket.rs/v0.5/rocket/request/type.Outcome.html#variant.Forward +[`Outcome::Error`]: https://api.rocket.rs/v0.5/rocket/outcome/enum.Outcome.html#variant.Error +[`IntoOutcome`]: https://api.rocket.rs/v0.5/rocket/outcome/trait.IntoOutcome.html +[`MediaType::JavaScript`]: https://api.rocket.rs/v0.5/rocket/http/struct.MediaType.html#associatedconstant.JavaScript +[`TempFile::open()`]: https://api.rocket.rs/v0.5/rocket/fs/enum.TempFile.html#method.open +[`Error::pretty_print()`]: https://api.rocket.rs/v0.5/rocket/struct.Error.html#method.pretty_print +[`RouteUri`]: https://api.rocket.rs/v0.5/rocket/route/struct.RouteUri.html # Version 0.4.10 (May 21, 2021) diff --git a/contrib/db_pools/README.md b/contrib/db_pools/README.md index 9ec29d181c..6ccaa1ff5c 100644 --- a/contrib/db_pools/README.md +++ b/contrib/db_pools/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_db_pools.svg [crate]: https://crates.io/crates/rocket_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_db_pools +[crate docs]: https://api.rocket.rs/v0.5/rocket_db_pools [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions @@ -17,7 +17,7 @@ full usage details. ```toml [dependencies.rocket_db_pools] - version = "=0.1.0-rc.4" + version = "0.1.0" features = ["sqlx_sqlite"] ``` @@ -60,8 +60,8 @@ full usage details. } ``` -[database driver features]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#supported-drivers -[`Pool`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#supported-drivers -[Configure]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/index.html#configuration -[Derive `Database`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/derive.Database.html -[`Connection`]: https://api.rocket.rs/v0.5-rc/rocket_db_pools/struct.Connection.html +[database driver features]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#supported-drivers +[`Pool`]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#supported-drivers +[Configure]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#configuration +[Derive `Database`]: https://api.rocket.rs/v0.5/rocket_db_pools/derive.Database.html +[`Connection`]: https://api.rocket.rs/v0.5/rocket_db_pools/struct.Connection.html diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index 3d23598bf2..0fd1f13e8f 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools_codegen" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Procedural macros for rocket_db_pools." repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 598fc8273a..0d7284cf14 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_db_pools" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Rocket async database pooling support" repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" @@ -29,12 +29,12 @@ diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpo [dependencies.rocket] path = "../../../core/lib" -version = "=0.5.0-rc.4" +version = "0.5.0" default-features = false [dependencies.rocket_db_pools_codegen] path = "../codegen" -version = "=0.1.0-rc.4" +version = "0.1.0" [dependencies.deadpool] version = "0.9" diff --git a/contrib/db_pools/lib/src/diesel.rs b/contrib/db_pools/lib/src/diesel.rs index 65974d3a51..911ba776b7 100644 --- a/contrib/db_pools/lib/src/diesel.rs +++ b/contrib/db_pools/lib/src/diesel.rs @@ -8,11 +8,11 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.4" +//! rocket = "0.5.0" //! diesel = "2" //! //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.4" +//! version = "0.1.0" //! features = ["diesel_mysql"] //! ``` //! diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index 05259e16e9..6e6b886add 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -7,7 +7,7 @@ //! //! ```toml //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.4" +//! version = "0.1.0" //! features = ["sqlx_sqlite"] //! ``` //! @@ -165,7 +165,7 @@ //! features = ["macros", "migrate"] //! //! [dependencies.rocket_db_pools] -//! version = "=0.1.0-rc.4" +//! version = "0.1.0" //! features = ["sqlx_sqlite"] //! ``` //! diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 1fe40ef642..92e61f8d82 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rocket_dyn_templates" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez "] description = "Dynamic templating engine integration for Rocket." -documentation = "https://api.rocket.rs/v0.5-rc/rocket_dyn_templates/" +documentation = "https://api.rocket.rs/v0.5/rocket_dyn_templates/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/dyn_templates" readme = "README.md" @@ -22,7 +22,7 @@ notify = "6" normpath = "1" [dependencies.rocket] -version = "=0.5.0-rc.4" +version = "0.5.0" path = "../../core/lib" default-features = false diff --git a/contrib/dyn_templates/README.md b/contrib/dyn_templates/README.md index 810f48461b..fad858ce79 100644 --- a/contrib/dyn_templates/README.md +++ b/contrib/dyn_templates/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_dyn_templates.svg [crate]: https://crates.io/crates/rocket_dyn_templates [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_dyn_templates +[crate docs]: https://api.rocket.rs/v0.5/rocket_dyn_templates [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions @@ -22,7 +22,7 @@ supports [Handlebars] and [Tera]. ```toml [dependencies.rocket_dyn_templates] - version = "=0.1.0-rc.4" + version = "0.1.0" features = ["handlebars", "tera"] ``` diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 7202a3a610..2dfb474d46 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -12,7 +12,7 @@ //! //! ```toml //! [dependencies.rocket_dyn_templates] -//! version = "=0.1.0-rc.4" +//! version = "0.1.0" //! features = ["handlebars", "tera"] //! ``` //! @@ -66,7 +66,7 @@ //! template directory is configured via the `template_dir` configuration //! parameter and defaults to `templates/`. The path set in `template_dir` is //! relative to the Rocket configuration file. See the [configuration -//! chapter](https://rocket.rs/v0.5-rc/guide/configuration) of the guide for more +//! chapter](https://rocket.rs/v0.5/guide/configuration) of the guide for more //! information on configuration. //! //! The corresponding templating engine used for a given template is based on a @@ -132,7 +132,7 @@ //! the templates directory since the previous request. In release builds, //! template reloading is disabled to improve performance and cannot be enabled. -#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_dyn_templates")] +#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_dyn_templates")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] diff --git a/contrib/sync_db_pools/README.md b/contrib/sync_db_pools/README.md index 9cfa24d00f..04edf74da6 100644 --- a/contrib/sync_db_pools/README.md +++ b/contrib/sync_db_pools/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_sync_db_pools.svg [crate]: https://crates.io/crates/rocket_sync_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools +[crate docs]: https://api.rocket.rs/v0.5/rocket_sync_db_pools [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions @@ -19,7 +19,7 @@ First, enable the feature corresponding to your database type: ```toml [dependencies.rocket_sync_db_pools] -version = "=0.1.0-rc.4" +version = "0.1.0" features = ["diesel_sqlite_pool"] ``` diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index c3319e9f53..a78b331054 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_sync_db_pools_codegen" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez "] description = "Procedural macros for rocket_sync_db_pools." repository = "https://github.com/SergioBenitez/Rocket/contrib/sync_db_pools" diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index 3f65e3c230..32854eab73 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rocket_sync_db_pools" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez "] description = "Rocket async database pooling support for sync database drivers." -repository = "https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/contrib/sync_db_pools" +repository = "https://github.com/SergioBenitez/Rocket/tree/v0.5/contrib/sync_db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" @@ -35,11 +35,11 @@ memcache = { version = "0.15", optional = true } r2d2-memcache = { version = "0.6", optional = true } [dependencies.rocket_sync_db_pools_codegen] -version = "=0.1.0-rc.4" +version = "0.1.0" path = "../codegen" [dependencies.rocket] -version = "=0.5.0-rc.4" +version = "0.5.0" path = "../../../core/lib" default-features = false diff --git a/contrib/sync_db_pools/lib/src/lib.rs b/contrib/sync_db_pools/lib/src/lib.rs index e390d725b4..1d83073c12 100644 --- a/contrib/sync_db_pools/lib/src/lib.rs +++ b/contrib/sync_db_pools/lib/src/lib.rs @@ -30,7 +30,7 @@ //! //! ```toml //! [dependencies.rocket_sync_db_pools] -//! version = "=0.1.0-rc.4" +//! version = "0.1.0" //! features = ["diesel_sqlite_pool"] //! ``` //! @@ -166,7 +166,7 @@ //! Lastly, databases can be configured via environment variables by specifying //! the `databases` table as detailed in the [Environment Variables //! configuration -//! guide](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables): +//! guide](https://rocket.rs/v0.5/guide/configuration/#environment-variables): //! //! ```bash //! ROCKET_DATABASES='{my_db={url="db.sqlite"}}' @@ -349,7 +349,7 @@ //! [request guards]: rocket::request::FromRequest //! [`Poolable`]: crate::Poolable -#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_sync_db_pools")] +#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_sync_db_pools")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #![cfg_attr(nightly, feature(doc_cfg))] diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index d4ae211f60..573427dffe 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rocket_ws" -version = "0.1.0-rc.4" +version = "0.1.0" authors = ["Sergio Benitez "] description = "WebSocket support for Rocket." -documentation = "https://api.rocket.rs/v0.5-rc/rocket_ws/" +documentation = "https://api.rocket.rs/v0.5/rocket_ws/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/ws" readme = "README.md" @@ -20,7 +20,7 @@ tungstenite = ["tokio-tungstenite"] tokio-tungstenite = { version = "0.20", optional = true } [dependencies.rocket] -version = "=0.5.0-rc.4" +version = "0.5.0" path = "../../core/lib" default-features = false diff --git a/contrib/ws/README.md b/contrib/ws/README.md index d7e947250d..27da59a4b5 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_ws.svg [crate]: https://crates.io/crates/rocket_ws [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_ws +[crate docs]: https://api.rocket.rs/v0.5/rocket_ws [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions @@ -16,7 +16,7 @@ This crate provides WebSocket support for Rocket via integration with Rocket's ```toml [dependencies] - ws = { package = "rocket_ws", version ="=0.1.0-rc.4" } + ws = { package = "rocket_ws", version = "0.1.0" } ``` 2. Use it! diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs index 06affb0005..4768a043fb 100644 --- a/contrib/ws/src/lib.rs +++ b/contrib/ws/src/lib.rs @@ -10,7 +10,7 @@ //! //! ```toml //! [dependencies] -//! ws = { package = "rocket_ws", version ="=0.1.0-rc.4" } +//! ws = { package = "rocket_ws", version = "0.1.0" } //! ``` //! //! Then, use [`WebSocket`] as a request guard in any route and either call @@ -70,7 +70,7 @@ //! } //! ``` -#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_ws")] +#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_ws")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index c0d4f0b152..d11974c1ce 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rocket_codegen" -version = "0.5.0-rc.4" +version = "0.5.0" authors = ["Sergio Benitez "] description = "Procedural macros for the Rocket web framework." -documentation = "https://api.rocket.rs/v0.5-rc/rocket_codegen/" +documentation = "https://api.rocket.rs/v0.5/rocket_codegen/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" @@ -21,7 +21,7 @@ quote = "1.0" syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits"] } proc-macro2 = "1.0.27" devise = "0.4" -rocket_http = { version = "=0.5.0-rc.4", path = "../http/" } +rocket_http = { version = "0.5.0", path = "../http/" } unicode-xid = "0.2" version_check = "0.9" glob = "0.3" diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 4f9a6b2faf..1d22fe431a 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1,6 +1,6 @@ #![recursion_limit="128"] -#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc")] +#![doc(html_root_url = "https://api.rocket.rs/v0.5")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] @@ -11,7 +11,7 @@ //! This crate implements the code generation portions of Rocket. This includes //! custom derives, custom attributes, and procedural macros. The documentation //! here is purely technical. The code generation facilities are documented -//! thoroughly in the [Rocket programming guide](https://rocket.rs/v0.5-rc/guide). +//! thoroughly in the [Rocket programming guide](https://rocket.rs/v0.5/guide). //! //! # Usage //! @@ -21,7 +21,7 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.4" +//! rocket = "0.5.0" //! ``` //! //! And to import all macros, attributes, and derives via `#[macro_use]` in the diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 8994e70492..a5664ce4b8 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "rocket_http" -version = "0.5.0-rc.4" +version = "0.5.0" authors = ["Sergio Benitez "] description = """ Types, traits, and parsers for HTTP requests, responses, and headers. """ -documentation = "https://api.rocket.rs/v0.5-rc/rocket_http/" +documentation = "https://api.rocket.rs/v0.5/rocket_http/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index c29d1d968b..2203ee1adb 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "rocket" -version = "0.5.0-rc.4" +version = "0.5.0" authors = ["Sergio Benitez "] description = """ Web framework with a focus on usability, security, extensibility, and speed. """ -documentation = "https://api.rocket.rs/v0.5-rc/rocket/" +documentation = "https://api.rocket.rs/v0.5/rocket/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" @@ -61,11 +61,11 @@ tokio-stream = { version = "0.1.6", features = ["signal", "time"] } state = "0.6" [dependencies.rocket_codegen] -version = "=0.5.0-rc.4" +version = "0.5.0" path = "../codegen" [dependencies.rocket_http] -version = "=0.5.0-rc.4" +version = "0.5.0" path = "../http" features = ["serde"] diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 4a23bf4f3d..3856e22874 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -23,7 +23,7 @@ use crate::config::SecretKey; /// See the [module level docs](crate::config) as well as the [configuration /// guide] for further details. /// -/// [configuration guide]: https://rocket.rs/v0.5-rc/guide/configuration/ +/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ /// /// # Defaults /// diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index a811cf999a..6bd2de3ff2 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -2,7 +2,7 @@ //! //! See the [configuration guide] for full details. //! -//! [configuration guide]: https://rocket.rs/v0.5-rc/guide/configuration/ +//! [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ //! //! ## Extracting Configuration Parameters //! diff --git a/core/lib/src/config/secret_key.rs b/core/lib/src/config/secret_key.rs index 02947e7090..8302c7f482 100644 --- a/core/lib/src/config/secret_key.rs +++ b/core/lib/src/config/secret_key.rs @@ -71,8 +71,8 @@ enum Kind { /// assert!(matches!(error.kind(), ErrorKind::InsecureSecretKey(profile))); /// ``` /// -/// [private cookies]: https://rocket.rs/v0.5-rc/guide/requests/#private-cookies -/// [configuration guide]: https://rocket.rs/v0.5-rc/guide/configuration/#secret-key +/// [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies +/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/#secret-key #[derive(Clone)] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub struct SecretKey { diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs index 1439d8f4cd..be17959562 100644 --- a/core/lib/src/fairing/mod.rs +++ b/core/lib/src/fairing/mod.rs @@ -423,7 +423,7 @@ pub type Result, E = Rocket> = std::result::Result { /// The value, if it was successfully parsed, or `None` otherwise. diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 12b1c40ced..cd80db4450 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -12,7 +12,7 @@ use crate::form::prelude::*; /// This type implements the [`FromData`] trait. It provides a generic means to /// parse arbitrary structures from incoming form data. /// -/// See the [forms guide](https://rocket.rs/v0.5-rc/guide/requests#forms) for +/// See the [forms guide](https://rocket.rs/v0.5/guide/requests#forms) for /// general form support documentation. /// /// # Leniency diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index 347b62d80c..0f04803d14 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -65,7 +65,7 @@ use crate::http::uncased::AsUncased; /// [FromFormField]: crate::form::FromFormField /// [`shift()`ed]: NameView::shift() /// [`key()`]: NameView::key() -/// [forms guide]: https://rocket.rs/v0.5-rc/guide/requests/#forms +/// [forms guide]: https://rocket.rs/v0.5/guide/requests/#forms /// /// # Parsing Strategy /// diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 44487876f9..1d1c21b7ba 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -1,6 +1,6 @@ //! Parsing and validation of HTTP forms and fields. //! -//! See the [forms guide](https://rocket.rs/v0.5-rc/guide/requests#forms) for +//! See the [forms guide](https://rocket.rs/v0.5/guide/requests#forms) for //! general form support documentation. //! //! # Field Wire Format diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 8ac0f9944b..33874a7fcc 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -1,6 +1,6 @@ #![recursion_limit="256"] -#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc")] +#![doc(html_root_url = "https://api.rocket.rs/v0.5")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #![cfg_attr(nightly, feature(doc_cfg))] @@ -18,10 +18,10 @@ //! detailed guide]. If you'd like pointers on getting started, see the //! [quickstart] or [getting started] chapters of the guide. //! -//! [overview]: https://rocket.rs/v0.5-rc/overview -//! [full, detailed guide]: https://rocket.rs/v0.5-rc/guide -//! [quickstart]: https://rocket.rs/v0.5-rc/guide/quickstart -//! [getting started]: https://rocket.rs/v0.5-rc/guide/getting-started +//! [overview]: https://rocket.rs/v0.5/overview +//! [full, detailed guide]: https://rocket.rs/v0.5/guide +//! [quickstart]: https://rocket.rs/v0.5/guide/quickstart +//! [getting started]: https://rocket.rs/v0.5/guide/getting-started //! //! ## Usage //! @@ -29,13 +29,13 @@ //! //! ```toml //! [dependencies] -//! rocket = "=0.5.0-rc.4" +//! rocket = "0.5.0" //! ``` //! //! Note that development versions, tagged with `-dev`, are not published //! and need to be specified as [git dependencies]. //! -//! See the [guide](https://rocket.rs/v0.5-rc/guide) for more information on how +//! See the [guide](https://rocket.rs/v0.5/guide) for more information on how //! to write Rocket applications. Here's a simple example to get you started: //! //! [git dependencies]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories @@ -73,21 +73,21 @@ //! //! ```toml //! [dependencies] -//! rocket = { version = "=0.5.0-rc.4", features = ["secrets", "tls", "json"] } +//! rocket = { version = "0.5.0", features = ["secrets", "tls", "json"] } //! ``` //! //! Conversely, HTTP/2 can be disabled: //! //! ```toml //! [dependencies] -//! rocket = { version = "=0.5.0-rc.4", default-features = false } +//! rocket = { version = "0.5.0", default-features = false } //! ``` //! //! [JSON (de)serialization]: crate::serde::json //! [MessagePack (de)serialization]: crate::serde::msgpack //! [UUID value parsing and (de)serialization]: crate::serde::uuid -//! [private cookies]: https://rocket.rs/v0.5-rc/guide/requests/#private-cookies -//! [TLS]: https://rocket.rs/v0.5-rc/guide/configuration/#tls +//! [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies +//! [TLS]: https://rocket.rs/v0.5/guide/configuration/#tls //! [mutual TLS]: crate::mtls //! //! ## Configuration @@ -103,8 +103,8 @@ //! integration testing of a Rocket application. The top-level [`local`] module //! documentation and the [testing guide] include detailed examples. //! -//! [configuration guide]: https://rocket.rs/v0.5-rc/guide/configuration/ -//! [testing guide]: https://rocket.rs/v0.5-rc/guide/testing/#testing +//! [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ +//! [testing guide]: https://rocket.rs/v0.5/guide/testing/#testing //! [Figment]: https://docs.rs/figment /// These are public dependencies! Update docs if these are changed, especially diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index 78668aa16b..4a2bed5ec5 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -80,7 +80,7 @@ //! //! For more details on testing, see the [testing guide]. //! -//! [testing guide]: https://rocket.rs/v0.5-rc/guide/testing/ +//! [testing guide]: https://rocket.rs/v0.5/guide/testing/ //! [`Client`]: crate::local::asynchronous::Client //! //! # `Client` diff --git a/core/lib/src/mtls.rs b/core/lib/src/mtls.rs index 5910bb520c..93e79945a9 100644 --- a/core/lib/src/mtls.rs +++ b/core/lib/src/mtls.rs @@ -2,7 +2,7 @@ //! //! For details on how to configure mutual TLS, see //! [`MutualTls`](crate::config::MutualTls) and the [TLS -//! guide](https://rocket.rs/v0.5-rc/guide/configuration/#tls). See +//! guide](https://rocket.rs/v0.5/guide/configuration/#tls). See //! [`Certificate`] for a request guard that validated, verifies, and retrieves //! client certificates. diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index a0776aaa9b..1dacb4d2b5 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -368,7 +368,7 @@ pub type Outcome = outcome::Outcome; /// Notice that these request guards provide access to *borrowed* data (`&'a /// User` and `Admin<'a>`) as the data is now owned by the request's cache. /// -/// [request-local state]: https://rocket.rs/v0.5-rc/guide/state/#request-local-state +/// [request-local state]: https://rocket.rs/v0.5/guide/state/#request-local-state #[crate::async_trait] pub trait FromRequest<'r>: Sized { /// The associated error to be returned if derivation fails. diff --git a/core/lib/src/serde/json.rs b/core/lib/src/serde/json.rs index 3a22f82258..677b20e726 100644 --- a/core/lib/src/serde/json.rs +++ b/core/lib/src/serde/json.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.4" +//! version = "0.5.0" //! features = ["json"] //! ``` //! diff --git a/core/lib/src/serde/msgpack.rs b/core/lib/src/serde/msgpack.rs index ff2381dc34..7af49700de 100644 --- a/core/lib/src/serde/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.4" +//! version = "0.5.0" //! features = ["msgpack"] //! ``` //! diff --git a/core/lib/src/serde/uuid.rs b/core/lib/src/serde/uuid.rs index 03c5f7811a..f834932a98 100644 --- a/core/lib/src/serde/uuid.rs +++ b/core/lib/src/serde/uuid.rs @@ -7,7 +7,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "=0.5.0-rc.4" +//! version = "0.5.0" //! features = ["uuid"] //! ``` //! diff --git a/core/lib/src/shield/mod.rs b/core/lib/src/shield/mod.rs index 09fe3e4d6c..b893c6c86c 100644 --- a/core/lib/src/shield/mod.rs +++ b/core/lib/src/shield/mod.rs @@ -4,7 +4,7 @@ //! security and privacy headers into all outgoing responses. It takes some //! inspiration from [helmetjs], a similar piece of middleware for [express]. //! -//! [fairing]: https://rocket.rs/v0.5-rc/guide/fairings/ +//! [fairing]: https://rocket.rs/v0.5/guide/fairings/ //! [helmetjs]: https://helmetjs.github.io/ //! [express]: https://expressjs.com //! diff --git a/scripts/config.sh b/scripts/config.sh index bfae241342..a9dc777a9a 100755 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -58,8 +58,8 @@ VERSION=$(git grep -h "^version" "${CORE_LIB_ROOT}" | head -n 1 | cut -d '"' -f2 MAJOR_VERSION=$(echo "${VERSION}" | cut -d'.' -f1-2) VIRTUAL_CODENAME="$(git branch --show-current)" PHYSICAL_CODENAME="v${MAJOR_VERSION}" -CURRENT_RELEASE=false -PRE_RELEASE=true +CURRENT_RELEASE=true +PRE_RELEASE=false # A generated codename for this version. Use the git branch for pre-releases. case $PRE_RELEASE in diff --git a/site/README.md b/site/README.md index 1bd8a6842a..d56b05eaed 100644 --- a/site/README.md +++ b/site/README.md @@ -13,7 +13,7 @@ This directory contains the following: * `news/*.md` - News articles linked to from `news/index.toml`. * `guide/*.md` - Guide pages linked to from `guide.md`. -[Rocket Programming Guide]: https://rocket.rs/v0.5-rc/guide/ +[Rocket Programming Guide]: https://rocket.rs/v0.5/guide/ ### Guide Links diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index cdee2af4e9..f833aa7cfc 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -38,7 +38,7 @@ private cookies, you _must_ enable the `secrets` feature in `Cargo.toml`: ```toml [dependencies] -rocket = { version = "=0.5.0-rc.4", features = ["secrets"] } +rocket = { version = "0.5.0", features = ["secrets"] } ``` ### Contrib Deprecation @@ -59,8 +59,8 @@ to `Cargo.toml`: [dependencies] - rocket = "0.4" - rocket_contrib = { version = "0.4", features = ["json"], default-features = false } -+ rocket = { version = "=0.5.0-rc.4", features = ["json"] } -+ rocket_dyn_templates = { version = "=0.1.0-rc.4", features = ["tera"] } ++ rocket = { version = "0.5.0", features = ["json"] } ++ rocket_dyn_templates = { version = "0.1.0", features = ["tera"] } ``` ! note: `rocket_dyn_templates` (and co.) _does not_ follow in version lock-step diff --git a/site/guide/1-quickstart.md b/site/guide/1-quickstart.md index 5d43c1003b..51f3d5efa0 100644 --- a/site/guide/1-quickstart.md +++ b/site/guide/1-quickstart.md @@ -14,7 +14,7 @@ For instance, the following set of commands runs the `hello` example: ```sh git clone https://github.com/SergioBenitez/Rocket cd Rocket -git checkout v0.5-rc +git checkout v0.5 cd examples/hello cargo run ``` diff --git a/site/guide/10-pastebin-tutorial.md b/site/guide/10-pastebin-tutorial.md index e0a6d222f8..ac44d064ae 100644 --- a/site/guide/10-pastebin-tutorial.md +++ b/site/guide/10-pastebin-tutorial.md @@ -52,7 +52,7 @@ Then add the usual Rocket dependencies to the `Cargo.toml` file: ```toml [dependencies] -rocket = "=0.5.0-rc.4" +rocket = "0.5.0" ``` And finally, create a skeleton Rocket application to work off of in diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index a11078fd9e..29854f430e 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -54,7 +54,7 @@ looks like: | Framework | Dependencies | Build Time | |----------------------|--------------|------------| -| Rocket 0.5-rc.2 | 151 | 50s | +| Rocket 0.5 | 151 | 50s | | Actix-Web 4.0.1 | 155 | 40s | | Tide 0.16 | 202 | 37s | | Warp 0.3.2 | 132 | 30s | @@ -646,7 +646,7 @@ is to depend on a `contrib` library from git while also depending on a `crates.io` version of Rocket or vice-versa: ```toml -rocket = "=0.5.0-rc.4" +rocket = "0.5.0" rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" } ``` diff --git a/site/guide/2-getting-started.md b/site/guide/2-getting-started.md index 74d601a473..0f485ca1a3 100644 --- a/site/guide/2-getting-started.md +++ b/site/guide/2-getting-started.md @@ -43,7 +43,7 @@ Now, add Rocket as a dependency in your `Cargo.toml`: ```toml [dependencies] -rocket = "=0.5.0-rc.4" +rocket = "0.5.0" ``` ! warning: Development versions must be _git_ dependencies. diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index fcdf6fb353..8569ed7241 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -606,7 +606,7 @@ feature: ```toml ## in Cargo.toml -rocket = { version = "=0.5.0-rc.4", features = ["secrets"] } +rocket = { version = "0.5.0", features = ["secrets"] } ``` The API for retrieving, adding, and removing private cookies is identical except @@ -784,7 +784,7 @@ complete example. feature can be enabled in the `Cargo.toml`: ` - rocket = { version = "=0.5.0-rc.4", features = ["json"] } + rocket = { version = "0.5.0", features = ["json"] } ` ### Temporary Files diff --git a/site/guide/6-state.md b/site/guide/6-state.md index fb70b8832e..20ae3a04d1 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -231,7 +231,7 @@ in three simple steps: ```toml [dependencies.rocket_db_pools] - version = "=0.1.0-rc.4" + version = "0.1.0" features = ["sqlx_sqlite"] ``` @@ -299,7 +299,7 @@ default-features = false features = ["macros", "migrate"] [dependencies.rocket_db_pools] -version = "=0.1.0-rc.4" +version = "0.1.0" features = ["sqlx_sqlite"] ``` diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 19a0d8d639..b2f47c0004 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -237,7 +237,7 @@ Security). To enable TLS support: ```toml,ignore [dependencies] - rocket = { version = "=0.5.0-rc.4", features = ["tls"] } + rocket = { version = "0.5.0", features = ["tls"] } ``` 2. Configure a TLS certificate chain and private key via the `tls.key` and @@ -302,7 +302,7 @@ enabled and support configured via the `tls.mutual` config parameter: ```toml,ignore [dependencies] - rocket = { version = "=0.5.0-rc.4", features = ["mtls"] } + rocket = { version = "0.5.0", features = ["mtls"] } ``` This implicitly enables the `tls` feature. diff --git a/site/index.toml b/site/index.toml index 87bad8cee8..a8846e5363 100644 --- a/site/index.toml +++ b/site/index.toml @@ -3,8 +3,8 @@ ############################################################################### [release] -version = "0.5.0-rc.4" -date = "Nov 1, 2023" +version = "0.5.0" +date = "Nov XX, 2023" ############################################################################### # Top features: displayed in the header under the introductory text. diff --git a/site/news/2021-06-09-version-0.5-rc.1.md b/site/news/2021-06-09-version-0.5-rc.1.md index 18792e438a..0044b4e877 100644 --- a/site/news/2021-06-09-version-0.5-rc.1.md +++ b/site/news/2021-06-09-version-0.5-rc.1.md @@ -27,7 +27,7 @@ to your feedback! [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/CHANGELOG.md#version-050-rc1-jun-9-2021 +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc1-jun-9-2021 [API docs]: @api [guide]: ../../guide diff --git a/site/news/2022-05-09-version-0.5-rc.2.md b/site/news/2022-05-09-version-0.5-rc.2.md index c3d9c00ede..58fd3dc71b 100644 --- a/site/news/2022-05-09-version-0.5-rc.2.md +++ b/site/news/2022-05-09-version-0.5-rc.2.md @@ -27,7 +27,7 @@ for the general release! [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions [migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/CHANGELOG.md#version-050-rc2-may-9-2022 +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 [API docs]: @api [guide]: ../../guide diff --git a/site/news/2023-03-23-version-0.5-rc.3.md b/site/news/2023-03-23-version-0.5-rc.3.md index c7b86cfbb3..cec4823b14 100644 --- a/site/news/2023-03-23-version-0.5-rc.3.md +++ b/site/news/2023-03-23-version-0.5-rc.3.md @@ -17,7 +17,7 @@ v0.4. For changes since Rocket v0.5.0-rc.2, please see the [CHANGELOG]. [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions [migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/CHANGELOG.md#version-050-rc2-may-9-2022 +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 ## About Rocket diff --git a/site/news/2023-05-26-version-0.5.md b/site/news/2023-05-26-version-0.5.md index 8249b1f37a..d4f3c8869c 100644 --- a/site/news/2023-05-26-version-0.5.md +++ b/site/news/2023-05-26-version-0.5.md @@ -176,7 +176,7 @@ For complete usage details, see the [`rocket_ws`] documentation. [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions [migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5-rc/CHANGELOG.md#version-050-rc2-may-9-2022 +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 ## Thank You diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index 11269a589e..61d6ff50d3 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_guide_tests" -version = "0.5.0-rc.4" +version = "0.5.0" workspace = "../../" edition = "2021" publish = false @@ -22,4 +22,4 @@ path = "../../contrib/db_pools/lib" features = ["sqlx_sqlite"] [dev-dependencies.rocket_ws] -path = "../../contrib/ws/lib" +path = "../../contrib/ws" From f7a6c8610e0dfbc1fab809847714aa832ca73137 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 9 Nov 2023 13:50:21 -0800 Subject: [PATCH 032/178] Update FAQ for 0.5.0. --- site/guide/12-faq.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index 29854f430e..895678690c 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -48,22 +48,18 @@ and correctness blunders. It does this by including, out-of-the-box: trivial DoS attacks. Of course, this functionality comes at a compile-time cost (but notably, _not_ -at a runtime cost), impacting Rocket's clean build-time. For comparison, here's -what building "Hello, world!" for the first time in popular Rust web frameworks -looks like: - -| Framework | Dependencies | Build Time | -|----------------------|--------------|------------| -| Rocket 0.5 | 151 | 50s | -| Actix-Web 4.0.1 | 155 | 40s | -| Tide 0.16 | 202 | 37s | -| Warp 0.3.2 | 132 | 30s | -| Axum 0.5.4 | 81 | 18s | - -ยท Measurements taken on a MacBookPro15,1 Intel Core i9 @ 2.9GHZ, macOS -12.1, Rust 1.60 stable. Best of 3.
-ยท Rocket includes features like multipart parsing and static file -serving that would require additional deps in other frameworks. +a runtime cost), impacting Rocket's clean build-time. For comparison, here's +what a clean build of "Hello, world!" looks like for some Rust web frameworks: + +| Framework | Dependencies | Build Time | Build w/ `sscache` | +|-----------------|--------------|------------|--------------------| +| Rocket 0.5 | 105 | 12s | 5s | +| Actix-Web 4.4.0 | 119 | 11s | 4s | +| Axum 0.6.20 | 78 | 10s | 4s | + +ยท Measurements taken on Apple Mac14,6 M2 Max, macOS 13, Rust 1.75. Best of 3.
+ยท Rocket includes features like graceful shutdown, HTTP/2 keepalive, SSE +support, and static file serving that require additional deps in other frameworks. Of course, iterative build-time is nearly identical for all frameworks, and the time can be further reduced by using faster linkers like `lld`. We think the From b70c2374616c7d61ea0f6665ddf6e9f6c3c07440 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 12 Nov 2023 17:21:46 -0800 Subject: [PATCH 033/178] Polish news for v0.5. Add RWF2 announcement. --- site/index.toml | 2 +- site/news/2023-05-26-version-0.5.md | 325 ------------- site/news/2023-11-17-rwf2-prelaunch.md | 253 ++++++++++ site/news/2023-11-17-version-0.5.md | 622 +++++++++++++++++++++++++ site/news/index.toml | 25 +- 5 files changed, 895 insertions(+), 332 deletions(-) delete mode 100644 site/news/2023-05-26-version-0.5.md create mode 100644 site/news/2023-11-17-rwf2-prelaunch.md create mode 100644 site/news/2023-11-17-version-0.5.md diff --git a/site/index.toml b/site/index.toml index a8846e5363..b722b5c6d9 100644 --- a/site/index.toml +++ b/site/index.toml @@ -4,7 +4,7 @@ [release] version = "0.5.0" -date = "Nov XX, 2023" +date = "Nov 17, 2023" ############################################################################### # Top features: displayed in the header under the introductory text. diff --git a/site/news/2023-05-26-version-0.5.md b/site/news/2023-05-26-version-0.5.md deleted file mode 100644 index d4f3c8869c..0000000000 --- a/site/news/2023-05-26-version-0.5.md +++ /dev/null @@ -1,325 +0,0 @@ -# Rocket v0.5: Stable, Async, Sentinels, Figment, Shield, Streams, SSE, WebSockets, & More! - - - -Four years, almost a thousand commits, and over a thousand issues, discussions, -and PRs later, I am ~~relieved~~ thrilled to announce the general availability -of Rocket v0.5. - -> **Rocket** is a backend web framework for Rust with a focus on usability, -> security, extensibility, and speed. Rocket makes it simple to write secure web -> applications without sacrificing usability or performance. - -We encourage all users to upgrade. For a guided upgrade from Rocket v0.4 to -Rocket v0.5, please consult the newly available [upgrading guide]. Rocket v0.4 -will continue to be supported and receive security updates until the next major -release. - -[upgrading guide]: ../../guide/upgrading - -## What's New? - -Almost every aspect has been reevaluated with a focus on usability, security, -and consistency across the library and [broader ecosystem]. The changes are -numerous, so we focus on the most impactful changes here and encourage everyone -to read the [CHANGELOG] for a complete list. For answers to frequently asked -questions, see the new [FAQ]. - -[broader ecosystem]: ../../guide/faq/#releases -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0/CHANGELOG.md -[FAQ]: ../../guide/faq - -### โš“ Support for Stable `rustc` - -Rocket v0.5 compiles and builds on Rust stable with an entirely asynchronous -core. This means that you can compile Rocket application with `rustc` from the -stable release channel. - -Using the stable release channel ensures that _no_ breakages will occur when -upgrading your Rust compiler or Rocket. That being said, Rocket continues to -take advantage of features only present in the nightly channel. - -### ๐Ÿ“ฅ Async I/O - -The new asynchronous core requires an async runtime to run. The new -[`launch`] and [`main`] attributes simplify starting a runtime suitable for -running Rocket applications. You should use [`launch`] whenever possible. - -Additionally, the `rocket::ignite()` function has been renamed to -[`rocket::build()`]; calls to the function or method should be replaced -accordingly. Together, these two changes result in the following diff to what -was previously the `main` function: - -```diff -- fn main() { -- rocket::ignite().mount("/hello", routes![hello]).launch(); -- } -+ #[launch] -+ fn rocket() -> _ { -+ rocket::build().mount("/hello", routes![hello]) -+ } -``` - -### ๐Ÿ’‚ Sentinels - -Rocket v0.5 introduces [sentinels]. Entirely unique to Rocket, sentinels offer -an automatic last line of defense against runtime errors by enabling any type -that appears in a route to abort application launch if invalid conditions are -detected. For example, the [`&State`] guard in v0.5 is a [`Sentinel`] that -aborts launch if the type `T` is not in managed state, thus preventing -associated runtime errors. - -You should consider implementing `Sentinel` for your types if you have guards -(request, data, form, etc.) or responders that depend on `Rocket` state to -function properly. For example, consider a `MyResponder` that expects: - - * An error catcher to be registered for the `400` status code. - * A specific type `T` to be in managed state. - -Making `MyResponder` a sentinel that guards against these conditions is as -simple as: - -```rust -use rocket::{Rocket, Ignite, Sentinel}; -# struct MyResponder; -# struct T; - -impl Sentinel for MyResponder { - fn abort(r: &Rocket) -> bool { - !r.catchers().any(|c| c.code == Some(400)) || r.state::().is_none() - } -} -``` - -[sentinels]: @api/rocket/trait.Sentinel.html -[`Sentinel`]: @api/rocket/trait.Sentinel.html -[`&State`]: @api/rocket/struct.State.html - - -### ๐Ÿ›ก๏ธ Shield - -### ๐ŸŒŠ Streams and SSE - -Rocket v0.5 introduces real-time, typed, `async` [streams]. The new [async -streams] section of the guide contains further details, and we encourage all -interested parties to see the new real-time, multi-room [chat example]. - -As a taste of what's possible, the following `stream` route emits a `"ping"` -Server-Sent Event every `n` seconds, defaulting to `1`: - -```rust -# use rocket::*; -use rocket::response::stream::{Event, EventStream};; -use rocket::tokio::time::{interval, Duration}; - -#[get("/ping?")] -fn stream(n: Option) -> EventStream![] { - EventStream! { - let mut timer = interval(Duration::from_secs(n.unwrap_or(1))); - loop { - yield Event::data("ping"); - timer.tick().await; - } - } -} -``` - -[streams]: @api/rocket/response/stream/index.html -[async streams]: @guide/responses/#async-streams -[chat example]: @example/chat - -### ๐Ÿ”Œ WebSockets - -Rocket v0.5 introduces support for HTTP connection upgrades via a new [upgrade -API]. The API allows responders to take over an HTTP connection and perform raw -I/O with the client. In other words, an HTTP connection can be _upgraded_ to any -protocol, including HTTP WebSockets! - -The newly introduced [`rocket_ws`] library takes advantage of the new API to -implement first-class support for WebSockets entirely outside of Rocket's core. -The simplest use of the library, implementing an echo server and showcasing that -the incoming message stream is `async`, looks like this: - -```rust -# use rocket::get; -# use rocket_ws as ws; - -#[get("/echo")] -fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { - ws.stream(|io| io) -} -``` - -The simplified [async streams] generator syntax can also be used: - -```rust -# use rocket::get; -# use rocket_ws as ws; - -#[get("/echo")] -fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::Stream! { ws => - for await message in ws { - yield message?; - } - } -} -``` - -For complete usage details, see the [`rocket_ws`] documentation. - -[upgrade API]: @api/rocket/response/struct.Response.html#upgrading -[`rocket_ws`]: @api/rocket_ws - -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 - -## Thank You - -
    -
  • Aaron Leopold
  • -
  • Abdullah Alyan
  • -
  • Aditya
  • -
  • Alex Macleod
  • -
  • Alex Sears
  • -
  • Alexander van Ratingen
  • -
  • ami-GS
  • -
  • Antoine Martin
  • -
  • arctic-alpaca
  • -
  • arlecchino
  • -
  • Arthur Woimbรฉe
  • -
  • atouchet
  • -
  • Aurora
  • -
  • badoken
  • -
  • Beep LIN
  • -
  • Ben Sully
  • -
  • Benedikt Weber
  • -
  • BlackDex
  • -
  • Bonex
  • -
  • Brenden Matthews
  • -
  • Brendon Federko
  • -
  • Brett Buford
  • -
  • Cedric Hutchings
  • -
  • Cezar Halmagean
  • -
  • Charles-Axel Dein
  • -
  • Compro Prasad
  • -
  • Daniel Wiesenberg
  • -
  • David Venhoek
  • -
  • Dimitri Sabadie
  • -
  • Dinu Blanovschi
  • -
  • Dominik Boehi
  • -
  • Doni Rubiagatra
  • -
  • Edgar Onghena
  • -
  • Edwin Svensson
  • -
  • est31
  • -
  • Felix Suominen
  • -
  • Filip Gospodinov
  • -
  • Flying-Toast
  • -
  • Follpvosten
  • -
  • Francois Stephany
  • -
  • Gabriel Fontes
  • -
  • gcarq
  • -
  • George Cheng
  • -
  • Giles Cope
  • -
  • Gonรงalo Ribeiro
  • -
  • hiyoko3m
  • -
  • Howard Su
  • -
  • hpodhaisky
  • -
  • Ian Jackson
  • -
  • IFcoltransG
  • -
  • Indosaram
  • -
  • inyourface34456
  • -
  • J. Cohen
  • -
  • Jacob Pratt
  • -
  • Jacob Sharf
  • -
  • Jacob Simpson
  • -
  • Jakub Dฤ…bek
  • -
  • Jakub Wieczorek
  • -
  • James Tai
  • -
  • Jason Hinch
  • -
  • Jeb Rosen
  • -
  • Jeremy Kaplan
  • -
  • Joakim Soderlund
  • -
  • Johannes Liebermann
  • -
  • John-John Tedro
  • -
  • Jonah Brรผchert
  • -
  • Jonas Mรธller
  • -
  • Jonathan Dickinson
  • -
  • Jonty
  • -
  • Joscha
  • -
  • Joshua Nitschke
  • -
  • JR Heard
  • -
  • Juhasz Sandor
  • -
  • Julian Bรผttner
  • -
  • Juraj Fiala
  • -
  • Kenneth Allen
  • -
  • Kevin Wang
  • -
  • Kian-Meng Ang
  • -
  • Konrad Borowski
  • -
  • Leonora Tindall
  • -
  • lewis
  • -
  • Lionel G
  • -
  • Lucille Blumire
  • -
  • Mai-Lapyst
  • -
  • Manuel
  • -
  • Marc Schreiber
  • -
  • Marc-Stefan Cassola
  • -
  • Marshall Bowers
  • -
  • Martin1887
  • -
  • Martinez
  • -
  • Matthew Pomes
  • -
  • Maxime Guerreiro
  • -
  • meltinglava
  • -
  • Michael Howell
  • -
  • Mikail Bagishov
  • -
  • mixio
  • -
  • multisn8
  • -
  • Necmettin Karakaya
  • -
  • Ning Sun
  • -
  • Nya
  • -
  • Paolo Barbolini
  • -
  • Paul Smith
  • -
  • Paul van Tilburg
  • -
  • Paul Weaver
  • -
  • pennae
  • -
  • Petr Portnov
  • -
  • philipp
  • -
  • Pieter Frenssen
  • -
  • PROgrm_JARvis
  • -
  • Razican
  • -
  • Redrield
  • -
  • Rรฉmi Lauzier
  • -
  • Riley Patterson
  • -
  • Rodolphe Brรฉard
  • -
  • Roger Mo
  • -
  • RotesWasser
  • -
  • rotoclone
  • -
  • Rudi Floren
  • -
  • Samuele Esposito
  • -
  • Scott McMurray
  • -
  • Sergio Benitez
  • -
  • Silas Sewell
  • -
  • Soham Roy
  • -
  • Stuart Hinson
  • -
  • Thibaud Martinez
  • -
  • Thomas Eckert
  • -
  • ThouCheese
  • -
  • Tilen Pintariฤ
  • -
  • timando
  • -
  • timokoesters
  • -
  • toshokan
  • -
  • TotalKrill
  • -
  • Vasili
  • -
  • Vladimir Ignatev
  • -
  • Wesley Norris
  • -
  • xelivous
  • -
  • YetAnotherMinion
  • -
  • Yohannes Kifle
  • -
  • Yusuke Kominami
  • -
- -## What's Next? diff --git a/site/news/2023-11-17-rwf2-prelaunch.md b/site/news/2023-11-17-rwf2-prelaunch.md new file mode 100644 index 0000000000..e665fdb28b --- /dev/null +++ b/site/news/2023-11-17-rwf2-prelaunch.md @@ -0,0 +1,253 @@ +# Building a Better Foundation for Rocket's Future + + + +Along with the [release of Rocket v0.5], today I'm sharing plans to launch the +Rocket Web Framework Foundation, or [_RWF2_]. The RWF2 is a nonprofit +organization designed to support Rocket and the surrounding ecosystem, +financially and organizationally. + +I'm also directly addressing the community's concerns regarding the pace of +Rocket's development, leadership, and release cadence. My hope is to assuage any +and all concerns about Rocket's future. I hope reading this leaves you feeling +confident that Rocket is here to stay, and that the RWF2 is the right step +towards increased community contributions and involvement. + +! note: This is a co-announcement [along with release of Rocket v0.5]. + +[along with release of Rocket v0.5]: ../2023-11-17-version-0.5/ +[release of Rocket v0.5]: ../2023-11-17-version-0.5/ +[_RWF2_]: https://rwf2.org +[RWF2]: https://rwf2.org + +## Background + +I released Rocket in 2016 to fanfare. It was lauded as _the_ web framework for +Rust. But in the last few years, I'd be remiss to claim the same. New frameworks +have emerged, Rocket's recent development has been nothing short of erratic, and +four years went by without a major release. + +The community rightfully voiced its disappointment and concern. Posts inquired +about the project's status: was it dead? I received copious email ranging from +concern over my well-being, to anger, to requests to transfer the project +entirely. The community ~~wasn't~~ isn't happy with Rocket. + +And I get it. I failed to adequately lead the project. I failed to communicate +when it mattered most. I couldn't control the life events that pulled me away +from Rocket and most of my responsibilities, but I could have done more to +communicate what was going on. And I certainly could have done _something_ to +make it easy, make it _possible_ for others to push the project forward in my +absense. + +But I did none of that. I couldn't make it happen. And I'm truly, sincerely +sorry. + +## A Better Foundation for Rocket's Future + +I'd like to make it impossible to repeat these mistakes. That's why today I'm +announcing plans for a new independent nonprofit foundation designed to support +and bolster Rocket's development, increase transparency, and diversify project +leadership: [RWF2]. + +> The **R**ocket **W**eb **F**ramework **F**oundation, _RWF2_, is a +> +> 501(c)(3) nonprofit and +> collective that supports the development and community of free and open +> source software, like Rocket, as well as +> education for a more secure web. + +Moving forward, the RWF2 will be responsible for governing Rocket and dictating +its trajectory. The goal is to distribute control of the project and prohibit +one person from being able to stall its development. The RWF2 will also act as a +vehicle for tax-deductible contributions, funds management, and development +grant distribution, all with the aim of increasing high-quality contributions +and educational material. + +In summary, the RWF2 exists to enable: + + * **Diversified Leadership** + + Key responsibilities, such as releases, security, infrastructure, and + community engagement will be distributed to community members under the + umbrella of the foundation. + + * **Tax-Deductible Contributions** + + Because the RWF2 is a 501(c)(3) organization, contributions are + tax-deductible. We particularly hope this encourages corporate sponsorship, + especially from those who depend on Rocket. As a nonprofit, the RWF2 must + transparently manage and disburse all funds. + + * **Development Grants** + + A key use for contributions is the foundation's sponsorship and + administration of ยตGrants: small (โ‰ค $1k) grants for concrete work on + Rocket or related projects. Compensation is staged upon completion of + predefined milestones and quality requirements. + + * **Increased Transparency** + + Milestones, release schedules, and periodic updates form part of the + foundation's responsibilities. The aim is to keep the community informed on + Rocket's development and plans, making it easier to get and remain involved. + + * **Educational Resource Expansion** + + The RWF2 aims to enhance the accessibility of educational resources, + training, and mentorship for web application security, especially for + traditionally marginalized groups. Our focus lies in delivering + high-quality, practical materials for building secure web applications. + +## What's Happening Now + +There's a lot to do to realize these goals, but the process starts today. Here's +what's being done now: + + 0. **Open Sponsorship** + + Starting now, you can sponsor the RWF2 through [GitHub Sponsors] or + [Open Collective]. Tiers are still a work in progress, but for now, consider + all tiers on Open Collective, and Bronze+ tiers on GitHub, as intended for + corporate sponsors. Note that only contributions made directly via Open + Collective are guaranteed to be tax-deductible. + + A special shout out to `@martynp`, `@nathanielford`, and `@wezm` for + jumping the gun in the best of ways and sponsoring the RWF2 via GitHub + ahead of schedule. Thank you! + + 0. **Team Assembly** + + Initially, RWF2 governance will be exceedingly simple and consist of a + president (hi!) and a handful of team leads. Individuals can + fill multiple positions, though the intent is for every position to be held + by a different individual. Positions are by appointment, either by the + presiding team lead, by the president in their absence, and by other team + leads in the president's absence. + + The initial teams and their responsibilities are listed below. If you're + interested in leading any of the teams (or another team you think should + exist), please reach out via the [Matrix channel] or directly via + [foundation@rwf2.org](mailto:foundation@rwf2.org). + + - *Maintenance* + + Reviews issues, pull requests, and discussions, and acts on them as + necessary. This largely means triaging issues, closing resolved or + duplicate issues and discussions, closing or merging stale or approved + pull requests, respectively, and pinging the appropriate individuals to + prevent issues or PRs from becoming stale. + + - *Release* + + Publishes code and documentation releases. This includes partitioning + commits according to scope and impact on breakage, writing and updating + CHANGELOGs, and testing and publishing new releases and their + documentation. + + - *Knowledge* + + Creates, maintains and improves materials that help others learn about + Rocket or web security. This includes documentation like API docs and the + Rocket guide, code such as examples and tutorials, and materials for live + or in-person education. + + - *Community* + + Keeps the community adjourned on happenings. This involves writing + periodic project updates as well as digesting and communicating + development milestones and schedules to a broad audience. + + - *Infrastructure* + + Maintains infrastructure including: building, testing, and release + scripts, static site generation, CI and other automated processes, and + domain registrar and cloud computing services. + + 0. **Transfer of Assets** + + The majority of Rocket's assets, including its domain, website, source + code, and associated infrastructure, are managed under personal accounts. + All assets are being transferred to foundation-owned accounts, and access + will be given to the appropriate teams. The [migration project on GitHub] + is tracking the progress of asset migration. + + 0. **Process Documentation** + + Some of Rocket's core processes, including releases and site building, are + generally inaccessible to others. These will be documented, and access will + be granted to the appropriate teams. + + 0. **Open Planning & Development** + + While Rocket's development has generally been done in the open through + GitHub issues, PRs, and projects, little has been done to publicize those + efforts. Furthermore, _planning_ has largely been a closed process. Moving + forward, planning will be done in the open, and the community team will be + engaged to publicize development efforts and progress. + +[GitHub Sponsors]: https://github.com/sponsors/rwf2 +[Open Collective]: https://opencollective.com/rwf2 +[Matrix channel]: https://chat.mozilla.org/#/room/#rocket:mozilla.org +[migration project on GitHub]: https://github.com/orgs/rwf2/projects/1 + +## What's Coming Soon + + * **ยตGrants** + + The ยตGrant specification is a work-in-progress. We simulatenously want to + encourage and financially incentivize high-quality contributions while not + disincentivizing existing contributors. This is a delicate balance, and we + want to take the time to get it right. To get involved, see the current + [draft proposal](https://github.com/rwf2/rwf2.org/blob/master/docs/micro-grants.md) and + share your thoughts in the [GitHub discussion](https://github.com/orgs/rwf2/discussions/8). + As soon as we have a specification that feels fair, the first ยตGrants will + be offered. + + * **Foundation Website** + + The [RWF2 website](https://rwf2.org) as it stands is a placeholder for a + more fully featured website. Besides articulating the foundation's mission + and goals, the RWF2's website will also serve as a source of truth for the + status of and means to engaging with ongoing projects, grants, and finances. + + * **Membership** + + While certainly premature at this point in time, a consideration for the + future comes in the form of foundation _membership_ whereby governance is + expanded to include foundation _members_. The [governance proposal + document](https://github.com/rwf2/rwf2.org/blob/master/docs/governance.md) + has one take on how this might work. Until such a proposal is accepted, + governance will follow the president + teams model articulated above. + +## How to Get Involved + +The RWF2 represents a conscious effort to transfer control of Rocket from an +individual (me) to the community (you). Without your involvement, the RWF2 +ceases to exist. If you're excited about Rocket or the foundation, or simply +want to see Rocket continue to exist and flourish, please get involved. + + * **Join the Discussion** + + Communicate with us via the [Matrix channel], via [GitHub + discussions](https://github.com/orgs/rwf2/discussions), or via email at + [foundation@rwf2.org](mailto:foundation@rwf2.org). The foundation bring-up + itself is designed to be collaborative, and any input you have is + invaluable. + + * **Make a Contribution** + + Any easy way to get involved is to financially contribute. You can sponsor + the RWF2 through [GitHub Sponsors] or [Open Collective]. If your company + uses Rocket, encourage it to sponsor the project through the foundation. + + * **Become a Team Lead** + + If you're interested in leading or learning more about any one of the + *Maintenance*, *Release*, *Knowledge*, *Community*, or *Infrastructure* + teams, or think another team should exist, please get in touch via the + [Matrix channel] or via email at [foundation@rwf2.org](mailto:foundation@rwf2.org). + +I'm excited for this next step in Rocket's history, and I hope you'll join me in +making it a success. diff --git a/site/news/2023-11-17-version-0.5.md b/site/news/2023-11-17-version-0.5.md new file mode 100644 index 0000000000..e3a81063b0 --- /dev/null +++ b/site/news/2023-11-17-version-0.5.md @@ -0,0 +1,622 @@ +# Rocket v0.5: Stable, Async, Sentinels, Streams, SSE, Forms, WebSockets, & So Much More + + + +Four years, four release candidates, a thousand commits, and over a thousand +issues, discussions, and PRs later, I am ~~relieved~~ thrilled to announce the +general availability of Rocket v0.5. + +> **Rocket** is an async backend web framework for Rust with a focus on +> usability, security, extensibility, and speed. Rocket makes it simple to write +> secure web applications without sacrificing productivity or performance. + +We encourage all users to upgrade. For a guided migration from Rocket v0.4 to +Rocket v0.5, please consult the newly available [upgrading guide]. Rocket v0.4 +will continue to be supported and receive security updates until the time Rocket +v0.6 is released. + +! note: This is a co-announcement [along with the prelaunch] of [RWF2]. + + We're addressing the community's concerns regarding the pace of Rocket's + development, leadership, and release cadence in a separate announcement. + Please see the accompanying [RWF2 prelaunch announcement](../2023-11-17-rwf2-prelaunch/) + to learn more and see how you can get involved. + +[RWF2]: https://rwf2.org +[along with the prelaunch]: ../2023-11-17-rwf2-prelaunch/ +[upgrading guide]: ../../guide/upgrading + +## What's New? + +Almost every bit has been reevaluated with a focus on usability and developer +productivity, security, and consistency across the library and [broader +ecosystem]. The changes are numerous, so we focus on the most noteworthy changes +here and encourage everyone to read the [CHANGELOG] for a complete list. For +answers to frequently asked questions, see the new [FAQ]. + +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0/CHANGELOG.md +[broader ecosystem]: ../../guide/faq/#releases +[FAQ]: ../../guide/faq + +### โš“ Support for Stable `rustc` since `rc.1` + +Rocket v0.5 compiles and builds on Rust stable. You can now compile and build +Rocket applications with `rustc` from the stable release channel and remove all +`#![feature(..)]` crate attributes. The complete canonical example with a single +`hello` route becomes: + +```rust +#[macro_use] extern crate rocket; + +#[get("//")] +fn hello(name: &str, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) +} + +#[launch] +fn rocket() -> _ { + rocket::build().mount("/hello", routes![hello]) +} +``` + +
+ See a diff of the changes from v0.4. + +```diff +- #![feature(proc_macro_hygiene, decl_macro)] +- + #[macro_use] extern crate rocket; + + #[get("//")] +- fn hello(name: String, age: u8) -> String { ++ fn hello(name: &str, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) +} + +- fn main() { +- rocket::ignite().mount("/hello", routes![hello]).launch(); +- } ++ #[launch] ++ fn rocket() -> _ { ++ rocket::build().mount("/hello", routes![hello]) ++ } +``` + +
+ +Note the new [`launch`] attribute, which simplifies starting an `async` runtime +for Rocket applications. See the [migration guide] for more on transitioning to +a stable toolchain. + +[`launch`]: @api/rocket/attr.launch.html + +### ๐Ÿ“ฅ Async I/O since `rc.1` + +Rocket's core request handling was rebuilt in v0.5 to take advantage of the +latest `async` networking facilities in Rust. Backed by `tokio`, Rocket +automatically multiplexes request handling across `async` tasks on all of the +available cores on the machine. As a result, route handlers can now be declared +`async` and make use of `await` syntax: + +```rust +use rocket::tokio; +use rocket::data::{Data, ToByteUnit}; + +#[post("/debug", data = "")] +async fn debug(data: Data<'_>) -> std::io::Result<()> { + // Stream at most 512KiB all of the body data to stdout. + data.open(512.kibibytes()) + .stream_to(tokio::io::stdout()) + .await?; + + Ok(()) +} +``` + +See the [Blocking I/O](@guide/upgrading#blocking-io) section of the upgrading +guide for complete details on the `async` I/O transition. + +### ๐Ÿ’‚ Sentinels since `rc.1` + +Rocket v0.5 introduces [sentinels]. Entirely unique to Rocket, sentinels offer +an automatic last line of defense against runtime errors by enabling any type +that appears in a route to abort application launch under invalid conditions. +For example, the [`&State`] guard in v0.5 is a [`Sentinel`] that aborts +launch if the type `T` is not in managed state, thus preventing associated +runtime errors. + +[`Sentinel`]s can be implemented outside of Rocket, too, and you should seek to +do so whenever possible. For instance, the [`Template`] type from +[`rocket_dyn_templates`] is a sentinel that ensures templates are properly +registered. As another example, consider a `MyResponder` that expects: + + * A specific type `T` to be in managed state. + * An catcher to be registered for the `400` status code. + +Making `MyResponder` a sentinel that guards against these conditions is as +simple as: + +```rust +use rocket::{Rocket, Ignite, Sentinel}; +# struct MyResponder; +# struct T; + +impl Sentinel for MyResponder { + fn abort(r: &Rocket) -> bool { + r.state::().is_none() || !r.catchers().any(|c| c.code == Some(400)) + } +} +``` + +[sentinels]: @api/rocket/trait.Sentinel.html +[`Sentinel`]: @api/rocket/trait.Sentinel.html +[`&State`]: @api/rocket/struct.State.html +[`Template`]: @api/rocket_dyn_templates/struct.Template.html +[`rocket_dyn_templates`]: @api/rocket_dyn_templates/index.html + +### โ˜„๏ธ Streams and SSE since `rc.1` + +Powered by the new asynchronous core, Rocket v0.5 introduces real-time, typed +`async` [streams]. The new [async streams] section of the guide contains further +details, and we encourage all interested parties to see the new real-time, +multi-room [chat example]. + +As a taste of what's possible, the following `stream` route emits a `"pong"` +Server-Sent Event every `n` seconds, defaulting to `1`: + +```rust +# use rocket::*; +use rocket::tokio::time::{interval, Duration}; +use rocket::response::stream::{Event, EventStream};; + +#[get("/ping?")] +fn stream(n: Option) -> EventStream![] { + EventStream! { + let mut timer = interval(Duration::from_secs(n.unwrap_or(1))); + loop { + yield Event::data("pong"); + timer.tick().await; + } + } +} +``` + +[streams]: @api/rocket/response/stream/index.html +[async streams]: @guide/responses/#async-streams +[chat example]: @example/chat + +### ๐Ÿ”Œ WebSockets since `rc.4` + +Rocket v0.5 introduces support for HTTP connection upgrades via a new [upgrade +API]. The API allows responders to assume control of raw I/O with the client in +an existing HTTP connection, thus allowing HTTP connections to be _upgraded_ to +any protocol, including WebSockets! + +The newly introduced [`rocket_ws`] library takes advantage of the new API to +implement first-class support for WebSockets entirely outside of Rocket's core. +Working with `rocket_ws` to implement an echo server looks like this: + +```rust +# use rocket::get; +use rocket_ws::{WebSocket, Stream}; + +#[get("/echo")] +fn echo_compose(ws: WebSocket) -> Stream!['static] { + ws.stream(|io| io) +} +``` + +Just like the newly introduced `async` streams, `rocket_ws` also supports using +generator syntax for WebSocket messages: + +```rust +# use rocket::get; +use rocket_ws::{WebSocket, Stream}; + +#[get("/echo")] +fn echo_stream(ws: WebSocket) -> Stream!['static] { + Stream! { ws => + for await message in ws { + yield message?; + } + } +} +``` + +For complete usage details, see the [`rocket_ws`] documentation. + +[upgrade API]: @api/rocket/response/struct.Response.html#upgrading +[`rocket_ws`]: @api/rocket_ws + +### ๐Ÿ“ Comprehensive Forms since `rc.1` + +Rocket v0.5 entirely revamps [forms] with support for [multipart uploads], +[arbitrary collections] with [arbitrary nesting], [ad-hoc validation], and an +improved [`FromForm` derive], obviating the need for nearly all custom +implementations of `FromForm` or `FromFormField`. Rocket's new wire protocol for +forms allows applications to express _any structure_ with _any level of nesting +and collection_ without any custom code, eclipsing what's offered by other web +frameworks. + +As an illustrative example, consider the following structures: + +```rust +use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm<'r> { + owner: Person<'r>, + pet: Pet<'r>, +} + +#[derive(FromForm)] +struct Person<'r> { + name: &'r str +} + +#[derive(FromForm)] +struct Pet<'r> { + name: &'r str, + #[field(validate = eq(true))] + good_pet: bool, +} +``` + +To parse request data into a `MyForm`, a form with fields of `owner.name`, +`pet.name`, and `pet.good_pet` must be submitted. The ad-hoc validation on +`good_pet` validates that `good_pet` parses as `true`. Such a form, URL-encoded, +may look like: + +```rust,ignore +"owner.name=Bob&pet.name=Sally&pet.good_pet=yes" +``` + +Rocket's derived `FromForm` implementation for `MyForm` will automatically parse +such a submission into the correct value: + +```rust,ignore +MyForm { + owner: Person { + name: "Bob".into() + }, + pet: Pet { + name: "Sally".into(), + good_pet: true, + } +} +# }; +``` + +The rewritten [forms guide] provides complete details on revamped forms support. + +[forms guide]: @guide/requests/#forms +[ad-hoc validation]: @guide/requests#ad-hoc-validation +[arbitrary nesting]: @guide/requests#nesting +[multipart uploads]: @guide/requests#multipart +[forms]: @guide/requests#forms +[`FromFormField`]: @api/rocket/form/trait.FromFormField.html +[arbitrary collections]: @guide/requests#collections +[`FromForm` derive]: @api/rocket/derive.FromForm.html + +### ๐Ÿš€ And so much more! + +Rocket v0.5 introduces over **40** new features and major improvements! We +encourage everyone to review the [CHANGELOG] to learn about them all. Here are a +few more we don't want you to miss: + + * An automatically enabled [`Shield`]: security and privacy headers for all responses. + * [Graceful shutdown] with configurable grace periods, [notification], and [shutdown fairings]. + * An entirely new, flexible and robust [configuration system] based on [Figment]. + * Type-system enforced [incoming data limits] to mitigate memory-based DoS attacks. + * Support for [mutual TLS] and client [`Certificate`]s. + * Asynchronous database pooling support via [`rocket_db_pools`]. + * Compile-time URI literals via a fully revamped [`uri!`] macro. + +[`Shield`]: @api/rocket/shield/struct.Shield.html +[graceful shutdown]: @api/rocket/config/struct.Shutdown.html#summary +[notification]: @api/rocket/struct.Shutdown.html +[shutdown fairings]: @api/rocket/fairing/trait.Fairing.html#shutdown +[configuration system]: @guide/configuration/#configuration +[Figment]: https://docs.rs/figment/ +[incoming data limits]: @guide/requests/#streaming +[mutual TLS]: @guide/configuration/#mutual-tls +[`uri!`]: @api/rocket/macro.uri.html +[`rocket_db_pools`]: @api/rocket_db_pools/index.html +[`Certificate`]: @api/rocket/mtls/struct.Certificate.html +[migration guide]: ../../guide/upgrading + +## What's Next? + +We think Rocket provides the most productive and confidence-inspiring web +development experience in Rust today, but as always, there's room for +improvement. To that end, here's what's on the docket for the next major +release: + + 0. **Migration to RWF2** + + Discussed further in the [RWF2 prelaunch announcement], Rocket will + transition to being managed by the newly formed Rocket Web Framework + Foundation: _RWF2_. The net effect is increased development transparency, + including public roadmaps and periodic updates, financial support for + high-quality contributions, and codified pathways into the project's + governance. + + 0. **Pluggable Connection Listeners** + + Rocket currently expects and enjoins connection origination via + TCP/IP. While sufficient for the common case, it excludes other desirable + interfaces such as Unix Domain Sockets (UDS). + + In the next major release, Rocket will expose [an interface for implementing + and plugging-in custom connection listeners]. Rocket itself will make use + of this interface to expose more common mediums out-of-the-box, such as the + aforementioned UDS. + + 0. **Native `async` Traits** + + Given the [stabilization of `async fn` in traits], the next major release + will seek to eliminate Rocket's dependence on `#[async_trait]` opting instead + for native `async` traits. This will greatly improve our documentation, which + currently calls out the attribute for each affected trait, as well as offer + modest performance improvements. + + 0. [**Typed Catchers**](https://github.com/SergioBenitez/Rocket/issues/749) + + Today's catchers cannot receive strictly typed error data. This results + in workarounds where error data is queried for well-typedness at runtime. + While it _has_ been possible to implement a form of typed error catching + prior, doing so necessitated limiting error data to `'static` values, as + other Rust web frameworks do, a concession we're unwilling to make. + + After much experimentation, we have an approach that is ergonomic to use, + safe, and correct, all without the `'static` limitation. This will allow error + catchers to "pattern match" against error types at compile-time. At runtime, + Rocket will match emerging error types against the declared catchers and + call the appropriate catcher with the fully-typed value. + + 0. **Short-Circuitable Request Processing** + + Whether with success or failure, fairings and guards cannot presently + terminate request processing early. The rationale for forbidding this + functionality was that it would allow third-party crates and plugins to + dictate responses without offering any recourse to the top-level application. + + With the advent of typed catchers, however, we now have a mechanism by which + a top-level application can intercept early responses via their type, + resolving the prior concern. As such, in the next major release, fairings and + guards will be able to respond to requests early, and catchers will be able to + intercept those early responses at will. + + 0. **Associated Resources** + + Often a set of routes will share a set requirements. For example, they + may share a URI prefix, subset of guards, and some managed state. In today's + Rocket, these common requirements must be repeatedly specified for each route. + While this is by design (we _want_ a route's requirements to be obvious), the + repetition is arduous and potentially error prone. + + In an upcoming major release, Rocket will introduce new mechanisms by which + a set of routes can share an explicitly declared set of requirements. Their + _explicit_ and _declarative_ nature results in requirements that are + simultaneously obvious _and_ declared once. + + We're really excited about this upcoming change and will be announcing more + in the near future. + + 0. **Performance Improvements** + + Rocket appears to lag behind other Rust web frameworks in benchmarks. This is + partly due to [poor benchmarking], partly due to security-minded design + decisions, and partially due to unexploited opportunities. In the next + release, we'll be addressing the latter points. Specifically: + + - _Explore making work stealing optional._ + + Rocket currently defaults to using tokio's multithreaded, work-stealing + scheduler. This avoids tail latency issues when faced with irregular and + heterogeneous tasks at the expense of throughput due to higher bookkeeping costs + associated with work stealing. Other Rust web frameworks instead opt to use + tokio's single-threaded scheduler, which while theoretically suboptimal, + may yield better performance results in practice, especially when + benchmarking homogeneous workloads. + + While we believe work-stealing schedulers are the right choice for the + majority of applications desireing robust performance characteristics, we also + believe the choice should be the user's. We'll seek to make this choice + easier in the next release. + + - _Reduce conversions from external to internal HTTP types._ + + Rocket revalidates and sometimes copies incoming HTTP request data. + In Rocket v0.5, we began transitioning to a model where we revalidate + security insensitive data in debug mode only, allowing for bugs to be + caught and reported while reducing performance impacts in production. In + the next release, we seek to extend this approach. + +[an interface for implementing and plugging-in custom connection listeners]: +https://github.com/SergioBenitez/Rocket/issues/1070#issuecomment-1491101952 +[stabilization of `async fn` in traits]: https://github.com/rust-lang/rust/pull/115822 +[poor benchmarking]: @guide/faq/#performance + + + +## โค๏ธ Thank You + +A very special thank you to [Jeb Rosen], Rocket's maintainer from v0.4 to +v0.5-rc.1, without whom Rocket v0.5 wouldn't exist. Jeb is responsible for +leading the migration to `async` and Rust stable along with tireless efforts to +improve Rocket's documentation and address the community. Rocket is better for +having had Jeb along for the ride. Thank you, Jeb. + +[Jeb Rosen]: https://github.com/SergioBenitez/Rocket/commits?author=jebrosen + +A special thank you to all of Rocket's users, especially those who diligently +waded through all four release candidates, raised issues, and participated on +[GitHub] and the [Matrix channel]. You all are an awesome, kind, and thoughtful +bunch. Thank you. + +A heartfelt _thank you_ as well to _all_ **148** who contributed to Rocket v0.5: + +
    +
  • Aaron Leopold
  • +
  • Abdullah Alyan
  • +
  • Aditya
  • +
  • Alex Macleod
  • +
  • Alex Sears
  • +
  • Alexander van Ratingen
  • +
  • ami-GS
  • +
  • Antoine Martin
  • +
  • arctic-alpaca
  • +
  • arlecchino
  • +
  • Arthur Woimbรฉe
  • +
  • atouchet
  • +
  • Aurora
  • +
  • badoken
  • +
  • Beep LIN
  • +
  • Ben Sully
  • +
  • Benedikt Weber
  • +
  • Benjamin B
  • +
  • BlackDex
  • +
  • Bonex
  • +
  • Brenden Matthews
  • +
  • Brendon Federko
  • +
  • Brett Buford
  • +
  • Cedric Hutchings
  • +
  • Cezar Halmagean
  • +
  • Charles-Axel Dein
  • +
  • Compro Prasad
  • +
  • cui fliter
  • +
  • Daniel Wiesenberg
  • +
  • David Venhoek
  • +
  • Dimitri Sabadie
  • +
  • Dinu Blanovschi
  • +
  • Dominik Boehi
  • +
  • Doni Rubiagatra
  • +
  • Edgar Onghena
  • +
  • Edwin Svensson
  • +
  • est31
  • +
  • Felix Suominen
  • +
  • Fenhl
  • +
  • Filip Gospodinov
  • +
  • Flying-Toast
  • +
  • Follpvosten
  • +
  • Francois Stephany
  • +
  • Gabriel Fontes
  • +
  • gcarq
  • +
  • George Cheng
  • +
  • Giles Cope
  • +
  • Gonรงalo Ribeiro
  • +
  • hiyoko3m
  • +
  • Howard Su
  • +
  • hpodhaisky
  • +
  • Ian Jackson
  • +
  • IFcoltransG
  • +
  • Indosaram
  • +
  • inyourface34456
  • +
  • J. Cohen
  • +
  • Jacob Pratt
  • +
  • Jacob Sharf
  • +
  • Jacob Simpson
  • +
  • Jakub Dฤ…bek
  • +
  • Jakub Wieczorek
  • +
  • James Tai
  • +
  • Jason Hinch
  • +
  • Jeb Rosen
  • +
  • Jeremy Kaplan
  • +
  • Jieyou Xu
  • +
  • Joakim Soderlund
  • +
  • Johannes Liebermann
  • +
  • John-John Tedro
  • +
  • Jonah Brรผchert
  • +
  • Jonas Mรธller
  • +
  • Jonathan Dickinson
  • +
  • Jonty
  • +
  • Joscha
  • +
  • Joshua Nitschke
  • +
  • JR Heard
  • +
  • Juhasz Sandor
  • +
  • Julian Bรผttner
  • +
  • Juraj Fiala
  • +
  • Kenneth Allen
  • +
  • Kevin Wang
  • +
  • Kian-Meng Ang
  • +
  • Konrad Borowski
  • +
  • Leonora Tindall
  • +
  • Lev Kokotov
  • +
  • lewis
  • +
  • Lionel G
  • +
  • Lucille Blumire
  • +
  • Mai-Lapyst
  • +
  • Manuel
  • +
  • Manuel Transfeld
  • +
  • Marc Schreiber
  • +
  • Marc-Stefan Cassola
  • +
  • Marshall Bowers
  • +
  • Martin1887
  • +
  • Martinez
  • +
  • Matthew Pomes
  • +
  • Maxime Guerreiro
  • +
  • meltinglava
  • +
  • Michael Howell
  • +
  • Mikail Bagishov
  • +
  • mixio
  • +
  • multisn8
  • +
  • Necmettin Karakaya
  • +
  • Ning Sun
  • +
  • Nya
  • +
  • Paolo Barbolini
  • +
  • Paul Smith
  • +
  • Paul van Tilburg
  • +
  • Paul Weaver
  • +
  • pennae
  • +
  • Petr Portnov
  • +
  • philipp
  • +
  • Pieter Frenssen
  • +
  • PROgrm_JARvis
  • +
  • Razican
  • +
  • Redrield
  • +
  • Riley Patterson
  • +
  • Rodolphe Brรฉard
  • +
  • Roger Mo
  • +
  • RotesWasser
  • +
  • rotoclone
  • +
  • Ruben Schmidmeister
  • +
  • Rudi Floren
  • +
  • Rรฉmi Lauzier
  • +
  • Samuele Esposito
  • +
  • Scott McMurray
  • +
  • Sergio Benitez
  • +
  • Silas Sewell
  • +
  • Soham Roy
  • +
  • Steven Murdoch
  • +
  • Stuart Hinson
  • +
  • Thibaud Martinez
  • +
  • Thomas Eckert
  • +
  • ThouCheese
  • +
  • Tilen Pintariฤ
  • +
  • timando
  • +
  • timokoesters
  • +
  • toshokan
  • +
  • TotalKrill
  • +
  • Unpublished
  • +
  • Vasili
  • +
  • Vladimir Ignatev
  • +
  • Wesley Norris
  • +
  • xelivous
  • +
  • YetAnotherMinion
  • +
  • Yohannes Kifle
  • +
  • Yusuke Kominami
  • +
+ +[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[Matrix channel]: https://chat.mozilla.org/#/room/#rocket:mozilla.org + +## Get Involved + +Looking to help with Rocket? To contribute code, head over to [GitHub]. To get +involved with the project, see the [RWF2 prelaunch announcement]. We'd love to have you. + +[GitHub]: https://github.com/SergioBenitez/Rocket +[RWF2 prelaunch announcement]: ../2023-11-17-rwf2-prelaunch/ diff --git a/site/news/index.toml b/site/news/index.toml index 3796240308..c4d8fe2629 100644 --- a/site/news/index.toml +++ b/site/news/index.toml @@ -1,17 +1,30 @@ [[articles]] title = """ -Rocket v0.5: Stable, Async, Featureful +Rocket v0.5: Stable, Async, Feature Packed """ -slug = "2023-05-26-version-0.5" +slug = "2023-11-17-version-0.5" author = "Sergio Benitez" author_url = "https://sergio.bz" -date = "May 26, 2023" +date = "Nov 17, 2023" snippet = """ -I am _elated_ to announce that Rocket v0.5 is generally available. A step +I am _elated_ to announce that Rocket v0.5 is now generally available. A step forward in every direction, it is **packed** with features and improvements that increase developer productivity, improve application security and robustness, -provide new opportunities for extensibility, and deliver a renewed degree of -toolchain stability. +provide new opportunities for extensibility, and afford toolchain stability. +""" + +[[articles]] +title = """ +Building a Better Foundation for Rocket's Future +""" +slug = "2023-11-17-rwf2-prelaunch" +author = "Sergio Benitez" +author_url = "https://sergio.bz" +date = "Nov 17, 2023" +snippet = """ +Today I'm excited to share plans to launch the Rocket Web Framework Foundation +(RWF2), a 501(c)(3) nonprofit and collective to support Rocket and the +surrounding ecosystem. """ [[articles]] From c6d7016146e1fe37a78568c159aa81af04af4adf Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 17 Nov 2023 17:49:58 +0100 Subject: [PATCH 034/178] New version: 0.5.0. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b49b57075e..2981353f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Version 0.5.0 (Nov XX, 2023) +# Version 0.5.0 (Nov 17, 2023) ## Major Features and Improvements From aabf856de4a35711f5c91ff7d25ae783275185b1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Nov 2023 10:44:17 +0100 Subject: [PATCH 035/178] Fix various typos in v0.5 news and guide. --- site/guide/01-upgrading.md | 22 +++++++++++----------- site/guide/12-faq.md | 14 ++++++++------ site/news/index.toml | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index f833aa7cfc..a6511c3f88 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -166,8 +166,8 @@ and deploy for production on the stable channel. ### Feature Attribute -As a result support for the stable release channel, Rocket applications no -longer need to enable any features to be used. You should **remove any +As a result of support for the stable release channel, Rocket applications no +longer need to enable any features to be used. You should **remove all `#[feature(..)]` crate attributes:** ```diff @@ -225,7 +225,7 @@ applications prior to v0.5, must take great care to convert all synchronous, blocking I/O, to `async` I/O. This is because, as the name implies, blocking I/O blocks a thread from making progress until the I/O result is available, meaning that no tasks can be scheduled on the waiting thread, wasting valuable resources -and significantly degrading performance. +and degrading performance. Common sources of blocking I/O and their `async` replacements include: @@ -330,7 +330,7 @@ All trait documentation has been updated to call out such traits with an example implementation that includes the invocation. The example implementation also serves as better documentation for trait and trait method signatures than the rustdocs. Because `async_trait` modifies these signatures, the rustdocs diverge -from what is written in source. For example, rustdoc renders: +from what is written in the source. For example, rustdoc renders: ```rust,ignore fn from_request<'life0, 'async_trait>( @@ -357,11 +357,11 @@ of truth for trait and method signatures. Rocket's configuration system has been entirely revamped for v0.5. The [configuration](../configuration) section of the guide contains a full walkthrough of the new system while the [general changes] section of the -CHANGELOG contains further details on configuration changes. We call out the +[CHANGELOG] contains further details on configuration changes. We call out the most important of these changes here. All users _must_: - * Replace `ROCKET_ENV` environment variable use with `ROCKET_PROFILE`. - * Replace `ROCKET_LOG` environment variable with `ROCKET_LOG_LEVEL`. + * Replace the `ROCKET_ENV` environment variable with `ROCKET_PROFILE`. + * Replace the `ROCKET_LOG` environment variable with `ROCKET_LOG_LEVEL`. * Use only IP addresses for the `address` configuration parameter. * Replace the `dev` or `development` profile with `debug`. * Note that the `stage`, `staging`, `prod`, and `production` profiles carry no @@ -445,7 +445,7 @@ Rocket v0.5 brings several major changes that affect routing: `FromParam`. 4. Query parameters parse with [`FromForm`] instead of `FromQuery` and support arbitrarily collections, nesting, structures, etc. - 5. All UTF-8 characters are allowed in static path components: `#[get("/โค๏ธ")]`. + 5. All UTF-8 characters are allowed in static path components: `#[get("/๐Ÿš€")]`. 6. The [`register()`] method require a path to [scope catchers] under. Using `"/"` emulates the previous behavior. @@ -538,8 +538,8 @@ corollary is three-fold: avoided. Most applications can simply swap uses of `&RawStr` and `String` for `&str` in -routes, forms, and so on to benefit from the increase web-safety and -performance. For instance, the front-page example becomes: +routes, forms, and so on to benefit from the increased safety and performance. +For instance, the front-page example becomes: ```diff #[get("//")] @@ -720,7 +720,7 @@ impl Sentinel for MyResponder { Rocket v0.5 brings a completely overhauled [`uri!()`] macro and support for typed URIs in more APIs. Notably, the `uri!()` macro now: - * Allows URIs to be constructed from static values: + * Allows URIs to be constructed from and as static values: ```rust # use rocket::uri; diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index 895678690c..9032c91391 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -272,13 +272,15 @@ Can I, and if so how, do I use WebSockets?
-Rocket doesn't support WebSockets quite yet. We're [working on it]. +You can! WebSocket support is provided by the officially maintained +[`rocket_ws`](@api/rocket_ws) crate. You'll find all the docs you need there. + +Rocket _also_ supports [Server-Sent Events], which allows for real-time +_unidirectional_ communication from the server to the client. The protocol is a +bit simpler, and you may find SSE sufficient for your use-case. For instance, +the [chat example] uses SSE to implement a real-time, multiroom chat +application. -That being said, Rocket _does_ support [Server-Sent Events], which allows for -real-time _unidirectional_ communication from the server to the client. This is -often sufficient for many of the applications that WebSockets are typically used -for. For instance, the [chat example] uses SSE to implement a real-time, -multiroom chat application.
diff --git a/site/news/index.toml b/site/news/index.toml index c4d8fe2629..b1db1704d1 100644 --- a/site/news/index.toml +++ b/site/news/index.toml @@ -1,6 +1,6 @@ [[articles]] title = """ -Rocket v0.5: Stable, Async, Feature Packed +Rocket v0.5: Stable, Async, Feature-Packed """ slug = "2023-11-17-version-0.5" author = "Sergio Benitez" From 44ac3f1eb6daf7000b5fc0e5f2ad9976380d7187 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Nov 2023 11:30:50 +0100 Subject: [PATCH 036/178] Move to 0.6.0-dev on master. --- contrib/db_pools/README.md | 12 ++++++------ contrib/db_pools/lib/Cargo.toml | 2 +- contrib/db_pools/lib/src/diesel.rs | 2 +- contrib/dyn_templates/Cargo.toml | 4 ++-- contrib/dyn_templates/README.md | 2 +- contrib/dyn_templates/src/lib.rs | 4 ++-- contrib/sync_db_pools/README.md | 2 +- contrib/sync_db_pools/lib/Cargo.toml | 4 ++-- contrib/sync_db_pools/lib/src/lib.rs | 4 ++-- contrib/ws/Cargo.toml | 4 ++-- contrib/ws/README.md | 2 +- contrib/ws/src/lib.rs | 2 +- core/codegen/Cargo.toml | 6 +++--- core/codegen/src/lib.rs | 6 +++--- core/http/Cargo.toml | 4 ++-- core/lib/Cargo.toml | 8 ++++---- core/lib/src/config/config.rs | 2 +- core/lib/src/config/mod.rs | 2 +- core/lib/src/config/secret_key.rs | 4 ++-- core/lib/src/fairing/ad_hoc.rs | 2 +- core/lib/src/fairing/mod.rs | 2 +- core/lib/src/form/context.rs | 2 +- core/lib/src/form/form.rs | 2 +- core/lib/src/form/from_form.rs | 2 +- core/lib/src/form/mod.rs | 2 +- core/lib/src/lib.rs | 26 +++++++++++++------------- core/lib/src/local/client.rs | 2 +- core/lib/src/local/mod.rs | 2 +- core/lib/src/mtls.rs | 2 +- core/lib/src/request/from_request.rs | 2 +- core/lib/src/serde/json.rs | 2 +- core/lib/src/serde/msgpack.rs | 2 +- core/lib/src/serde/uuid.rs | 2 +- core/lib/src/shield/mod.rs | 2 +- scripts/config.sh | 2 +- site/README.md | 2 +- site/guide/1-quickstart.md | 2 +- site/guide/10-pastebin-tutorial.md | 2 +- site/guide/12-faq.md | 2 +- site/guide/2-getting-started.md | 2 +- site/guide/3-overview.md | 4 ++-- site/guide/4-requests.md | 4 ++-- site/guide/9-configuration.md | 4 ++-- site/guide/index.md | 2 +- site/tests/Cargo.toml | 2 +- 45 files changed, 79 insertions(+), 79 deletions(-) diff --git a/contrib/db_pools/README.md b/contrib/db_pools/README.md index 6ccaa1ff5c..c06b03b96f 100644 --- a/contrib/db_pools/README.md +++ b/contrib/db_pools/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_db_pools.svg [crate]: https://crates.io/crates/rocket_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5/rocket_db_pools +[crate docs]: https://api.rocket.rs/master/rocket_db_pools [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions @@ -60,8 +60,8 @@ full usage details. } ``` -[database driver features]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#supported-drivers -[`Pool`]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#supported-drivers -[Configure]: https://api.rocket.rs/v0.5/rocket_db_pools/index.html#configuration -[Derive `Database`]: https://api.rocket.rs/v0.5/rocket_db_pools/derive.Database.html -[`Connection`]: https://api.rocket.rs/v0.5/rocket_db_pools/struct.Connection.html +[database driver features]: https://api.rocket.rs/master/rocket_db_pools/index.html#supported-drivers +[`Pool`]: https://api.rocket.rs/master/rocket_db_pools/index.html#supported-drivers +[Configure]: https://api.rocket.rs/master/rocket_db_pools/index.html#configuration +[Derive `Database`]: https://api.rocket.rs/master/rocket_db_pools/derive.Database.html +[`Connection`]: https://api.rocket.rs/master/rocket_db_pools/struct.Connection.html diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 0d7284cf14..e837bf7f0d 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -29,7 +29,7 @@ diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpo [dependencies.rocket] path = "../../../core/lib" -version = "0.5.0" +version = "0.6.0-dev" default-features = false [dependencies.rocket_db_pools_codegen] diff --git a/contrib/db_pools/lib/src/diesel.rs b/contrib/db_pools/lib/src/diesel.rs index 911ba776b7..c7229eab43 100644 --- a/contrib/db_pools/lib/src/diesel.rs +++ b/contrib/db_pools/lib/src/diesel.rs @@ -8,7 +8,7 @@ //! //! ```toml //! [dependencies] -//! rocket = "0.5.0" +//! rocket = "0.6.0-dev" //! diesel = "2" //! //! [dependencies.rocket_db_pools] diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 92e61f8d82..f6cca21256 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_dyn_templates" version = "0.1.0" authors = ["Sergio Benitez "] description = "Dynamic templating engine integration for Rocket." -documentation = "https://api.rocket.rs/v0.5/rocket_dyn_templates/" +documentation = "https://api.rocket.rs/master/rocket_dyn_templates/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/dyn_templates" readme = "README.md" @@ -22,7 +22,7 @@ notify = "6" normpath = "1" [dependencies.rocket] -version = "0.5.0" +version = "0.6.0-dev" path = "../../core/lib" default-features = false diff --git a/contrib/dyn_templates/README.md b/contrib/dyn_templates/README.md index fad858ce79..34a521e217 100644 --- a/contrib/dyn_templates/README.md +++ b/contrib/dyn_templates/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_dyn_templates.svg [crate]: https://crates.io/crates/rocket_dyn_templates [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5/rocket_dyn_templates +[crate docs]: https://api.rocket.rs/master/rocket_dyn_templates [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 2dfb474d46..488f802d1f 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -66,7 +66,7 @@ //! template directory is configured via the `template_dir` configuration //! parameter and defaults to `templates/`. The path set in `template_dir` is //! relative to the Rocket configuration file. See the [configuration -//! chapter](https://rocket.rs/v0.5/guide/configuration) of the guide for more +//! chapter](https://rocket.rs/master/guide/configuration) of the guide for more //! information on configuration. //! //! The corresponding templating engine used for a given template is based on a @@ -132,7 +132,7 @@ //! the templates directory since the previous request. In release builds, //! template reloading is disabled to improve performance and cannot be enabled. -#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_dyn_templates")] +#![doc(html_root_url = "https://api.rocket.rs/master/rocket_dyn_templates")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] diff --git a/contrib/sync_db_pools/README.md b/contrib/sync_db_pools/README.md index 04edf74da6..1a6301904c 100644 --- a/contrib/sync_db_pools/README.md +++ b/contrib/sync_db_pools/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_sync_db_pools.svg [crate]: https://crates.io/crates/rocket_sync_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5/rocket_sync_db_pools +[crate docs]: https://api.rocket.rs/master/rocket_sync_db_pools [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index 32854eab73..fd5fdd7526 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_sync_db_pools" version = "0.1.0" authors = ["Sergio Benitez "] description = "Rocket async database pooling support for sync database drivers." -repository = "https://github.com/SergioBenitez/Rocket/tree/v0.5/contrib/sync_db_pools" +repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/sync_db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" @@ -39,7 +39,7 @@ version = "0.1.0" path = "../codegen" [dependencies.rocket] -version = "0.5.0" +version = "0.6.0-dev" path = "../../../core/lib" default-features = false diff --git a/contrib/sync_db_pools/lib/src/lib.rs b/contrib/sync_db_pools/lib/src/lib.rs index 1d83073c12..6fafac0c43 100644 --- a/contrib/sync_db_pools/lib/src/lib.rs +++ b/contrib/sync_db_pools/lib/src/lib.rs @@ -166,7 +166,7 @@ //! Lastly, databases can be configured via environment variables by specifying //! the `databases` table as detailed in the [Environment Variables //! configuration -//! guide](https://rocket.rs/v0.5/guide/configuration/#environment-variables): +//! guide](https://rocket.rs/master/guide/configuration/#environment-variables): //! //! ```bash //! ROCKET_DATABASES='{my_db={url="db.sqlite"}}' @@ -349,7 +349,7 @@ //! [request guards]: rocket::request::FromRequest //! [`Poolable`]: crate::Poolable -#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_sync_db_pools")] +#![doc(html_root_url = "https://api.rocket.rs/master/rocket_sync_db_pools")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #![cfg_attr(nightly, feature(doc_cfg))] diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index 573427dffe..a6c5750c55 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_ws" version = "0.1.0" authors = ["Sergio Benitez "] description = "WebSocket support for Rocket." -documentation = "https://api.rocket.rs/v0.5/rocket_ws/" +documentation = "https://api.rocket.rs/master/rocket_ws/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/ws" readme = "README.md" @@ -20,7 +20,7 @@ tungstenite = ["tokio-tungstenite"] tokio-tungstenite = { version = "0.20", optional = true } [dependencies.rocket] -version = "0.5.0" +version = "0.6.0-dev" path = "../../core/lib" default-features = false diff --git a/contrib/ws/README.md b/contrib/ws/README.md index 27da59a4b5..e019659aea 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -3,7 +3,7 @@ [crates.io]: https://img.shields.io/crates/v/rocket_ws.svg [crate]: https://crates.io/crates/rocket_ws [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 -[crate docs]: https://api.rocket.rs/v0.5/rocket_ws +[crate docs]: https://api.rocket.rs/master/rocket_ws [ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg [ci]: https://github.com/SergioBenitez/Rocket/actions diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs index 4768a043fb..e3d63ef0af 100644 --- a/contrib/ws/src/lib.rs +++ b/contrib/ws/src/lib.rs @@ -70,7 +70,7 @@ //! } //! ``` -#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_ws")] +#![doc(html_root_url = "https://api.rocket.rs/master/rocket_ws")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index d11974c1ce..7cf65156bb 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "rocket_codegen" -version = "0.5.0" +version = "0.6.0-dev" authors = ["Sergio Benitez "] description = "Procedural macros for the Rocket web framework." -documentation = "https://api.rocket.rs/v0.5/rocket_codegen/" +documentation = "https://api.rocket.rs/master/rocket_codegen/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" @@ -21,7 +21,7 @@ quote = "1.0" syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits"] } proc-macro2 = "1.0.27" devise = "0.4" -rocket_http = { version = "0.5.0", path = "../http/" } +rocket_http = { version = "0.6.0-dev", path = "../http/" } unicode-xid = "0.2" version_check = "0.9" glob = "0.3" diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 1d22fe431a..c375351f1c 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1,6 +1,6 @@ #![recursion_limit="128"] -#![doc(html_root_url = "https://api.rocket.rs/v0.5")] +#![doc(html_root_url = "https://api.rocket.rs/master")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] @@ -11,7 +11,7 @@ //! This crate implements the code generation portions of Rocket. This includes //! custom derives, custom attributes, and procedural macros. The documentation //! here is purely technical. The code generation facilities are documented -//! thoroughly in the [Rocket programming guide](https://rocket.rs/v0.5/guide). +//! thoroughly in the [Rocket programming guide](https://rocket.rs/master/guide). //! //! # Usage //! @@ -21,7 +21,7 @@ //! //! ```toml //! [dependencies] -//! rocket = "0.5.0" +//! rocket = "0.6.0-dev" //! ``` //! //! And to import all macros, attributes, and derives via `#[macro_use]` in the diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index a5664ce4b8..10e5d2253f 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "rocket_http" -version = "0.5.0" +version = "0.6.0-dev" authors = ["Sergio Benitez "] description = """ Types, traits, and parsers for HTTP requests, responses, and headers. """ -documentation = "https://api.rocket.rs/v0.5/rocket_http/" +documentation = "https://api.rocket.rs/master/rocket_http/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 2203ee1adb..8a49cc603a 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "rocket" -version = "0.5.0" +version = "0.6.0-dev" authors = ["Sergio Benitez "] description = """ Web framework with a focus on usability, security, extensibility, and speed. """ -documentation = "https://api.rocket.rs/v0.5/rocket/" +documentation = "https://api.rocket.rs/master/rocket/" homepage = "https://rocket.rs" repository = "https://github.com/SergioBenitez/Rocket" readme = "../../README.md" @@ -61,11 +61,11 @@ tokio-stream = { version = "0.1.6", features = ["signal", "time"] } state = "0.6" [dependencies.rocket_codegen] -version = "0.5.0" +version = "0.6.0-dev" path = "../codegen" [dependencies.rocket_http] -version = "0.5.0" +version = "0.6.0-dev" path = "../http" features = ["serde"] diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 3856e22874..635f2801b6 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -23,7 +23,7 @@ use crate::config::SecretKey; /// See the [module level docs](crate::config) as well as the [configuration /// guide] for further details. /// -/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ +/// [configuration guide]: https://rocket.rs/master/guide/configuration/ /// /// # Defaults /// diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 6bd2de3ff2..7ea4c653c4 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -2,7 +2,7 @@ //! //! See the [configuration guide] for full details. //! -//! [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ +//! [configuration guide]: https://rocket.rs/master/guide/configuration/ //! //! ## Extracting Configuration Parameters //! diff --git a/core/lib/src/config/secret_key.rs b/core/lib/src/config/secret_key.rs index 8302c7f482..07d804a323 100644 --- a/core/lib/src/config/secret_key.rs +++ b/core/lib/src/config/secret_key.rs @@ -71,8 +71,8 @@ enum Kind { /// assert!(matches!(error.kind(), ErrorKind::InsecureSecretKey(profile))); /// ``` /// -/// [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies -/// [configuration guide]: https://rocket.rs/v0.5/guide/configuration/#secret-key +/// [private cookies]: https://rocket.rs/master/guide/requests/#private-cookies +/// [configuration guide]: https://rocket.rs/master/guide/configuration/#secret-key #[derive(Clone)] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub struct SecretKey { diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index b428d3070f..e689cdfaf7 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -310,7 +310,7 @@ impl AdHoc { /// let response = client.get("/bar").dispatch(); /// assert_eq!(response.into_string().unwrap(), "bar"); /// ``` - // #[deprecated(since = "0.6", note = "routing from Rocket v0.5 is now standard")] + // #[deprecated(since = "0.7", note = "routing from Rocket 0.6 is now standard")] pub fn uri_normalizer() -> impl Fairing { #[derive(Default)] struct Normalizer { diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs index be17959562..fec2b33f2b 100644 --- a/core/lib/src/fairing/mod.rs +++ b/core/lib/src/fairing/mod.rs @@ -423,7 +423,7 @@ pub type Result, E = Rocket> = std::result::Result { /// The value, if it was successfully parsed, or `None` otherwise. diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index cd80db4450..848d490d5a 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -12,7 +12,7 @@ use crate::form::prelude::*; /// This type implements the [`FromData`] trait. It provides a generic means to /// parse arbitrary structures from incoming form data. /// -/// See the [forms guide](https://rocket.rs/v0.5/guide/requests#forms) for +/// See the [forms guide](https://rocket.rs/master/guide/requests#forms) for /// general form support documentation. /// /// # Leniency diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs index 0f04803d14..4733469a10 100644 --- a/core/lib/src/form/from_form.rs +++ b/core/lib/src/form/from_form.rs @@ -65,7 +65,7 @@ use crate::http::uncased::AsUncased; /// [FromFormField]: crate::form::FromFormField /// [`shift()`ed]: NameView::shift() /// [`key()`]: NameView::key() -/// [forms guide]: https://rocket.rs/v0.5/guide/requests/#forms +/// [forms guide]: https://rocket.rs/master/guide/requests/#forms /// /// # Parsing Strategy /// diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 1d1c21b7ba..aa772a39a4 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -1,6 +1,6 @@ //! Parsing and validation of HTTP forms and fields. //! -//! See the [forms guide](https://rocket.rs/v0.5/guide/requests#forms) for +//! See the [forms guide](https://rocket.rs/master/guide/requests#forms) for //! general form support documentation. //! //! # Field Wire Format diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 33874a7fcc..232a981482 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -1,6 +1,6 @@ #![recursion_limit="256"] -#![doc(html_root_url = "https://api.rocket.rs/v0.5")] +#![doc(html_root_url = "https://api.rocket.rs/master")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #![cfg_attr(nightly, feature(doc_cfg))] @@ -18,10 +18,10 @@ //! detailed guide]. If you'd like pointers on getting started, see the //! [quickstart] or [getting started] chapters of the guide. //! -//! [overview]: https://rocket.rs/v0.5/overview -//! [full, detailed guide]: https://rocket.rs/v0.5/guide -//! [quickstart]: https://rocket.rs/v0.5/guide/quickstart -//! [getting started]: https://rocket.rs/v0.5/guide/getting-started +//! [overview]: https://rocket.rs/master/overview +//! [full, detailed guide]: https://rocket.rs/master/guide +//! [quickstart]: https://rocket.rs/master/guide/quickstart +//! [getting started]: https://rocket.rs/master/guide/getting-started //! //! ## Usage //! @@ -29,13 +29,13 @@ //! //! ```toml //! [dependencies] -//! rocket = "0.5.0" +//! rocket = "0.6.0-dev" //! ``` //! //! Note that development versions, tagged with `-dev`, are not published //! and need to be specified as [git dependencies]. //! -//! See the [guide](https://rocket.rs/v0.5/guide) for more information on how +//! See the [guide](https://rocket.rs/master/guide) for more information on how //! to write Rocket applications. Here's a simple example to get you started: //! //! [git dependencies]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories @@ -73,21 +73,21 @@ //! //! ```toml //! [dependencies] -//! rocket = { version = "0.5.0", features = ["secrets", "tls", "json"] } +//! rocket = { version = "0.6.0-dev", features = ["secrets", "tls", "json"] } //! ``` //! //! Conversely, HTTP/2 can be disabled: //! //! ```toml //! [dependencies] -//! rocket = { version = "0.5.0", default-features = false } +//! rocket = { version = "0.6.0-dev", default-features = false } //! ``` //! //! [JSON (de)serialization]: crate::serde::json //! [MessagePack (de)serialization]: crate::serde::msgpack //! [UUID value parsing and (de)serialization]: crate::serde::uuid -//! [private cookies]: https://rocket.rs/v0.5/guide/requests/#private-cookies -//! [TLS]: https://rocket.rs/v0.5/guide/configuration/#tls +//! [private cookies]: https://rocket.rs/master/guide/requests/#private-cookies +//! [TLS]: https://rocket.rs/master/guide/configuration/#tls //! [mutual TLS]: crate::mtls //! //! ## Configuration @@ -103,8 +103,8 @@ //! integration testing of a Rocket application. The top-level [`local`] module //! documentation and the [testing guide] include detailed examples. //! -//! [configuration guide]: https://rocket.rs/v0.5/guide/configuration/ -//! [testing guide]: https://rocket.rs/v0.5/guide/testing/#testing +//! [configuration guide]: https://rocket.rs/master/guide/configuration/ +//! [testing guide]: https://rocket.rs/master/guide/testing/#testing //! [Figment]: https://docs.rs/figment /// These are public dependencies! Update docs if these are changed, especially diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index 5c2f92ee06..48001f05d6 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -137,7 +137,7 @@ macro_rules! pub_client_impl { /// Deprecated alias to [`Client::tracked()`]. #[deprecated( - since = "0.5.0", + since = "0.6.0-dev", note = "choose between `Client::untracked()` and `Client::tracked()`" )] pub $($prefix)? fn new(rocket: Rocket

) -> Result { diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index 4a2bed5ec5..bed689a417 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -80,7 +80,7 @@ //! //! For more details on testing, see the [testing guide]. //! -//! [testing guide]: https://rocket.rs/v0.5/guide/testing/ +//! [testing guide]: https://rocket.rs/master/guide/testing/ //! [`Client`]: crate::local::asynchronous::Client //! //! # `Client` diff --git a/core/lib/src/mtls.rs b/core/lib/src/mtls.rs index 93e79945a9..441fffb6f7 100644 --- a/core/lib/src/mtls.rs +++ b/core/lib/src/mtls.rs @@ -2,7 +2,7 @@ //! //! For details on how to configure mutual TLS, see //! [`MutualTls`](crate::config::MutualTls) and the [TLS -//! guide](https://rocket.rs/v0.5/guide/configuration/#tls). See +//! guide](https://rocket.rs/master/guide/configuration/#tls). See //! [`Certificate`] for a request guard that validated, verifies, and retrieves //! client certificates. diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 1dacb4d2b5..0d1879ccb4 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -368,7 +368,7 @@ pub type Outcome = outcome::Outcome; /// Notice that these request guards provide access to *borrowed* data (`&'a /// User` and `Admin<'a>`) as the data is now owned by the request's cache. /// -/// [request-local state]: https://rocket.rs/v0.5/guide/state/#request-local-state +/// [request-local state]: https://rocket.rs/master/guide/state/#request-local-state #[crate::async_trait] pub trait FromRequest<'r>: Sized { /// The associated error to be returned if derivation fails. diff --git a/core/lib/src/serde/json.rs b/core/lib/src/serde/json.rs index 677b20e726..c46db1202c 100644 --- a/core/lib/src/serde/json.rs +++ b/core/lib/src/serde/json.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "0.5.0" +//! version = "0.6.0-dev" //! features = ["json"] //! ``` //! diff --git a/core/lib/src/serde/msgpack.rs b/core/lib/src/serde/msgpack.rs index 7af49700de..3360f998d8 100644 --- a/core/lib/src/serde/msgpack.rs +++ b/core/lib/src/serde/msgpack.rs @@ -9,7 +9,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "0.5.0" +//! version = "0.6.0-dev" //! features = ["msgpack"] //! ``` //! diff --git a/core/lib/src/serde/uuid.rs b/core/lib/src/serde/uuid.rs index f834932a98..48dbad24ab 100644 --- a/core/lib/src/serde/uuid.rs +++ b/core/lib/src/serde/uuid.rs @@ -7,7 +7,7 @@ //! //! ```toml //! [dependencies.rocket] -//! version = "0.5.0" +//! version = "0.6.0-dev" //! features = ["uuid"] //! ``` //! diff --git a/core/lib/src/shield/mod.rs b/core/lib/src/shield/mod.rs index b893c6c86c..060f243b96 100644 --- a/core/lib/src/shield/mod.rs +++ b/core/lib/src/shield/mod.rs @@ -4,7 +4,7 @@ //! security and privacy headers into all outgoing responses. It takes some //! inspiration from [helmetjs], a similar piece of middleware for [express]. //! -//! [fairing]: https://rocket.rs/v0.5/guide/fairings/ +//! [fairing]: https://rocket.rs/master/guide/fairings/ //! [helmetjs]: https://helmetjs.github.io/ //! [express]: https://expressjs.com //! diff --git a/scripts/config.sh b/scripts/config.sh index a9dc777a9a..42f4ec2957 100755 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -58,7 +58,7 @@ VERSION=$(git grep -h "^version" "${CORE_LIB_ROOT}" | head -n 1 | cut -d '"' -f2 MAJOR_VERSION=$(echo "${VERSION}" | cut -d'.' -f1-2) VIRTUAL_CODENAME="$(git branch --show-current)" PHYSICAL_CODENAME="v${MAJOR_VERSION}" -CURRENT_RELEASE=true +CURRENT_RELEASE=false PRE_RELEASE=false # A generated codename for this version. Use the git branch for pre-releases. diff --git a/site/README.md b/site/README.md index d56b05eaed..72bf794464 100644 --- a/site/README.md +++ b/site/README.md @@ -13,7 +13,7 @@ This directory contains the following: * `news/*.md` - News articles linked to from `news/index.toml`. * `guide/*.md` - Guide pages linked to from `guide.md`. -[Rocket Programming Guide]: https://rocket.rs/v0.5/guide/ +[Rocket Programming Guide]: https://rocket.rs/master/guide/ ### Guide Links diff --git a/site/guide/1-quickstart.md b/site/guide/1-quickstart.md index 51f3d5efa0..c9254a6def 100644 --- a/site/guide/1-quickstart.md +++ b/site/guide/1-quickstart.md @@ -14,7 +14,7 @@ For instance, the following set of commands runs the `hello` example: ```sh git clone https://github.com/SergioBenitez/Rocket cd Rocket -git checkout v0.5 +git checkout master cd examples/hello cargo run ``` diff --git a/site/guide/10-pastebin-tutorial.md b/site/guide/10-pastebin-tutorial.md index ac44d064ae..90da9e5c18 100644 --- a/site/guide/10-pastebin-tutorial.md +++ b/site/guide/10-pastebin-tutorial.md @@ -52,7 +52,7 @@ Then add the usual Rocket dependencies to the `Cargo.toml` file: ```toml [dependencies] -rocket = "0.5.0" +rocket = "0.6.0-dev" ``` And finally, create a skeleton Rocket application to work off of in diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index 9032c91391..48cde1e369 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -644,7 +644,7 @@ is to depend on a `contrib` library from git while also depending on a `crates.io` version of Rocket or vice-versa: ```toml -rocket = "0.5.0" +rocket = "0.6.0-dev" rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" } ``` diff --git a/site/guide/2-getting-started.md b/site/guide/2-getting-started.md index 0f485ca1a3..5232ecabe3 100644 --- a/site/guide/2-getting-started.md +++ b/site/guide/2-getting-started.md @@ -43,7 +43,7 @@ Now, add Rocket as a dependency in your `Cargo.toml`: ```toml [dependencies] -rocket = "0.5.0" +rocket = "0.6.0-dev" ``` ! warning: Development versions must be _git_ dependencies. diff --git a/site/guide/3-overview.md b/site/guide/3-overview.md index 499a75579a..9ac19941ce 100644 --- a/site/guide/3-overview.md +++ b/site/guide/3-overview.md @@ -260,8 +260,8 @@ You can find async-ready libraries on [crates.io](https://crates.io) with the ! note - Rocket v0.5 uses the tokio runtime. The runtime is started for you if you - use `#[launch]` or `#[rocket::main]`, but you can still `launch()` a Rocket + Rocket uses the tokio runtime. The runtime is started for you if you use + `#[launch]` or `#[rocket::main]`, but you can still `launch()` a Rocket instance on a custom-built runtime by not using _either_ attribute. ### Async Routes diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 8569ed7241..b6247a929f 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -606,7 +606,7 @@ feature: ```toml ## in Cargo.toml -rocket = { version = "0.5.0", features = ["secrets"] } +rocket = { version = "0.6.0-dev", features = ["secrets"] } ``` The API for retrieving, adding, and removing private cookies is identical except @@ -784,7 +784,7 @@ complete example. feature can be enabled in the `Cargo.toml`: ` - rocket = { version = "0.5.0", features = ["json"] } + rocket = { version = "0.6.0-dev", features = ["json"] } ` ### Temporary Files diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index b2f47c0004..230d345bac 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -237,7 +237,7 @@ Security). To enable TLS support: ```toml,ignore [dependencies] - rocket = { version = "0.5.0", features = ["tls"] } + rocket = { version = "0.6.0-dev", features = ["tls"] } ``` 2. Configure a TLS certificate chain and private key via the `tls.key` and @@ -302,7 +302,7 @@ enabled and support configured via the `tls.mutual` config parameter: ```toml,ignore [dependencies] - rocket = { version = "0.5.0", features = ["mtls"] } + rocket = { version = "0.6.0-dev", features = ["mtls"] } ``` This implicitly enables the `tls` feature. diff --git a/site/guide/index.md b/site/guide/index.md index 46dc9d6238..fe9f0d645b 100644 --- a/site/guide/index.md +++ b/site/guide/index.md @@ -2,7 +2,7 @@ Welcome to Rocket! -This is the official guide for Rocket v0.5. It is designed to serve as a +This is the official guide for Rocket master. It is designed to serve as a starting point to writing web applications with Rocket and Rust. The guide is also designed to be a reference for experienced Rocket developers. This guide is conversational in tone. For purely technical documentation with examples, see diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index 61d6ff50d3..7281464dbb 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_guide_tests" -version = "0.5.0" +version = "0.6.0-dev" workspace = "../../" edition = "2021" publish = false From 99849bd563637a7d535c4e55a8b73bf5169d5025 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Nov 2023 11:43:30 +0100 Subject: [PATCH 037/178] Add placeholder v0.5 to v0.6 upgrade guide. --- site/guide/01-upgrading.md | 852 +------------------------------------ site/guide/index.md | 2 +- 2 files changed, 2 insertions(+), 852 deletions(-) diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index a6511c3f88..5a037ac89b 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -1,856 +1,6 @@ # Upgrading -Rocket v0.5 bring many new features and improvements over Rocket v0.4. Rocket -v0.5 also includes many changes that improve the overall usability, stability, -and security of the framework and applications written in it. While the Rust -compiler can guide you through many of these changes, others require special -attention. The intent of this guide is to guide you through these changes and -more, migrating your Rocket application to 0.5 and reaping the benefits of new -features and improvements. - -This guide is _not_ intended to replace, but instead complement, a reading of -the [CHANGELOG]. The [CHANGELOG] should be considered required reading for all -developers wishing to migrate their applications to Rocket v0.5. - -[CHANGELOG]: @github/CHANGELOG.md - -! note Don't panic! - - Simply upgrading Rocket's version string to the `0.5` series will result in - _many_ `rustc` compiler errors. But don't let this faze you! The vast majority - of changes are simple renames and `#[async_trait]` attributions which manifest - in a cascading of errors. As such, resolving _one_ top-level issue, typically - requiring minimal, trivial changes, often resolves _many_ errors in one go. - -## Crate Organization - -Rocket v0.5 incorporates an improved module structure and crate ecosystem. -Modules and items that have been moved or removed will trigger a compiler error. -We encourage users to search through the [CHANGELOG] or [API docs](@api/rocket) -for the v0.5 analog. All previously existing functionality, except for that -incompatible with async I/O, is available in v0.5. - -### Off-by-Default Secrets - -The `private-cookies` crate feature, which was previously enabled by default, -has been renamed to `secrets` and is disabled by default. If you are using -private cookies, you _must_ enable the `secrets` feature in `Cargo.toml`: - -```toml -[dependencies] -rocket = { version = "0.5.0", features = ["secrets"] } -``` - -### Contrib Deprecation - -The `rocket_contrib` crate is deprecated and is wholly incompatible with Rocket -0.5. _All_ users of `rocket_contrib` _must_: - - * Remove all dependencies and references to `rocket_contrib`. - * For templating support, depend on the new [`rocket_dyn_templates`] crate. - * For database pools, depend on the new [`rocket_sync_db_pools`] and/or - [`rocket_db_pools`] crates. - * Enable [features in `rocket`] as necessary. - -For example, to make use of JSON and Tera templates, make the following changes -to `Cargo.toml`: - -```diff -[dependencies] -- rocket = "0.4" -- rocket_contrib = { version = "0.4", features = ["json"], default-features = false } -+ rocket = { version = "0.5.0", features = ["json"] } -+ rocket_dyn_templates = { version = "0.1.0", features = ["tera"] } -``` - -! note: `rocket_dyn_templates` (and co.) _does not_ follow in version lock-step -with the `rocket` crate. - - This is intentional. The crate depends on many external dependencies which may - evolve at a different pace than Rocket itself. Allowing their versions to - diverge enables keeping dependencies up-to-date without breaking `rocket` - itself. - -All features previously in `rocket_contrib` are available. Consult the [contrib -graduation] section of the CHANGELOG for full details. - -[`rocket_dyn_templates`]: @api/rocket_dyn_templates -[`rocket_sync_db_pools`]: @api/rocket_sync_db_pools -[`rocket_db_pools`]: @api/rocket_db_pools -[features in `rocket`]: @api/rocket/#features -[contrib graduation]: @github/CHANGELOG.md#contrib-graduation - -## Stable and Async Support - -Rocket v0.5 compiles and builds on Rust stable with an entirely asynchronous -core. You are encouraged to: - - * Switch to the Rust stable release channel for production builds. - * Remove the previously required `#![feature(..)]` crate attribute. - -All application authors _must_: - - * Use `rocket::build()` instead of `rocket::ignite()`. - * Use either the `#[launch]` or `#[rocket::main]` async entry attribute. - * Use `async` versions of any blocking I/O or execute it in another thread. - -Application authors _may_: - - * Prefer to explicitly import macros via `use` instead of `#[macro_use]`. - -The rest of the section describes making these changes in detail. - -### Stable Release Channel - -If you prefer to use Rust's stable release channel, you can switch to it using -`rustup`: - -```sh -## switch globally -rustup default stable - -## switch locally -rustup override set stable -``` - -Using the stable release channel ensures that _no_ breakages will occur when -upgrading your Rust compiler or Rocket. That being said, Rocket continues to -take advantage of features only present in the nightly channel. As a result, the -development experience will be superior on nightly for the foreseeable future. -For example, compiler diagnostics on `nightly` are more detailed and accurate: - -

-Example Diagnostic on Nightly - -```rust,ignore -error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:55:18 - | -55 | uri!(has_two(id = 100, cookies = "hi")); - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: uri parameters are: id: i32, name: String - = help: missing parameter: `name` -help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:55:28 - | -55 | uri!(has_two(id = 100, cookies = "hi")); - | ^^^^^^^ -``` - -
- -
-Example Diagnostic on Stable - -```rust,ignore -error: invalid parameters for `has_two` route uri - --- note: uri parameters are: id: i32, name: String - --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:55:18 - | -55 | uri!(has_two(id = 100, cookies = "hi")); - | ^^ - -error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:55:28 - | -55 | uri!(has_two(id = 100, cookies = "hi")); - | ^^^^^^^ -``` - -
- -Our **recommendation** is to develop locally on the nightly channel but build -and deploy for production on the stable channel. - -### Feature Attribute - -As a result of support for the stable release channel, Rocket applications no -longer need to enable any features to be used. You should **remove all -`#[feature(..)]` crate attributes:** - -```diff -- #![feature(proc_macro_hygiene, decl_macro)] -- - #[macro_use] extern crate rocket; - - fn main() { .. } -``` - -### Updates to Launch - -The new asynchronous core requires an async runtime to run. The new -[`launch`] and [`main`] attributes simplify starting a runtime suitable for -running Rocket applications. You should use [`launch`] whenever possible. - -Additionally, the `rocket::ignite()` function has been renamed to -[`rocket::build()`]; calls to the function or method should be replaced -accordingly. Together, these two changes result in the following diff to what -was previously the `main` function: - -```diff -- fn main() { -- rocket::ignite().mount("/hello", routes![hello]).launch(); -- } -+ #[launch] -+ fn rocket() -> _ { -+ rocket::build().mount("/hello", routes![hello]) -+ } -``` - -[`launch`]: @api/rocket/attr.launch.html -[`main`]: @api/rocket/attr.main.html -[`rocket::build()`]: @api/rocket/struct.Rocket.html#method.build - -### Blocking I/O - -Rocket v0.5 takes advantage of the latest developments in async I/O in Rust by -migrating to a fully asynchronous core powered by [`tokio`]. Specifically, -_every_ request is handled by an asynchronous task which internally calls one or -more request handlers. Asynchronous tasks are multiplexed on a [configurable -number of worker threads]. Though there is no limit to the number of tasks that -can run concurrently, at most `worker` tasks can run in parallel. - -The runtime can switch between tasks in a single worker thread _iff_ (_if -and only if_) an `await` point in reached. In other words, context -switching is _cooperative_, _not_ preemptive. This _iff_ is critical: if an -`await` point is _not_ reached, no task switching can occur. As such, it is -important that `await` points occur periodically in a task so that tasks waiting -to be scheduled are not starved. - -In general, when working with `async` APIs, await points occur naturally. -However, an application written for synchronous I/O, including all Rocket -applications prior to v0.5, must take great care to convert all synchronous, -blocking I/O, to `async` I/O. This is because, as the name implies, blocking I/O -blocks a thread from making progress until the I/O result is available, meaning -that no tasks can be scheduled on the waiting thread, wasting valuable resources -and degrading performance. - -Common sources of blocking I/O and their `async` replacements include: - - * Anything in `std::fs`: replace with `rocket::tokio::fs`. - * Anything in `std::sync`: replace with `rocket::tokio::sync`. - * Anything in `std::net`: replace with `rocket::tokio::net`. - * Anything in `std::io`: replace with `rocket::tokio::io`. - * Sleep or timers: replace with `rocket::tokio::time`. - * Any networking: replace with `rocket::tokio::net`. - * Any file system access: replace with `rocket::tokio::fs`. - -Unfortunately, the Rust compiler provides no support for identifying blocking -I/O via lints or compile-time checks: it is up to you to scan your application -for sources of blocking I/O and replace them with their `async` counterpart. If -no such counterpart exists, you should execute the relevant I/O in its own -thread by using [`rocket::tokio::task::spawn_blocking`]. - -All of Rocket's I/O APIs have been updated to be `async`-safe. -This results in requiring `.await` calls for common APIs like [`NamedFile`]. To -use `.await` in a route, the handler must be marked with `async`: - -```rust -# use rocket::get; -use rocket::fs::NamedFile; - -#[get("/")] -async fn index() -> Option { - NamedFile::open("index.html").await.ok() -} -``` - -! warning: Non-`async` routes are _also_ executed on the `async` runtime. - - A route that _isn't_ declared as `async` is _still_ executed on the `async` - runtime. As a result, it should not execute blocking I/O. - -
-See a diff of the changes from v0.4. - -```diff -- use rocket::response::NamedFile; -+ use rocket::fs::NamedFile; - -#[get("/")] -- fn index() -> Option { -- NamedFile::open("index.html").ok() -+ async fn index() -> Option { -+ NamedFile::open("index.html").await.ok() -} -``` - -
- -[`tokio`]: https://tokio.rs -[configurable number of worker threads]: ../configuration/#workers -[`NamedFile`]: @api/rocket/fs/struct.NamedFile.html -[`rocket::tokio::task::spawn_blocking`]: @tokio/task/fn.spawn_blocking.html - -### Blocking Compute - -By the same reasoning, performing large amounts of compute (really, just another -form of I/O) can prevent other tasks from executing in a timely manner. If you -are performing long computations in a handler, you should execute the -computation in its own thread, again using [`rocket::tokio::task::spawn_blocking`]: - -```rust -# use rocket::get; -use rocket::tokio::task; -use rocket::response::Debug; - -#[get("/")] -async fn expensive() -> Result<(), Debug> { - let result = task::spawn_blocking(move || { - // perform the computation - }).await?; - - Ok(result) -} -``` - -### Async Traits - -To support `async` methods in traits, Rocket provides the [`async_trait`] -attribute. The attribute _must_ be applied to all implementations of _async -traits_ like [`FromRequest`] and [`Fairing`]: - -```diff -use rocket::request::{self, Request, FromRequest}; - -+ #[rocket::async_trait] -impl<'r> FromRequest<'r> for MyType { - type Error = MyError; - -- fn from_request(req: &'r Request<'_>) -> request::Outcome { -+ async fn from_request(req: &'r Request<'_>) -> request::Outcome { - /* .. */ - } -} -``` - -All trait documentation has been updated to call out such traits with an example -implementation that includes the invocation. The example implementation also -serves as better documentation for trait and trait method signatures than the -rustdocs. Because `async_trait` modifies these signatures, the rustdocs diverge -from what is written in the source. For example, rustdoc renders: - -```rust,ignore -fn from_request<'life0, 'async_trait>( - request: &'r Request<'life0> -) -> Pin> + Send + 'async_trait>>; -``` - -...whereas the source looks like: - -```rust,ignore -async fn from_request(req: &'r Request<'_>) -> Outcome; -``` - -Unfortunately, rustdoc does not provide a mechanism to render the source as it -is written. As such, we encourage all authors to use the examples as the source -of truth for trait and method signatures. - -[`async_trait`]: @api/rocket/attr.async_trait.html -[`FromRequest`]: @api/rocket/request/trait.FromRequest.html -[`Fairing`]: @api/rocket/fairing/trait.Fairing.html - -## Configuration - -Rocket's configuration system has been entirely revamped for v0.5. The -[configuration](../configuration) section of the guide contains a full -walkthrough of the new system while the [general changes] section of the -[CHANGELOG] contains further details on configuration changes. We call out the -most important of these changes here. All users _must_: - - * Replace the `ROCKET_ENV` environment variable with `ROCKET_PROFILE`. - * Replace the `ROCKET_LOG` environment variable with `ROCKET_LOG_LEVEL`. - * Use only IP addresses for the `address` configuration parameter. - * Replace the `dev` or `development` profile with `debug`. - * Note that the `stage`, `staging`, `prod`, and `production` profiles carry no - special meaning in v0.5. - * Use `0` to disable `keep_alive` instead of `false` or `off`. - * Replace uses of "extras" with [typed extraction]. - -Rocket will emit warnings at launch time if use of the previous functionality is -detected. - -### Profiles - -The new system deals with "profiles" where there were previously "environments". -As opposed to environments, profiles: - - * Can be arbitrarily named and any number can exist. - * Match Rust profiles in naming: `debug` and `release` are the default - profiles for the respective Rust compilation profile. - * Are programmatically selectable and configurable. - * Have a `default` profile with fallback values for all profiles. - * Have a `global` profile with overrides for all profiles. - -Authors should read the new [configuration](../configuration) section of the -guide to determine the scope of changes required. This likely includes: - - * Defining most configuration in the `default` profile instead. - * Using the `debug` profile where `dev` or `development` was used. - * Using the `release` profile where `prod` or `production` was used. - -[general changes]: @github/CHANGELOG.md#general -[typed extraction]: ../configuration/#extracting-values - -### Typed Extraction - -The "extras" configuration in v0.4 is entirely replaced by [typed extraction], -which allows any `Deserialize` structure to be derived from configuration -sources. All users _should_ make use of typed extraction where "extras" were -being used previously. The diff below illustrates one such example: - -```diff -use rocket::fairing::AdHoc; - -+ #[derive(Deserialize)] -struct AppConfig { - id: Option, - port: u16, -} - -- fn main() { -- rocket::ignite() -- .attach(AdHoc::on_attach("Token Config", |rocket| { -- println!("Adding token managed state from config..."); -- let id = match rocket.config().get_int("id") { -- Ok(v) if v >= 0 => Some(v as usize), -- _ => None, -- }; -- -- let port = match rocket.config().get_int("port") { -- Ok(v) if v => 0 && v < 1 << 16 => v as u16, -- _ => return Err(rocket) -- }; -- -- Ok(rocket.manage(AppConfig { id, port })) -- })) -- } - -+ #[launch] -+ fn rocket() -> _ { -+ rocket::build().attach(AdHoc::config::()) -+ } -``` - -## Routing - -Rocket v0.5 brings several major changes that affect routing: - - 1. [Default ranking] is more precise, so fewer routes need manual ranking. - 2. Multi-segment route parameters (``) now match _zero_ or more - segments. - 3. Parameters are _always_ percent-decoded, so `&RawStr` no longer implements - `FromParam`. - 4. Query parameters parse with [`FromForm`] instead of `FromQuery` and support - arbitrarily collections, nesting, structures, etc. - 5. All UTF-8 characters are allowed in static path components: `#[get("/๐Ÿš€")]`. - 6. The [`register()`] method require a path to [scope catchers] under. Using - `"/"` emulates the previous behavior. - -[Default ranking]: ../requests#default-ranking -[`FromForm`]: @api/rocket/form/trait.FromForm.html -[`FromParam`]: @api/rocket/request/trait.FromParam.html -[`register()`]: @api/rocket/struct.Rocket.html#method.register -[scope catchers]: ../requests/#scoping - -### Default Ranks - -Default route ranking now takes into account partially dynamic paths, increasing -the range of default ranks from `[-6, -1]` to `[-12, -1]`. The net effect is -that fewer routes collide by default, requiring less manual ranking. For -example, the following two routes collide in v0.4 but not in v0.5: - -```rust -# use rocket::get; - -#[get("/foo/<_>/bar")] -fn foo_bar() { } - -#[get("/<_..>")] -fn everything() { } -``` - -
-See a diff of the changes from v0.4. - -```diff -- #[get("/foo/<_>/bar", rank = 1)] -+ #[get("/foo/<_>/bar")] - fn foo_bar() { } - -- #[get("/<_..>", rank = 2)] -+ #[get("/<_..>")] - fn everything() { } -``` -
- -**The recommendation** is to remove all unnecessary manual ranking parameters. -For smaller applications, you may find that _all_ manual ranks can be removed. -Larger applications may still require ranks to resolve ambiguities. - -### Kleene Multi-Segments - -The multi-segment route parameter `` now matches _zero or more_ segments, -a change from the previous _one_ or more segments. The implication is two-fold: - - 1. Where previously two routes were required to match a prefix and its - suffixes, now one suffices: - - ```diff - - #[get("/")] - - fn index(); - - - #[get("/")] - - fn rest(path: PathBuf); - - + #[get("/")] - + fn all(path: PathBuf); - ``` - - 2. A prefix collides with a route that matches all of its suffixes. For - example, `index` and `rest` above collide. - -Most applications will likely benefit from this change by allowing the extra -prefix-only route to be removed entirely. If the previous functionality of -requiring at least one segment is desired, a route that explicitly matches the -first segment can be used: - -```rust -# use std::path::PathBuf; -# use rocket::get; - -#[get("//")] -fn rest(first: PathBuf, rest: PathBuf) { /* .. */ } -``` - -### Fewer Raw Strings - -Rocket v0.5 makes a concerted effort to limit the exposure to strings from the -raw HTTP payload. In line with this philosophy, Rocket now percent-decodes all -incoming parameters automatically as opposed to doing so on-demand. The -corollary is three-fold: - - 1. The `&RawStr` type no longer implements [`FromParam`]. - 2. The `&str` type now implements [`FromParam`] and is fully decoded. - 3. The `String` parameter type is identical to the `&str` type and should be - avoided. - -Most applications can simply swap uses of `&RawStr` and `String` for `&str` in -routes, forms, and so on to benefit from the increased safety and performance. -For instance, the front-page example becomes: - -```diff - #[get("//")] -- fn hello(name: String, age: u8) -> String { -+ fn hello(name: &str, age: u8) -> String { - format!("Hello, {} year old named {}!", age, name) -} -``` - -A form that previously used `String` becomes: - -```diff -#[derive(FromForm)] -- struct MyForm { -+ struct MyForm<'r> { -- value: String, -+ value: &'r str, -} -``` - -### Queries as Forms - -Query strings in Rocket v0.5 are in parity with forms and support their [full -breadth](../requests#forms). Single segment query parameters (``) should -require little to no changes, except that they now support collections, -structures, and any other `FromForm` type. This implies that the majority, if -not _all_ custom `FromQuery` implementations, should be derivable via `FromForm` -or have a built-in equivalent like `Vec`: - -```rust -# use rocket::post; - -#[post("/?")] -fn form(numbers: Vec) { /* .. */ } -``` - -Multi-segment query parameters (``) no longer require the use of a -`Form` guard. Instead, `T` can be used directly: - -```diff -#[derive(FromForm)] -struct Person { /* .. */ } - -#[get("/hello?")] -- fn hello(person: Option>) -+ fn hello(person: Option) -``` - -## Forms - -Rocket v0.5 introduces entirely revamped [forms] with support for: - - * [Multipart uploads.](../requests#multipart) - * [Collections: maps, vectors, and more.](../requests#collections) - * [Nesting.](../requests#nesting) - * [Ad-Hoc validation.](../requests#ad-hoc-validation) - -Additionally, the [`FromForm` derive] has been substantially improved so that -nearly all custom implementations of `FromForm` or [`FromFormField`], which -replaces `FromFormValue` from v0.4, can be derived. Altogether, this means that -any external crate dependency for form handling and most custom `FromForm` or -`FromFormValue` implementations are unnecessary and should be removed. - -[`FromFormField`]: @api/rocket/form/trait.FromFormField.html - -### Multipart - -If your application used an external crate to accept multipart form submissions, -the dependency should be removed: Rocket v0.5 natively handles multipart. A file -upload can be accepted via the [`TempFile`] form guard: - -```rust -# #[macro_use] extern crate rocket; - -use rocket::form::Form; -use rocket::fs::TempFile; - -#[derive(FromForm)] -struct Upload<'r> { - save: bool, - file: TempFile<'r>, -} - -#[post("/upload", data = "")] -fn upload(upload: Form>) { /* .. */ } -``` - -[`TempFile`]: @api/rocket/fs/enum.TempFile.html - -### Field Validation - -In Rocket v0.4, it was encouraged and often required to implement -`FromFormValue` to introduce typed field validation. In v0.5, this can be -accomplished by [deriving `FromForm`]: - -```diff -- use rocket::request::FromFormValue; -- use rocket::http::RawStr; -- -- struct AdultAge(usize); -- -- impl<'v> FromFormValue<'v> for AdultAge { -- type Error = &'v RawStr; -- -- fn from_form_value(form_value: &'v RawStr) -> Result { -- match form_value.parse::() { -- Ok(age) if age >= 21 => Ok(AdultAge(age)), -- _ => Err(form_value), -- } -- } -- } - -+ #[derive(FromForm)] -+ #[field(validate = range(21..))] -+ struct AdultAge(usize); -``` - -If a given validation is used once, a new type may offer no additional safety. -The validation can be performed directly on a field: - -```rust -use rocket::form::FromForm; - -#[derive(FromForm)] -struct MyForm { - #[field(validate = range(21..))] - age: usize, -} -``` - -[forms]: ../requests#forms -[`FromForm` derive]: @api/rocket/derive.FromForm.html -[deriving `FromForm`]: @api/rocket/derive.FromForm.html - -## Notable New Features - -Rocket v0.5 brings an abundance of new features that enable new functionality, -increase productivity, and make existing applications more robust. We encourage -all users to take advantage of these new features. - -### Sentinels - -Rocket v0.5 introduces [sentinels]. Entirely unique to Rocket, sentinels offer -an automatic last line of defense against runtime errors by enabling any type -that appears in a route to abort application launch if invalid conditions are -detected. For example, the [`&State`] guard in v0.5 is a [`Sentinel`] that -aborts launch if the type `T` is not in managed state, thus preventing -associated runtime errors. - -You should consider implementing `Sentinel` for your types if you have guards -(request, data, form, etc.) or responders that depend on `Rocket` state to -function properly. For example, consider a `MyResponder` that expects: - - * An error catcher to be registered for the `400` status code. - * A specific type `T` to be in managed state. - -Making `MyResponder` a sentinel that guards against these conditions is as -simple as: - -```rust -use rocket::{Rocket, Ignite, Sentinel}; -# struct MyResponder; -# struct T; - -impl Sentinel for MyResponder { - fn abort(r: &Rocket) -> bool { - !r.catchers().any(|c| c.code == Some(400)) || r.state::().is_none() - } -} -``` - -[sentinels]: @api/rocket/trait.Sentinel.html -[`Sentinel`]: @api/rocket/trait.Sentinel.html -[`&State`]: @api/rocket/struct.State.html - -### More Typed URIs - -Rocket v0.5 brings a completely overhauled [`uri!()`] macro and support for -typed URIs in more APIs. Notably, the `uri!()` macro now: - - * Allows URIs to be constructed from and as static values: - - ```rust - # use rocket::uri; - use rocket::http::uri::Absolute; - - const HOST: Absolute<'static> = uri!("http://localhost:8000"); - ``` - - * Allows static and dynamic [prefixes and suffixes] to route URIs to be - specified: - - ```rust - # use rocket::{uri, get}; - - #[get("/person/?")] - fn person(name: &str, age: Option) { } - - let uri = uri!("https://rocket.rs/", person("Bob", Some(28)), "#woo"); - assert_eq!(uri.to_string(), "https://rocket.rs/person/Bob?age=28#woo"); - - let host = uri!("http://bob.me"); - let uri = uri!(host, person("Bob", Some(28))); - assert_eq!(uri.to_string(), "http://bob.me/person/Bob?age=28"); - ``` - -APIs like [`Redirect`] and [`Client`] now accept typed URIs: - -```rust -# #[macro_use] extern crate rocket; - -use rocket::response::Redirect; - -#[get("/bye//")] -fn bye(name: &str, age: u8) -> Redirect { - Redirect::to(uri!("https://rocket.rs", bye(name, age), "?bye#now")) -} - -#[test] -fn test() { - use rocket::local::blocking::Client; - - let client = Client::new(rocket::build()); - let r = client.get(uri!(super::bye("Bob", 30))).dispatch(); -} -``` - -[URI types] have been overhauled accordingly. A new [`Reference`] type encodes -URI-references. Additionally, all URI types are now `Serialize` and -`Deserialize`, allowing URIs to be used in configuration and passed over the -wire. - -[`Redirect`]: @api/rocket/response/struct.Redirect.html -[`Client`]: @api/rocket/local/index.html -[prefixes and suffixes]: @api/rocket/macro.uri.html#prefixes-and-suffixes -[`uri!()`]: @api/rocket/macro.uri.html -[URI types]: @api/rocket/http/uri/index.html -[`Reference`]: @api/rocket/http/uri/struct.Reference.html - -### Real-Time Streams - -Rocket v0.5 introduces real-time, typed, `async` [streams]. The new [async -streams] section of the guide contains further details, and we encourage all -interested parties to see the new real-time, multi-room [chat example]. - -As a taste of what's possible, the following `stream` route emits a `"ping"` -Server-Sent Event every `n` seconds, defaulting to `1`: - -```rust -# use rocket::*; -use rocket::response::stream::{Event, EventStream};; -use rocket::tokio::time::{interval, Duration}; - -#[get("/ping?")] -fn stream(n: Option) -> EventStream![] { - EventStream! { - let mut timer = interval(Duration::from_secs(n.unwrap_or(1))); - loop { - yield Event::data("ping"); - timer.tick().await; - } - } -} -``` - -[streams]: @api/rocket/response/stream/index.html -[async streams]: ../responses/#async-streams -[chat example]: @example/chat - -### WebSockets - -Rocket v0.5 introduces support for HTTP connection upgrades via a new [upgrade -API]. The API allows responders to take over an HTTP connection and perform raw -I/O with the client. In other words, an HTTP connection can be _upgraded_ to any -protocol, including HTTP WebSockets! - -The newly introduced [`rocket_ws`] library takes advantage of the new API to -implement first-class support for WebSockets entirely outside of Rocket's core. -The simplest use of the library, implementing an echo server and showcasing that -the incoming message stream is `async`, looks like this: - -```rust -# use rocket::get; -# use rocket_ws as ws; - -#[get("/echo")] -fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { - ws.stream(|io| io) -} -``` - -The simplified [async streams] generator syntax can also be used: - -```rust -# use rocket::get; -# use rocket_ws as ws; - -#[get("/echo")] -fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::Stream! { ws => - for await message in ws { - yield message?; - } - } -} -``` - -For complete usage details, see the [`rocket_ws`] documentation. - -[upgrade API]: @api/rocket/response/struct.Response.html#upgrading -[`rocket_ws`]: @api/rocket_ws +This a placeholder for an eventual migration guide from v0.5 to v0.6. ## Getting Help diff --git a/site/guide/index.md b/site/guide/index.md index fe9f0d645b..fdcebe9d9a 100644 --- a/site/guide/index.md +++ b/site/guide/index.md @@ -14,7 +14,7 @@ aspect of Rocket. The sections are: - **[Introduction](introduction/):** introduces Rocket and its philosophy. - **[Quickstart](quickstart/):** presents the minimal steps necessary to run your first Rocket application. - - **[Upgrading from v0.4](upgrading/):** a migration guide from v0.4 to v0.5. + - **[Upgrading from v0.5](upgrading/):** a migration guide from v0.5 to v0.6. - **[Getting Started](getting-started/):** a gentle introduction to getting your first Rocket application running. - **[Overview](overview/):** describes the core concepts of Rocket. From 89a2af179b7564d668742c5c11221d3df8b6fe94 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Nov 2023 11:43:11 +0100 Subject: [PATCH 038/178] Use versioned URIs in news articles. --- site/news/2021-06-09-version-0.5-rc.1.md | 6 +-- site/news/2022-05-09-version-0.5-rc.2.md | 8 +-- site/news/2023-03-23-version-0.5-rc.3.md | 4 +- site/news/2023-11-01-version-0.5-rc.4.md | 2 +- site/news/2023-11-17-version-0.5.md | 68 ++++++++++++------------ 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/site/news/2021-06-09-version-0.5-rc.1.md b/site/news/2021-06-09-version-0.5-rc.1.md index 0044b4e877..d61c1bd62b 100644 --- a/site/news/2021-06-09-version-0.5-rc.1.md +++ b/site/news/2021-06-09-version-0.5-rc.1.md @@ -27,9 +27,9 @@ to your feedback! [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc1-jun-9-2021 -[API docs]: @api -[guide]: ../../guide +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.1/CHANGELOG.md +[API docs]: @api-v0.5 +[guide]: @guide-v0.5 ## About Rocket diff --git a/site/news/2022-05-09-version-0.5-rc.2.md b/site/news/2022-05-09-version-0.5-rc.2.md index 58fd3dc71b..7456f3e19a 100644 --- a/site/news/2022-05-09-version-0.5-rc.2.md +++ b/site/news/2022-05-09-version-0.5-rc.2.md @@ -26,10 +26,10 @@ for the general release! [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 -[API docs]: @api -[guide]: ../../guide +[migration guide]: @guide-v0.5/upgrading +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.2/CHANGELOG.md +[API docs]: @api-v0.5 +[guide]: @guide-v0.5 ## About Rocket diff --git a/site/news/2023-03-23-version-0.5-rc.3.md b/site/news/2023-03-23-version-0.5-rc.3.md index cec4823b14..6501529add 100644 --- a/site/news/2023-03-23-version-0.5-rc.3.md +++ b/site/news/2023-03-23-version-0.5-rc.3.md @@ -16,8 +16,8 @@ v0.4. For changes since Rocket v0.5.0-rc.2, please see the [CHANGELOG]. [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[migration guide]: ../../guide/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5/CHANGELOG.md#version-050-rc2-may-9-2022 +[migration guide]: @guide-v0.5/upgrading +[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.3/CHANGELOG.md ## About Rocket diff --git a/site/news/2023-11-01-version-0.5-rc.4.md b/site/news/2023-11-01-version-0.5-rc.4.md index 31a3b7503a..ecd81614ea 100644 --- a/site/news/2023-11-01-version-0.5-rc.4.md +++ b/site/news/2023-11-01-version-0.5-rc.4.md @@ -16,7 +16,7 @@ v0.4. For changes since Rocket v0.5.0-rc.3, please see the [CHANGELOG]. [GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues [GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[migration guide]: ../../guide/upgrading +[migration guide]: @guide-v0.5/upgrading [CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.4/CHANGELOG.md ## About Rocket diff --git a/site/news/2023-11-17-version-0.5.md b/site/news/2023-11-17-version-0.5.md index e3a81063b0..cf66c5ccd8 100644 --- a/site/news/2023-11-17-version-0.5.md +++ b/site/news/2023-11-17-version-0.5.md @@ -26,7 +26,7 @@ v0.6 is released. [RWF2]: https://rwf2.org [along with the prelaunch]: ../2023-11-17-rwf2-prelaunch/ -[upgrading guide]: ../../guide/upgrading +[upgrading guide]: @guide-v0.5/upgrading ## What's New? @@ -37,8 +37,8 @@ here and encourage everyone to read the [CHANGELOG] for a complete list. For answers to frequently asked questions, see the new [FAQ]. [CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0/CHANGELOG.md -[broader ecosystem]: ../../guide/faq/#releases -[FAQ]: ../../guide/faq +[broader ecosystem]: @guide-v0.5/faq/#releases +[FAQ]: @guide-v0.5/faq ### โš“ Support for Stable `rustc` since `rc.1` @@ -90,7 +90,7 @@ Note the new [`launch`] attribute, which simplifies starting an `async` runtime for Rocket applications. See the [migration guide] for more on transitioning to a stable toolchain. -[`launch`]: @api/rocket/attr.launch.html +[`launch`]: @api-v0.5/rocket/attr.launch.html ### ๐Ÿ“ฅ Async I/O since `rc.1` @@ -115,7 +115,7 @@ async fn debug(data: Data<'_>) -> std::io::Result<()> { } ``` -See the [Blocking I/O](@guide/upgrading#blocking-io) section of the upgrading +See the [Blocking I/O](@guide-v0.5/upgrading#blocking-io) section of the upgrading guide for complete details on the `async` I/O transition. ### ๐Ÿ’‚ Sentinels since `rc.1` @@ -150,11 +150,11 @@ impl Sentinel for MyResponder { } ``` -[sentinels]: @api/rocket/trait.Sentinel.html -[`Sentinel`]: @api/rocket/trait.Sentinel.html -[`&State`]: @api/rocket/struct.State.html -[`Template`]: @api/rocket_dyn_templates/struct.Template.html -[`rocket_dyn_templates`]: @api/rocket_dyn_templates/index.html +[sentinels]: @api-v0.5/rocket/trait.Sentinel.html +[`Sentinel`]: @api-v0.5/rocket/trait.Sentinel.html +[`&State`]: @api-v0.5/rocket/struct.State.html +[`Template`]: @api-v0.5/rocket_dyn_templates/struct.Template.html +[`rocket_dyn_templates`]: @api-v0.5/rocket_dyn_templates/index.html ### โ˜„๏ธ Streams and SSE since `rc.1` @@ -183,8 +183,8 @@ fn stream(n: Option) -> EventStream![] { } ``` -[streams]: @api/rocket/response/stream/index.html -[async streams]: @guide/responses/#async-streams +[streams]: @api-v0.5/rocket/response/stream/index.html +[async streams]: @guide-v0.5/responses/#async-streams [chat example]: @example/chat ### ๐Ÿ”Œ WebSockets since `rc.4` @@ -227,8 +227,8 @@ fn echo_stream(ws: WebSocket) -> Stream!['static] { For complete usage details, see the [`rocket_ws`] documentation. -[upgrade API]: @api/rocket/response/struct.Response.html#upgrading -[`rocket_ws`]: @api/rocket_ws +[upgrade API]: @api-v0.5/rocket/response/struct.Response.html#upgrading +[`rocket_ws`]: @api-v0.5/rocket_ws ### ๐Ÿ“ Comprehensive Forms since `rc.1` @@ -291,14 +291,14 @@ MyForm { The rewritten [forms guide] provides complete details on revamped forms support. -[forms guide]: @guide/requests/#forms -[ad-hoc validation]: @guide/requests#ad-hoc-validation -[arbitrary nesting]: @guide/requests#nesting -[multipart uploads]: @guide/requests#multipart -[forms]: @guide/requests#forms -[`FromFormField`]: @api/rocket/form/trait.FromFormField.html -[arbitrary collections]: @guide/requests#collections -[`FromForm` derive]: @api/rocket/derive.FromForm.html +[forms guide]: @guide-v0.5/requests/#forms +[ad-hoc validation]: @guide-v0.5/requests#ad-hoc-validation +[arbitrary nesting]: @guide-v0.5/requests#nesting +[multipart uploads]: @guide-v0.5/requests#multipart +[forms]: @guide-v0.5/requests#forms +[`FromFormField`]: @api-v0.5/rocket/form/trait.FromFormField.html +[arbitrary collections]: @guide-v0.5/requests#collections +[`FromForm` derive]: @api-v0.5/rocket/derive.FromForm.html ### ๐Ÿš€ And so much more! @@ -314,18 +314,18 @@ few more we don't want you to miss: * Asynchronous database pooling support via [`rocket_db_pools`]. * Compile-time URI literals via a fully revamped [`uri!`] macro. -[`Shield`]: @api/rocket/shield/struct.Shield.html -[graceful shutdown]: @api/rocket/config/struct.Shutdown.html#summary -[notification]: @api/rocket/struct.Shutdown.html -[shutdown fairings]: @api/rocket/fairing/trait.Fairing.html#shutdown -[configuration system]: @guide/configuration/#configuration +[`Shield`]: @api-v0.5/rocket/shield/struct.Shield.html +[graceful shutdown]: @api-v0.5/rocket/config/struct.Shutdown.html#summary +[notification]: @api-v0.5/rocket/struct.Shutdown.html +[shutdown fairings]: @api-v0.5/rocket/fairing/trait.Fairing.html#shutdown +[configuration system]: @guide-v0.5/configuration/#configuration [Figment]: https://docs.rs/figment/ -[incoming data limits]: @guide/requests/#streaming -[mutual TLS]: @guide/configuration/#mutual-tls -[`uri!`]: @api/rocket/macro.uri.html -[`rocket_db_pools`]: @api/rocket_db_pools/index.html -[`Certificate`]: @api/rocket/mtls/struct.Certificate.html -[migration guide]: ../../guide/upgrading +[incoming data limits]: @guide-v0.5/requests/#streaming +[mutual TLS]: @guide-v0.5/configuration/#mutual-tls +[`uri!`]: @api-v0.5/rocket/macro.uri.html +[`rocket_db_pools`]: @api-v0.5/rocket_db_pools/index.html +[`Certificate`]: @api-v0.5/rocket/mtls/struct.Certificate.html +[migration guide]: @guide-v0.5/upgrading ## What's Next? @@ -438,7 +438,7 @@ release: [an interface for implementing and plugging-in custom connection listeners]: https://github.com/SergioBenitez/Rocket/issues/1070#issuecomment-1491101952 [stabilization of `async fn` in traits]: https://github.com/rust-lang/rust/pull/115822 -[poor benchmarking]: @guide/faq/#performance +[poor benchmarking]: @guide-v0.5/faq/#performance From 9a04cff9c0e7adb6456b092503198b4f886a8afc Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 21 Nov 2023 07:21:19 -0800 Subject: [PATCH 039/178] Add GitHub FUNDING file. --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..574c51264e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: rwf2 +open_collective: rwf2 From e81f6c83236245bf5bd2fbaa99cc43feb0a27be6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 18 Nov 2023 21:13:54 +0100 Subject: [PATCH 040/178] Set prelease config setting to true. --- scripts/config.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/config.sh b/scripts/config.sh index 42f4ec2957..bfae241342 100755 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -59,7 +59,7 @@ MAJOR_VERSION=$(echo "${VERSION}" | cut -d'.' -f1-2) VIRTUAL_CODENAME="$(git branch --show-current)" PHYSICAL_CODENAME="v${MAJOR_VERSION}" CURRENT_RELEASE=false -PRE_RELEASE=false +PRE_RELEASE=true # A generated codename for this version. Use the git branch for pre-releases. case $PRE_RELEASE in From e526fa04d41c6c8d05589e3dd517b462145cef7f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 21 Nov 2023 16:29:44 +0100 Subject: [PATCH 041/178] Update 'SergioBenitez/Rocket' to 'rwf2/Rocket'. Resolves rwf2/rwf2#5. --- CHANGELOG.md | 40 ++++++++++++------------ README.md | 8 ++--- contrib/db_pools/README.md | 4 +-- contrib/db_pools/codegen/Cargo.toml | 2 +- contrib/db_pools/lib/Cargo.toml | 2 +- contrib/dyn_templates/Cargo.toml | 2 +- contrib/dyn_templates/README.md | 4 +-- contrib/sync_db_pools/README.md | 4 +-- contrib/sync_db_pools/codegen/Cargo.toml | 2 +- contrib/sync_db_pools/lib/Cargo.toml | 2 +- contrib/ws/Cargo.toml | 2 +- contrib/ws/README.md | 4 +-- core/codegen/Cargo.toml | 2 +- core/codegen/src/attribute/route/mod.rs | 2 +- core/http/Cargo.toml | 2 +- core/http/src/lib.rs | 2 +- core/http/src/uri/fmt/formatter.rs | 2 +- core/lib/Cargo.toml | 2 +- core/lib/src/log.rs | 2 +- site/README.md | 4 +-- site/guide/01-upgrading.md | 2 +- site/guide/1-quickstart.md | 2 +- site/guide/11-conclusion.md | 2 +- site/guide/12-faq.md | 16 +++++----- site/guide/2-getting-started.md | 2 +- site/news/2017-02-06-version-0.2.md | 14 ++++----- site/news/2017-07-14-version-0.3.md | 12 +++---- site/news/2018-10-31-version-0.4-rc.md | 4 +-- site/news/2018-11-30-version-0.4-rc-2.md | 4 +-- site/news/2018-12-08-version-0.4.md | 16 +++++----- site/news/2021-06-09-version-0.5-rc.1.md | 6 ++-- site/news/2022-05-09-version-0.5-rc.2.md | 6 ++-- site/news/2023-03-23-version-0.5-rc.3.md | 6 ++-- site/news/2023-11-01-version-0.5-rc.4.md | 6 ++-- site/news/2023-11-17-version-0.5.md | 12 +++---- site/news/index.toml | 6 ++-- 36 files changed, 105 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2981353f69..7d5902262e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -569,7 +569,7 @@ The following changes were made to the project's infrastructure: [automatic typed config extraction]: https://api.rocket.rs/v0.5/rocket/fairing/struct.AdHoc.html#method.config [misconfigured secrets]: https://api.rocket.rs/v0.5/rocket/config/struct.SecretKey.html [default ranking colors]: https://rocket.rs/v0.5/guide/requests/#default-ranking -[`chat`]: https://github.com/SergioBenitez/Rocket/tree/v0.5/examples/chat +[`chat`]: https://github.com/rwf2/Rocket/tree/v0.5/examples/chat [`Form` guard]: https://api.rocket.rs/v0.5/rocket/form/struct.Form.html [`Strict`]: https://api.rocket.rs/v0.5/rocket/form/struct.Strict.html [`CookieJar`#pending]: https://api.rocket.rs/v0.5/rocket/http/struct.CookieJar.html#pending @@ -728,7 +728,7 @@ The following changes were made to the project's infrastructure: * [[`3276b8`]] Removed `unsafe` in `Origin::parse_owned()`, fixing a soundness issue. -[`3276b8`]: https://github.com/SergioBenitez/Rocket/commit/3276b8 +[`3276b8`]: https://github.com/rwf2/Rocket/commit/3276b8 # Version 0.4.9 (May 19, 2021) @@ -736,8 +736,8 @@ The following changes were made to the project's infrastructure: * [[`#1645`], [`f2a56f`]] Fixed `Try` `impl FromResidual for Outcome`. -[`#1645`]: https://github.com/SergioBenitez/Rocket/issues/1645 -[`f2a56f`]: https://github.com/SergioBenitez/Rocket/commit/f2a56f +[`#1645`]: https://github.com/rwf2/Rocket/issues/1645 +[`f2a56f`]: https://github.com/rwf2/Rocket/commit/f2a56f # Version 0.4.8 (May 18, 2021) @@ -752,8 +752,8 @@ The following changes were made to the project's infrastructure: * Updated `base64` dependency to `0.13`. -[`#1548`]: https://github.com/SergioBenitez/Rocket/issues/1548 -[`93e88b0`]: https://github.com/SergioBenitez/Rocket/commit/93e88b0 +[`#1548`]: https://github.com/rwf2/Rocket/issues/1548 +[`93e88b0`]: https://github.com/rwf2/Rocket/commit/93e88b0 # Version 0.4.7 (Feb 09, 2021) @@ -762,8 +762,8 @@ The following changes were made to the project's infrastructure: * [[#1534], [`2059a6`]] Fixed a low-severity, minimal impact soundness issue in `uri::Formatter`. -[#1534]: https://github.com/SergioBenitez/Rocket/issues/1534 -[`2059a6`]: https://github.com/SergioBenitez/Rocket/commit/2059a6 +[#1534]: https://github.com/rwf2/Rocket/issues/1534 +[`2059a6`]: https://github.com/rwf2/Rocket/commit/2059a6 # Version 0.4.6 (Nov 09, 2020) @@ -784,8 +784,8 @@ The following changes were made to the project's infrastructure: * Updated source code for Rust 2018. * UI tests now use `trybuild` instead of `compiletest-rs`. -[`86bd7c`]: https://github.com/SergioBenitez/Rocket/commit/86bd7c -[`c24a96`]: https://github.com/SergioBenitez/Rocket/commit/c24a96 +[`86bd7c`]: https://github.com/rwf2/Rocket/commit/86bd7c +[`c24a96`]: https://github.com/rwf2/Rocket/commit/c24a96 [enables flushing]: https://api.rocket.rs/v0.4/rocket/response/struct.Stream.html#buffering-and-blocking # Version 0.4.5 (May 30, 2020) @@ -807,10 +807,10 @@ The following changes were made to the project's infrastructure: * Fixed various typos. -[#1312]: https://github.com/SergioBenitez/Rocket/issues/1312 -[`89150f`]: https://github.com/SergioBenitez/Rocket/commit/89150f -[#1263]: https://github.com/SergioBenitez/Rocket/issues/1263 -[`376f74`]: https://github.com/SergioBenitez/Rocket/commit/376f74 +[#1312]: https://github.com/rwf2/Rocket/issues/1312 +[`89150f`]: https://github.com/rwf2/Rocket/commit/89150f +[#1263]: https://github.com/rwf2/Rocket/issues/1263 +[`376f74`]: https://github.com/rwf2/Rocket/commit/376f74 [`Origin::map_path()`]: https://api.rocket.rs/v0.4/rocket/http/uri/struct.Origin.html#method.map_path [`handler::Outcome::from_or_forward()`]: https://api.rocket.rs/v0.4/rocket/handler/type.Outcome.html#method.from_or_forward [`Options::NormalizeDirs`]: https://api.rocket.rs/v0.4/rocket_contrib/serve/struct.Options.html#associatedconstant.NormalizeDirs @@ -1314,7 +1314,7 @@ In addition to new features, Rocket saw the following improvements: * The `rustls` dependency was updated to `0.14`. * The `cookie` dependency was updated to `0.11`. -[Tera templates example]: https://github.com/SergioBenitez/Rocket/tree/v0.4/examples/tera_templates +[Tera templates example]: https://github.com/rwf2/Rocket/tree/v0.4/examples/tera_templates [`FormItems`]: https://api.rocket.rs/v0.4/rocket/request/enum.FormItems.html [`Config::active()`]: https://api.rocket.rs/v0.4/rocket/config/struct.Config.html#method.active [`Flash`]: https://api.rocket.rs/v0.4/rocket/response/struct.Flash.html @@ -1576,7 +1576,7 @@ In addition to new features, Rocket saw the following improvements: * Ignored named route parameters are now allowed (`_ident`). * Fixed issue where certain paths would cause a lint `assert!` to fail - ([#367](https://github.com/SergioBenitez/Rocket/issues/367)). + ([#367](https://github.com/rwf2/Rocket/issues/367)). * Lints were updated for `2017-08-10` nightly. * Minimum required `rustc` is `1.21.0-nightly (2017-08-10)`. @@ -1868,7 +1868,7 @@ In addition to new features, Rocket saw the following improvements: * Allow `k` and `v` to be used as fields in `FromForm` structures by avoiding identifier collisions ([#265]). -[#265]: https://github.com/SergioBenitez/Rocket/issues/265 +[#265]: https://github.com/rwf2/Rocket/issues/265 # Version 0.2.5 (Apr 16, 2017) @@ -2064,7 +2064,7 @@ applications. A couple of bugs were fixed in this release: * Handlebars partials were not properly registered - ([#122](https://github.com/SergioBenitez/Rocket/issues/122)). + ([#122](https://github.com/rwf2/Rocket/issues/122)). * `Rocket::custom` did not set the custom configuration as the `active` configuration. * Route path segments containing more than one dynamic parameter were @@ -2168,13 +2168,13 @@ In addition to new features, Rocket saw the following smaller improvements: ## Codegen * Fix `get_raw_segments` index argument in route codegen - ([#41](https://github.com/SergioBenitez/Rocket/issues/41)). + ([#41](https://github.com/rwf2/Rocket/issues/41)). * Segments params (``) respect prefixes. ## Contrib * Fix nested template name resolution - ([#42](https://github.com/SergioBenitez/Rocket/issues/42)). + ([#42](https://github.com/rwf2/Rocket/issues/42)). ## Infrastructure diff --git a/README.md b/README.md index f62a5f2f6b..ecd22dc364 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Rocket -[![Build Status](https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg)](https://github.com/SergioBenitez/Rocket/actions) +[![Build Status](https://github.com/rwf2/Rocket/workflows/CI/badge.svg)](https://github.com/rwf2/Rocket/actions) [![Rocket Homepage](https://img.shields.io/badge/web-rocket.rs-red.svg?style=flat&label=https&colorB=d33847)](https://rocket.rs) [![Current Crates.io Version](https://img.shields.io/crates/v/rocket.svg)](https://crates.io/crates/rocket) [![Matrix: #rocket:mozilla.org](https://img.shields.io/badge/style-%23rocket:mozilla.org-blue.svg?style=flat&label=[m])](https://chat.mozilla.org/#/room/#rocket:mozilla.org) @@ -128,9 +128,9 @@ come in many forms. You could: 3. Comment on [issues that require feedback]. 4. Contribute code via [pull requests]. -[issue]: https://github.com/SergioBenitez/Rocket/issues -[issues that require feedback]: https://github.com/SergioBenitez/Rocket/issues?q=is%3Aissue+is%3Aopen+label%3A%22feedback+wanted%22 -[pull requests]: https://github.com/SergioBenitez/Rocket/pulls +[issue]: https://github.com/rwf2/Rocket/issues +[issues that require feedback]: https://github.com/rwf2/Rocket/issues?q=is%3Aissue+is%3Aopen+label%3A%22feedback+wanted%22 +[pull requests]: https://github.com/rwf2/Rocket/pulls We aim to keep Rocket's code quality at the highest level. This means that any code you contribute must be: diff --git a/contrib/db_pools/README.md b/contrib/db_pools/README.md index c06b03b96f..aa6a0e5e92 100644 --- a/contrib/db_pools/README.md +++ b/contrib/db_pools/README.md @@ -4,8 +4,8 @@ [crate]: https://crates.io/crates/rocket_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 [crate docs]: https://api.rocket.rs/master/rocket_db_pools -[ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg -[ci]: https://github.com/SergioBenitez/Rocket/actions +[ci.svg]: https://github.com/rwf2/Rocket/workflows/CI/badge.svg +[ci]: https://github.com/rwf2/Rocket/actions Asynchronous database driver integration for Rocket. See the [crate docs] for full usage details. diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index 0fd1f13e8f..35176dc1ee 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_db_pools_codegen" version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Procedural macros for rocket_db_pools." -repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" +repository = "https://github.com/rwf2/Rocket/contrib/db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index e837bf7f0d..5beb44fb38 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_db_pools" version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Rocket async database pooling support" -repository = "https://github.com/SergioBenitez/Rocket/contrib/db_pools" +repository = "https://github.com/rwf2/Rocket/contrib/db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index f6cca21256..d08fabdb24 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sergio Benitez "] description = "Dynamic templating engine integration for Rocket." documentation = "https://api.rocket.rs/master/rocket_dyn_templates/" homepage = "https://rocket.rs" -repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/dyn_templates" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/dyn_templates" readme = "README.md" keywords = ["rocket", "framework", "templates", "templating", "engine"] license = "MIT OR Apache-2.0" diff --git a/contrib/dyn_templates/README.md b/contrib/dyn_templates/README.md index 34a521e217..604ea27239 100644 --- a/contrib/dyn_templates/README.md +++ b/contrib/dyn_templates/README.md @@ -4,8 +4,8 @@ [crate]: https://crates.io/crates/rocket_dyn_templates [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 [crate docs]: https://api.rocket.rs/master/rocket_dyn_templates -[ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg -[ci]: https://github.com/SergioBenitez/Rocket/actions +[ci.svg]: https://github.com/rwf2/Rocket/workflows/CI/badge.svg +[ci]: https://github.com/rwf2/Rocket/actions This crate adds support for dynamic template rendering to Rocket. It automatically discovers templates, provides a `Responder` to render templates, diff --git a/contrib/sync_db_pools/README.md b/contrib/sync_db_pools/README.md index 1a6301904c..ba5c86d25c 100644 --- a/contrib/sync_db_pools/README.md +++ b/contrib/sync_db_pools/README.md @@ -4,8 +4,8 @@ [crate]: https://crates.io/crates/rocket_sync_db_pools [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 [crate docs]: https://api.rocket.rs/master/rocket_sync_db_pools -[ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg -[ci]: https://github.com/SergioBenitez/Rocket/actions +[ci.svg]: https://github.com/rwf2/Rocket/workflows/CI/badge.svg +[ci]: https://github.com/rwf2/Rocket/actions This crate provides traits, utilities, and a procedural macro for configuring and accessing database connection pools in Rocket. This implementation is backed diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index a78b331054..007c8f2b4c 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_sync_db_pools_codegen" version = "0.1.0" authors = ["Sergio Benitez "] description = "Procedural macros for rocket_sync_db_pools." -repository = "https://github.com/SergioBenitez/Rocket/contrib/sync_db_pools" +repository = "https://github.com/rwf2/Rocket/contrib/sync_db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index fd5fdd7526..452be35b1a 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_sync_db_pools" version = "0.1.0" authors = ["Sergio Benitez "] description = "Rocket async database pooling support for sync database drivers." -repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/sync_db_pools" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/sync_db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index a6c5750c55..2bb2a03134 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sergio Benitez "] description = "WebSocket support for Rocket." documentation = "https://api.rocket.rs/master/rocket_ws/" homepage = "https://rocket.rs" -repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/ws" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/ws" readme = "README.md" keywords = ["rocket", "web", "framework", "websocket"] license = "MIT OR Apache-2.0" diff --git a/contrib/ws/README.md b/contrib/ws/README.md index e019659aea..a0c37b7705 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -4,8 +4,8 @@ [crate]: https://crates.io/crates/rocket_ws [docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 [crate docs]: https://api.rocket.rs/master/rocket_ws -[ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg -[ci]: https://github.com/SergioBenitez/Rocket/actions +[ci.svg]: https://github.com/rwf2/Rocket/workflows/CI/badge.svg +[ci]: https://github.com/rwf2/Rocket/actions This crate provides WebSocket support for Rocket via integration with Rocket's [connection upgrades] API. diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 7cf65156bb..9fffcb3cc2 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Sergio Benitez "] description = "Procedural macros for the Rocket web framework." documentation = "https://api.rocket.rs/master/rocket_codegen/" homepage = "https://rocket.rs" -repository = "https://github.com/SergioBenitez/Rocket" +repository = "https://github.com/rwf2/Rocket" readme = "../../README.md" keywords = ["rocket", "web", "framework", "code", "generation"] license = "MIT OR Apache-2.0" diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 3b72f470a7..6e29a401c0 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -161,7 +161,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { #_None => { #_log::error_!("Internal invariant broken: dyn param {} not found.", #i); #_log::error_!("Please report this to the Rocket issue tracker."); - #_log::error_!("https://github.com/SergioBenitez/Rocket/issues"); + #_log::error_!("https://github.com/rwf2/Rocket/issues"); return #Outcome::Forward((#__data, #Status::InternalServerError)); } } diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 10e5d2253f..7200b6bd1e 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -7,7 +7,7 @@ Types, traits, and parsers for HTTP requests, responses, and headers. """ documentation = "https://api.rocket.rs/master/rocket_http/" homepage = "https://rocket.rs" -repository = "https://github.com/SergioBenitez/Rocket" +repository = "https://github.com/rwf2/Rocket" readme = "../../README.md" keywords = ["rocket", "web", "framework", "http"] license = "MIT OR Apache-2.0" diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 5c1ff85b07..33a0028e68 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -9,7 +9,7 @@ //! HTTP library when needed. Because the underlying HTTP library is likely to //! change (see [#17]), types in [`hyper`] should be considered unstable. //! -//! [#17]: https://github.com/SergioBenitez/Rocket/issues/17 +//! [#17]: https://github.com/rwf2/Rocket/issues/17 #[macro_use] extern crate pear; diff --git a/core/http/src/uri/fmt/formatter.rs b/core/http/src/uri/fmt/formatter.rs index c2e81ac839..9f79a47030 100644 --- a/core/http/src/uri/fmt/formatter.rs +++ b/core/http/src/uri/fmt/formatter.rs @@ -631,7 +631,7 @@ impl SuffixedRouteUri { } } -// See https://github.com/SergioBenitez/Rocket/issues/1534. +// See https://github.com/rwf2/Rocket/issues/1534. #[cfg(test)] mod prefix_soundness_test { use crate::uri::fmt::{Formatter, UriDisplay, Query}; diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 8a49cc603a..d7becfe235 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -7,7 +7,7 @@ Web framework with a focus on usability, security, extensibility, and speed. """ documentation = "https://api.rocket.rs/master/rocket/" homepage = "https://rocket.rs" -repository = "https://github.com/SergioBenitez/Rocket" +repository = "https://github.com/rwf2/Rocket" readme = "../../README.md" keywords = ["rocket", "web", "framework", "server"] license = "MIT OR Apache-2.0" diff --git a/core/lib/src/log.rs b/core/lib/src/log.rs index 87f5118942..2b48dd04ec 100644 --- a/core/lib/src/log.rs +++ b/core/lib/src/log.rs @@ -40,7 +40,7 @@ define_log_macro!(launch_meta (launch_meta_): info, "rocket::launch", $); define_log_macro!(launch_info (launch_msg_): warn, "rocket::launch", $); // `print!` panics when stdout isn't available, but this macro doesn't. See -// SergioBenitez/Rocket#2019 and rust-lang/rust#46016 for more. +// rwf2/Rocket#2019 and rust-lang/rust#46016 for more. // // Unfortunately, `libtest` captures output by replacing a special sink that // `print!`, and _only_ `print!`, writes to. Using `write!` directly bypasses diff --git a/site/README.md b/site/README.md index 72bf794464..c8fc7cbdfa 100644 --- a/site/README.md +++ b/site/README.md @@ -29,8 +29,8 @@ Rocket API. They are replaced at build time with a URL prefix. At present, the following aliases are available, where `${version}` is Rocket's version string at the time of compilation: - * `@example`: https://github.com/SergioBenitez/Rocket/tree/${version}/examples - * `@github`: https://github.com/SergioBenitez/Rocket/tree/${version} + * `@example`: https://github.com/rwf2/Rocket/tree/${version}/examples + * `@github`: https://github.com/rwf2/Rocket/tree/${version} * `@api`: https://api.rocket.rs/${version} For example, to link to `Rocket::launch()`, you might write: diff --git a/site/guide/01-upgrading.md b/site/guide/01-upgrading.md index 5a037ac89b..f5b4fb04be 100644 --- a/site/guide/01-upgrading.md +++ b/site/guide/01-upgrading.md @@ -9,6 +9,6 @@ If you run into any issues upgrading, we encourage you to ask questions via bridged [`#rocket`] IRC channel at `irc.libera.chat`. The [FAQ](../faq/) also provides answers to commonly asked questions. -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions [`#rocket:mozilla.org`]: https://chat.mozilla.org/#/room/#rocket:mozilla.org [`#rocket`]: https://kiwiirc.com/client/irc.libera.chat/#rocket diff --git a/site/guide/1-quickstart.md b/site/guide/1-quickstart.md index c9254a6def..a88303bd0d 100644 --- a/site/guide/1-quickstart.md +++ b/site/guide/1-quickstart.md @@ -12,7 +12,7 @@ Rocket repository and run the included examples in the `examples/` directory. For instance, the following set of commands runs the `hello` example: ```sh -git clone https://github.com/SergioBenitez/Rocket +git clone https://github.com/rwf2/Rocket cd Rocket git checkout master cd examples/hello diff --git a/site/guide/11-conclusion.md b/site/guide/11-conclusion.md index 5c057e63f7..fff47d092a 100644 --- a/site/guide/11-conclusion.md +++ b/site/guide/11-conclusion.md @@ -3,7 +3,7 @@ We hope you agree that Rocket is a refreshing take on web frameworks. As with any software project, Rocket is _alive_. There are always things to improve, and we're happy to take the best ideas. If you have something in mind, please -[submit an issue](https://github.com/SergioBenitez/Rocket/issues). +[submit an issue](https://github.com/rwf2/Rocket/issues). ## Getting Help diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index 48cde1e369..0704fbdb2d 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -4,7 +4,7 @@ Below you'll find a collection of commonly asked questions and answers. If you have suggestions for questions you'd like to see answered here, [comment on the discussion thread]. -[comment on the discussion thread]: https://github.com/SergioBenitez/Rocket/discussions/1836 +[comment on the discussion thread]: https://github.com/rwf2/Rocket/discussions/1836 ## About Rocket @@ -215,7 +215,7 @@ you'd like to see here! [Plume]: https://github.com/Plume-org/Plume [Hagrid]: https://gitlab.com/hagrid-keyserver/hagrid/ [SourceGraph Syntax Highlighter]: https://github.com/sourcegraph/sourcegraph/tree/main/docker-images/syntax-highlighter -[Let us know]: https://github.com/SergioBenitez/Rocket/discussions/categories/show-and-tell +[Let us know]: https://github.com/rwf2/Rocket/discussions/categories/show-and-tell [Revolt]: https://github.com/revoltchat/backend
@@ -242,14 +242,14 @@ For example, work for Rocket v0.5 included: * [Reporting multiple](https://github.com/bikeshedder/deadpool/issues/114) [correctness issues](https://github.com/bikeshedder/deadpool/issues/113) in `deadpool`. * [Fixing a major usability issue in `async-stream`.](https://github.com/tokio-rs/async-stream/pull/57) - * [Creating a brand new configuration library.](https://github.com/SergioBenitez/Figment) + * [Creating a brand new configuration library.](https://github.com/rwf2/Figment) * [Updating](https://github.com/rousan/multer-rs/pull/21), [fixing](https://github.com/rousan/multer-rs/pull/29), and [maintaining](https://github.com/rousan/multer-rs/commit/2758e778e6aa2785b737c82fe45e58026bea2f01) `multer`. * [Significantly improving `async_trait` correctness and usability.](https://github.com/dtolnay/async-trait/pull/143) - * [Porting `Pattern` APIs to stable.](https://github.com/SergioBenitez/stable-pattern) - * [Porting macro diagnostics to stable.](https://github.com/SergioBenitez/proc-macro2-diagnostics) - * [Creating a brand new byte unit library.](https://github.com/SergioBenitez/ubyte) + * [Porting `Pattern` APIs to stable.](https://github.com/rwf2/stable-pattern) + * [Porting macro diagnostics to stable.](https://github.com/rwf2/proc-macro2-diagnostics) + * [Creating a brand new byte unit library.](https://github.com/rwf2/ubyte) * [Fixing a bug in `rustc`'s `libtest`.](https://github.com/rust-lang/rust/pull/78227) A version of Rocket is released whenever it is feature-complete and exceeds @@ -284,7 +284,7 @@ application. -[working on it]: https://github.com/SergioBenitez/Rocket/issues/90 +[working on it]: https://github.com/rwf2/Rocket/issues/90 [Server-Sent Events]: @api/rocket/response/stream/struct.EventStream.html [chat example]: @example/chat @@ -645,7 +645,7 @@ is to depend on a `contrib` library from git while also depending on a ```toml rocket = "0.6.0-dev" -rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" } +rocket_db_pools = { git = "https://github.com/rwf2/Rocket.git" } ``` This is _never_ correct. If libraries or applications interact via types from a diff --git a/site/guide/2-getting-started.md b/site/guide/2-getting-started.md index 5232ecabe3..defa1e5004 100644 --- a/site/guide/2-getting-started.md +++ b/site/guide/2-getting-started.md @@ -56,7 +56,7 @@ rocket = "0.6.0-dev" [dependencies] ` ` - rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "######" } + rocket = { git = "https://github.com/rwf2/Rocket", rev = "######" } ` Modify `src/main.rs` so that it contains the code for the Rocket `Hello, world!` diff --git a/site/news/2017-02-06-version-0.2.md b/site/news/2017-02-06-version-0.2.md index f657f9d82e..56bf2cdaac 100644 --- a/site/news/2017-02-06-version-0.2.md +++ b/site/news/2017-02-06-version-0.2.md @@ -9,7 +9,7 @@ Today marks the first major release since Rocket's debut a little over a month ago. Rocket v0.2 packs a ton of new features, fixes, and general improvements. Much of the development in v0.2 was led by the community, either through reports -via the [GitHub issue tracker](https://github.com/SergioBenitez/Rocket/issues) +via the [GitHub issue tracker](https://github.com/rwf2/Rocket/issues) or via direct contributions. In fact, there have been **20 unique contributors** to Rocket's codebase since Rocket's initial introduction! Community feedback has been incredible. As a special thank you, we include the names of these @@ -290,7 +290,7 @@ applications. Three bugs were fixed in this release: * Handlebars partials were not properly registered - ([#122](https://github.com/SergioBenitez/Rocket/issues/122)). + ([#122](https://github.com/rwf2/Rocket/issues/122)). * `Rocket::custom` did not set the custom configuration as the `active` configuration. * Route path segments with more than one dynamic parameter were erroneously @@ -319,7 +319,7 @@ In addition to new features, Rocket saw the following smaller improvements: Rocket v0.2 also includes all of the new features, bug fixes, and improvements from versions 0.1.1 through 0.1.6. You can read more about these changes in the [v0.1 -CHANGELOG](https://github.com/SergioBenitez/Rocket/blob/v0.1/CHANGELOG.md). +CHANGELOG](https://github.com/rwf2/Rocket/blob/v0.1/CHANGELOG.md). ## What's next? @@ -327,7 +327,7 @@ Work now begins on Rocket v0.3! The focus of the next major release will be on security. In particular, three major security features are planned: 1. **Automatic CSRF protection across all payload-based requests - ([#14](https://github.com/SergioBenitez/Rocket/issues/14)).** + ([#14](https://github.com/rwf2/Rocket/issues/14)).** Rocket will automatically check the origin of requests made for HTTP `PUT`, `POST`, `DELETE`, and `PATCH` requests, allowing only authorized requests to @@ -335,14 +335,14 @@ security. In particular, three major security features are planned: requests made via JavaScript. 2. **Encryption and signing of session-based cookies - ([#20](https://github.com/SergioBenitez/Rocket/issues/20)).** + ([#20](https://github.com/rwf2/Rocket/issues/20)).** Built-in session support will encrypt and sign cookies using a user supplied `session_key`. Encryption and signing will occur automatically for session-based cookies. 3. **Explicit typing of raw HTTP data strings - ([#43](https://github.com/SergioBenitez/Rocket/issues/43)).** + ([#43](https://github.com/rwf2/Rocket/issues/43)).** A present, the standard `&str` type is used to represent raw HTTP data strings. In the next release, a new type, `&RawStr`, will be used for this @@ -383,7 +383,7 @@ The following wonderful people helped make Rocket v0.2 happen: Thank you all! Your contributions are greatly appreciated! Looking to help with Rocket's development? Head over to [Rocket's -GitHub](https://github.com/SergioBenitez/Rocket#contributing) and start +GitHub](https://github.com/rwf2/Rocket#contributing) and start contributing! ## Start using Rocket today! diff --git a/site/news/2017-07-14-version-0.3.md b/site/news/2017-07-14-version-0.3.md index e422878289..99ba2f3c96 100644 --- a/site/news/2017-07-14-version-0.3.md +++ b/site/news/2017-07-14-version-0.3.md @@ -30,7 +30,7 @@ Rocket 0.3 is a _big_ release, packed with over 100 changes. We highlight the biggest new features here. For a complete description of everything new and different in 0.3, please see the [CHANGELOG]. -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.3.0/CHANGELOG.md#version-030-jul-14-2017 +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.3.0/CHANGELOG.md#version-030-jul-14-2017 ### Fairings @@ -238,7 +238,7 @@ following new features: This release includes many breaking changes such as support for `serde` 1.0. To keep this release note short, please see the -[CHANGELOG](https://github.com/SergioBenitez/Rocket/blob/v0.3.0/CHANGELOG.md#breaking-changes) +[CHANGELOG](https://github.com/rwf2/Rocket/blob/v0.3.0/CHANGELOG.md#breaking-changes) for the full list of breaking changes along with a short note about how to handle the breaking change in existing applications. @@ -277,7 +277,7 @@ Rocket 0.4, of course! The focus of the next major release is two-fold: security and usability. The following major features are planned: 1. **Automatic CSRF protection across all payload-based requests - ([#14](https://github.com/SergioBenitez/Rocket/issues/14)).** + ([#14](https://github.com/rwf2/Rocket/issues/14)).** This is a carry-over from the 0.3 wishlist. Rocket will automatically check the origin of requests made for HTTP `PUT`, `POST`, `DELETE`, and `PATCH` @@ -285,7 +285,7 @@ and usability. The following major features are planned: checking form submissions and requests made via JavaScript. 2. **First-class database support - ([#167](https://github.com/SergioBenitez/Rocket/issues/167)).** + ([#167](https://github.com/rwf2/Rocket/issues/167)).** Connecting a database to Rocket is presently [much wordier than necessary]. The plan for 0.4 is to minimize the amount of effort. At most, a couple of @@ -293,7 +293,7 @@ and usability. The following major features are planned: required. 3. **Typed URL generation from routes - ([#263](https://github.com/SergioBenitez/Rocket/issues/263)).** + ([#263](https://github.com/rwf2/Rocket/issues/263)).** Explicitly writing URLs is error-prone. Because routes are fully-typed in Rocket, it's possible to check that a URL corresponding to a route @@ -327,5 +327,5 @@ The following wonderful people helped make Rocket v0.3 happen: Thank you all! Your contributions are greatly appreciated! Looking to help with Rocket's development? Head over to [Rocket's -GitHub](https://github.com/SergioBenitez/Rocket#contributing) and start +GitHub](https://github.com/rwf2/Rocket#contributing) and start contributing! diff --git a/site/news/2018-10-31-version-0.4-rc.md b/site/news/2018-10-31-version-0.4-rc.md index 56e3c9b66c..83a227b12d 100644 --- a/site/news/2018-10-31-version-0.4-rc.md +++ b/site/news/2018-10-31-version-0.4-rc.md @@ -24,10 +24,10 @@ and [API docs], has been updated in full for v0.4. We're excited for your feedback, and we look forward to seeing you again on Friday, November 9th for the general release! -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues [API docs]: https://api.rocket.rs/v0.4/rocket/ [guide]: @guide-v0.4 -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/tree/v0.4/CHANGELOG.md#version-040-rc-oct-31-2018 +[CHANGELOG]: https://github.com/rwf2/Rocket/tree/v0.4/CHANGELOG.md#version-040-rc-oct-31-2018 ## About Rocket diff --git a/site/news/2018-11-30-version-0.4-rc-2.md b/site/news/2018-11-30-version-0.4-rc-2.md index 5474e2a705..b081da4df9 100644 --- a/site/news/2018-11-30-version-0.4-rc-2.md +++ b/site/news/2018-11-30-version-0.4-rc-2.md @@ -31,10 +31,10 @@ in full for the second release candidate. We're excited for your feedback, and we look forward to seeing you again on Wednesday, December 5th for the general release! -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues [API docs]: @api-v0.4 [guide]: @guide-v0.4 -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/tree/v0.4/CHANGELOG.md#version-040-rc2-nov-30-2018 +[CHANGELOG]: https://github.com/rwf2/Rocket/tree/v0.4/CHANGELOG.md#version-040-rc2-nov-30-2018 ## About Rocket diff --git a/site/news/2018-12-08-version-0.4.md b/site/news/2018-12-08-version-0.4.md index 4b53a13b1d..c2f88206ae 100644 --- a/site/news/2018-12-08-version-0.4.md +++ b/site/news/2018-12-08-version-0.4.md @@ -32,7 +32,7 @@ Rocket 0.4 is the largest release to date by a _wide_ margin. It is packed with hundreds of changes. We highlight the largest of them here. For a complete description of everything new and different in 0.4, please see the [CHANGELOG]. -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.4.0/CHANGELOG.md#version-040-dec-06-2018 +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.4.0/CHANGELOG.md#version-040-dec-06-2018 ### Maintainers += 1 @@ -245,7 +245,7 @@ section and the updated [`route` attribute] documentation. [`FromQuery`]: @api-v0.4/rocket/request/trait.FromQuery.html [`route` attribute]: @api-v0.4/rocket_codegen/attr.get.html [Query Strings]: @guide-v0.4/requests/#query-strings -[#608]: https://github.com/SergioBenitez/Rocket/issues/608 +[#608]: https://github.com/rwf2/Rocket/issues/608 ### Stateful Handlers @@ -399,7 +399,7 @@ following new features: ## Breaking Changes This release includes many breaking changes. Please see the -[CHANGELOG](https://github.com/SergioBenitez/Rocket/blob/v0.3.0/CHANGELOG.md#breaking-changes) +[CHANGELOG](https://github.com/rwf2/Rocket/blob/v0.3.0/CHANGELOG.md#breaking-changes) for a complete list of breaking changes along with details on handling the breaking change in existing applications. @@ -513,10 +513,10 @@ in mind, the roadmap for 0.5 includes: will protect against CSRF using more robust techniques. Rocket will also add support for automatic, browser-based XSS protection. -[#17]: https://github.com/SergioBenitez/Rocket/issues/17 -[#19]: https://github.com/SergioBenitez/Rocket/issues/19 -[#106]: https://github.com/SergioBenitez/Rocket/issues/106 -[#14]: https://github.com/SergioBenitez/Rocket/issues/14 +[#17]: https://github.com/rwf2/Rocket/issues/17 +[#19]: https://github.com/rwf2/Rocket/issues/19 +[#106]: https://github.com/rwf2/Rocket/issues/106 +[#14]: https://github.com/rwf2/Rocket/issues/14 ## Rocket v0.4 Contributors @@ -570,5 +570,5 @@ The following wonderful people helped make Rocket 0.4 happen: Thank you all! Your contributions are **greatly** appreciated! Looking to help with Rocket's development? Head over to [Rocket's -GitHub](https://github.com/SergioBenitez/Rocket#contributing) and start +GitHub](https://github.com/rwf2/Rocket#contributing) and start contributing! diff --git a/site/news/2021-06-09-version-0.5-rc.1.md b/site/news/2021-06-09-version-0.5-rc.1.md index d61c1bd62b..96de21614b 100644 --- a/site/news/2021-06-09-version-0.5-rc.1.md +++ b/site/news/2021-06-09-version-0.5-rc.1.md @@ -25,9 +25,9 @@ v0.4. We hope you enjoy Rocket v0.5 as much as we enjoyed creating it. We look forward to your feedback! -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.1/CHANGELOG.md +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.5.0-rc.1/CHANGELOG.md [API docs]: @api-v0.5 [guide]: @guide-v0.5 diff --git a/site/news/2022-05-09-version-0.5-rc.2.md b/site/news/2022-05-09-version-0.5-rc.2.md index 7456f3e19a..1c85fdaddd 100644 --- a/site/news/2022-05-09-version-0.5-rc.2.md +++ b/site/news/2022-05-09-version-0.5-rc.2.md @@ -24,10 +24,10 @@ has been updated in full for the second release candidate. We're excited for your feedback, and we look forward to seeing you again soon for the general release! -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions [migration guide]: @guide-v0.5/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.2/CHANGELOG.md +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.5.0-rc.2/CHANGELOG.md [API docs]: @api-v0.5 [guide]: @guide-v0.5 diff --git a/site/news/2023-03-23-version-0.5-rc.3.md b/site/news/2023-03-23-version-0.5-rc.3.md index 6501529add..46ebd8982e 100644 --- a/site/news/2023-03-23-version-0.5-rc.3.md +++ b/site/news/2023-03-23-version-0.5-rc.3.md @@ -14,10 +14,10 @@ release candidate and report any issues to the [GitHub issue tracker]. Please see the new [migration guide] for complete details on how to upgrade from Rocket v0.4. For changes since Rocket v0.5.0-rc.2, please see the [CHANGELOG]. -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions [migration guide]: @guide-v0.5/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.3/CHANGELOG.md +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.5.0-rc.3/CHANGELOG.md ## About Rocket diff --git a/site/news/2023-11-01-version-0.5-rc.4.md b/site/news/2023-11-01-version-0.5-rc.4.md index ecd81614ea..3a3bc85e50 100644 --- a/site/news/2023-11-01-version-0.5-rc.4.md +++ b/site/news/2023-11-01-version-0.5-rc.4.md @@ -14,10 +14,10 @@ release candidate and report any issues to the [GitHub issue tracker]. Please see the new [migration guide] for complete details on how to upgrade from Rocket v0.4. For changes since Rocket v0.5.0-rc.3, please see the [CHANGELOG]. -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions [migration guide]: @guide-v0.5/upgrading -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0-rc.4/CHANGELOG.md +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.5.0-rc.4/CHANGELOG.md ## About Rocket diff --git a/site/news/2023-11-17-version-0.5.md b/site/news/2023-11-17-version-0.5.md index cf66c5ccd8..202b0ef2f8 100644 --- a/site/news/2023-11-17-version-0.5.md +++ b/site/news/2023-11-17-version-0.5.md @@ -36,7 +36,7 @@ ecosystem]. The changes are numerous, so we focus on the most noteworthy changes here and encourage everyone to read the [CHANGELOG] for a complete list. For answers to frequently asked questions, see the new [FAQ]. -[CHANGELOG]: https://github.com/SergioBenitez/Rocket/blob/v0.5.0/CHANGELOG.md +[CHANGELOG]: https://github.com/rwf2/Rocket/blob/v0.5.0/CHANGELOG.md [broader ecosystem]: @guide-v0.5/faq/#releases [FAQ]: @guide-v0.5/faq @@ -362,7 +362,7 @@ release: currently calls out the attribute for each affected trait, as well as offer modest performance improvements. - 0. [**Typed Catchers**](https://github.com/SergioBenitez/Rocket/issues/749) + 0. [**Typed Catchers**](https://github.com/rwf2/Rocket/issues/749) Today's catchers cannot receive strictly typed error data. This results in workarounds where error data is queried for well-typedness at runtime. @@ -436,7 +436,7 @@ release: the next release, we seek to extend this approach. [an interface for implementing and plugging-in custom connection listeners]: -https://github.com/SergioBenitez/Rocket/issues/1070#issuecomment-1491101952 +https://github.com/rwf2/Rocket/issues/1070#issuecomment-1491101952 [stabilization of `async fn` in traits]: https://github.com/rust-lang/rust/pull/115822 [poor benchmarking]: @guide-v0.5/faq/#performance @@ -450,7 +450,7 @@ leading the migration to `async` and Rust stable along with tireless efforts to improve Rocket's documentation and address the community. Rocket is better for having had Jeb along for the ride. Thank you, Jeb. -[Jeb Rosen]: https://github.com/SergioBenitez/Rocket/commits?author=jebrosen +[Jeb Rosen]: https://github.com/rwf2/Rocket/commits?author=jebrosen A special thank you to all of Rocket's users, especially those who diligently waded through all four release candidates, raised issues, and participated on @@ -610,7 +610,7 @@ A heartfelt _thank you_ as well to _all_ **148** who contributed to Rocket v0.5:
  • Yusuke Kominami
  • -[GitHub discussions]: https://github.com/SergioBenitez/Rocket/discussions +[GitHub discussions]: https://github.com/rwf2/Rocket/discussions [Matrix channel]: https://chat.mozilla.org/#/room/#rocket:mozilla.org ## Get Involved @@ -618,5 +618,5 @@ A heartfelt _thank you_ as well to _all_ **148** who contributed to Rocket v0.5: Looking to help with Rocket? To contribute code, head over to [GitHub]. To get involved with the project, see the [RWF2 prelaunch announcement]. We'd love to have you. -[GitHub]: https://github.com/SergioBenitez/Rocket +[GitHub]: https://github.com/rwf2/Rocket [RWF2 prelaunch announcement]: ../2023-11-17-rwf2-prelaunch/ diff --git a/site/news/index.toml b/site/news/index.toml index b1db1704d1..283194d2c1 100644 --- a/site/news/index.toml +++ b/site/news/index.toml @@ -122,7 +122,7 @@ opportunity to discover issues with Rocket v0.4 and its documentation before its general release. We encourage all users to migrate their applications to the second release candidate and report any issues to the [GitHub issue tracker]. -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues """ [[articles]] @@ -142,7 +142,7 @@ its documentation before its general release. We encourage all users to migrate their applications to the release candidate and report any issues to the [GitHub issue tracker]. -[GitHub issue tracker]: https://github.com/SergioBenitez/Rocket/issues +[GitHub issue tracker]: https://github.com/rwf2/Rocket/issues """ [[articles]] @@ -172,7 +172,7 @@ snippet = """ Today marks the first major release since Rocket's debut a little over a month ago. Rocket v0.2 packs a ton of new features, fixes, and general improvements. Much of the development in v0.2 was led by the community, either through reports -via the [GitHub issue tracker](https://github.com/SergioBenitez/Rocket/issues) +via the [GitHub issue tracker](https://github.com/rwf2/Rocket/issues) or via direct contributions. In fact, there have been **20 unique contributors** to Rocket's codebase since Rocket's initial introduction! Community feedback has been incredible. As a special thank you, we include the names of these From 2b8d6d3f06d8b1f330b6cbcf3bd821ec370a660c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 22 Nov 2023 21:38:53 +0100 Subject: [PATCH 042/178] Update data for site refresh. --- site/guide/5-responses.md | 38 ++++++ site/index.toml | 267 +++++++++++++++++++++++++++++++++----- site/overview.toml | 160 ----------------------- 3 files changed, 274 insertions(+), 191 deletions(-) diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 831b31ac8f..8a1fc468e6 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -421,6 +421,44 @@ how to detect and handle graceful shutdown requests. [`EventStream`]: @api/rocket/response/stream/struct.EventStream.html [`chat` example]: @example/chat +### WebSockets + +Enabled by Rocket's support for [HTTP connection upgrades], the official +[`rocket_ws`] crate implements first-class support for WebSockets. Working with +`rocket_ws` to implement an echo server looks like this: + +```rust +# use rocket::get; +use rocket_ws::{WebSocket, Stream}; + +#[get("/echo")] +fn echo_compose(ws: WebSocket) -> Stream!['static] { + ws.stream(|io| io) +} +``` + +As with `async` streams, `rocket_ws` also supports using generator syntax for +WebSocket messages: + +```rust +# use rocket::get; +use rocket_ws::{WebSocket, Stream}; + +#[get("/echo")] +fn echo_stream(ws: WebSocket) -> Stream!['static] { + Stream! { ws => + for await message in ws { + yield message?; + } + } +} +``` + +For complete usage details, see the [`rocket_ws`] documentation. + +[HTTP connection upgrades]: @api-v0.5/rocket/response/struct.Response.html#upgrading +[`rocket_ws`]: @api-v0.5/rocket_ws + ### JSON The [`Json`] responder in allows you to easily respond with well-formed JSON diff --git a/site/index.toml b/site/index.toml index b722b5c6d9..f3e1db7795 100644 --- a/site/index.toml +++ b/site/index.toml @@ -12,14 +12,14 @@ date = "Nov 17, 2023" [[top_features]] title = "Type Safe" -text = "From request to response Rocket ensures that your types mean something." +text = "Type safety turned up to 11 means security and robustness come at compile-time." image = "helmet" button = "Learn More" url = "overview/#how-rocket-works" [[top_features]] title = "Boilerplate Free" -text = "Spend your time writing code that really matters, and let Rocket generate the rest." +text = "Spend your time writing code that really matters and let Rocket handle the rest." image = "robot-free" button = "See Examples" url = "overview/#anatomy-of-a-rocket-application" @@ -66,7 +66,7 @@ text = ''' Hello, 58 year old named John! If someone visits a path with an `` that isnโ€™t a `u8`, Rocket doesnโ€™t - blindly call `hello`. Instead, it tries other matching routes or returns a + just call `hello`. Instead, it tries other matching routes or returns a **404**. ''' @@ -91,14 +91,14 @@ text = ''' `data` parameter to a `Form` type. Rocket automatically **parses and validates** the form data into your structure and calls your function. + File uploads? A breeze with [`TempFile`](@api/rocket/fs/enum.TempFile.html). Bad form request? Rocket doesnโ€™t call your function! Need to know what went wrong? Use a `data` parameter of `Result`! Want to rerender the form with user - input and errors? Use [`Context`](guide/requests/#context)! File uploads? A - breeze with [`TempFile`](@api/rocket/fs/enum.TempFile.html). + input and errors? Use [`Context`](guide/requests/#context)! ''' [[sections]] -title = "JSON, out of the box." +title = "JSON, always on." code = ''' #[derive(Serialize, Deserialize)] struct Message<'r> { @@ -145,11 +145,11 @@ text = "View, add, or remove cookies, with or without encryption, without hassle image = 'cookies-icon' url = 'guide/requests/#cookies' button = 'Learn More' -color = 'purple' +color = 'fucsia' margin = -6 [[bottom_features]] -title = 'Async Streams' +title = 'WebSockets + Streams' text = "Create and return potentially infinite async streams of data with ease." image = 'streams-icon' url = 'guide/responses/#async-streams' @@ -167,35 +167,240 @@ color = 'yellow' margin = -3 [[bottom_features]] -title = 'Testing Library' -text = "Unit test your applications with ease using the built-in testing library." -image = 'testing-icon' -url = 'guide/testing#testing' +title = 'Type-Checked URIs' +text = "Never mistype or forget to update a URI again with Rocket's typed URIs." +image = 'pencil-icon' +url = 'guide/requests/#private-cookies' button = 'Learn More' color = 'orange' +height = '60px' +margin = -3 [[bottom_features]] -title = 'Typed URIs' -text = "Rocket typechecks route URIs for you so you never mistype a URI again." +title = 'Structured Middleware' +text = "Fairings are Rocket's simpler approach to structured middleware." image = 'ship-icon' -url = 'guide/responses/#typed-uris' +url = 'guide/fairings/#fairings' button = 'Learn More' color = 'green' margin = -20 -# [[bottom_features]] -# title = 'Query Strings' -# text = "Handling query strings and parameters is type-safe and easy in Rocket." -# image = 'query-icon' -# url = 'guide/requests/#query-strings' -# button = 'Learn More' -# color = 'red' -# margin = -3 - -# [[bottom_features]] -# title = 'Private Cookies' -# text = "Safe, secure, private cookies are built-in so your users can stay safe." -# image = 'sessions-icon' -# url = 'guide/requests/#private-cookies' -# button = 'Learn More' -# color = 'purple' +[[bottom_features]] +title = 'Database Support' +text = "Store data with ease with Rocket's built-in ORM agnostic database support." +image = 'query-icon' +url = 'guide/state/#databases' +button = 'Learn More' +color = 'pink' +margin = -3 + +[[bottom_features]] +title = 'Testing' +text = "Unit and integration test using the comprehensive, built-in testing library." +image = 'testing-icon' +url = 'guide/testing#testing' +button = 'Learn More' +color = 'aqua' + +[[bottom_features]] +title = 'Community' +text = "Join an extensive community of 20,000+ Rocketeers that love Rocket." +image = 'globe' +url = 'https://github.com/rwf2/Rocket/network/dependents' +button = 'See Dependents' +color = 'purple' +height = '62px' + +############################################################################### +# Panels: displayed in a tabbed arrangement. +############################################################################### + +[[panels]] +name = "Routing" +checked = true +content = ''' +Rocket's main task is to route incoming requests to the appropriate request +handler using your application's declared routes. Routes are declared using +Rocket's _route_ attributes. The attribute describes the requests that match the +route. The attribute is placed on top of a function that is the request handler +for that route. + +As an example, consider the simple route below: + +```rust +#[get("/")] +fn index() -> &'static str { + "Hello, world!" +} +``` + +This `index` route matches any incoming HTTP `GET` request to `/`, the index. +The handler returns a `String`. Rocket automatically converts the string into a +well-formed HTTP response that includes the appropriate `Content-Type` and body +encoding metadata. +''' + +[[panels]] +name = "Dynamic Params" +content = ''' +Rocket automatically parses dynamic data in path segments into any desired type. +To illustrate, let's use the following route: + +```rust +#[get("/hello//")] +fn hello(name: &str, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) +} +``` + +This `hello` route has two dynamic parameters, identified with angle brackets, +declared in the route URI: `` and ``. Rocket maps each parameter to +an identically named function argument: `name: &str` and `age: u8`. The dynamic +data in the incoming request is parsed automatically into a value of the +argument's type. The route is called only when parsing succeeds. + +Parsing is directed by the +[`FromParam`](@api/rocket/request/trait.FromParam.html) trait. Rocket implements +`FromParam` for many standard types, including both `&str` and `u8`. You can +implement it for your own types, too! +''' + +[[panels]] +name = "Handling Data" +content = ''' +Rocket can automatically parse body data, too! + +```rust +#[post("/login", data = "")] +fn login(login: Form) -> String { + format!("Hello, {}!", login.name) +} +``` + +The dynamic parameter declared in the `data` route attribute parameter again +maps to a function argument. Here, `login` maps to `login: Form`. +Parsing is again trait-directed, this time by the +[`FromData`](@api/rocket/data/trait.FromData.html) trait. + +The [`Form`](@api/rocket/form/struct.Form.html) type is Rocket's [robust form +data parser](@guide/requests/#forms). It automatically parses the request body into the internal type, +here `UserLogin`. Other built-in `FromData` types include +[`Data`](@api/rocket/struct.Data.html), +[`Json`](@api/rocket/serde/json/struct.Json.html), and +[`MsgPack`](@api/rocket/serde/msgpack/struct.MsgPack.html). As always, you can +implement `FromData` for your own types, too! +''' + +[[panels]] +name = "Request Guards" +content = ''' +In addition to dynamic path and data parameters, request handlers can also +contain a third type of parameter: _request guards_. Request guards aren't +declared in the route attribute, and any number of them can appear in the +request handler signature. + +Request guards _protect_ the handler from running unless some set of conditions +are met by the incoming request metadata. For instance, if you are writing an +API that requires sensitive calls to be accompanied by an API key in the request +header, Rocket can protect those calls via a custom `ApiKey` request guard: + +```rust +#[get("/sensitive")] +fn sensitive(key: ApiKey) { ... } +``` + +`ApiKey` protects the `sensitive` handler from running incorrectly. In order for +Rocket to call the `sensitive` handler, the `ApiKey` type needs to be derived +through a [`FromRequest`](@api/rocket/request/trait.FromRequest.html) +implementation, which in this case, validates the API key header. Request guards +are a powerful and unique Rocket concept; they centralize application policy and +invariants through types. +''' + +[[panels]] +name = "Responders" +content = ''' +The return type of a request handler can be any type that implements +[`Responder`](@api/rocket/response/trait.Responder.html): + +```rust +#[get("/")] +fn route() -> T { ... } +``` + +Above, T must implement `Responder`. Rocket implements `Responder` for many of +the standard library types including `&str`, `String`, `File`, `Option`, and +`Result`. Rocket also implements custom responders such as +[`Redirect`](@api/rocket/response/struct.Redirect.html), +[`Flash`](@api/rocket/response/struct.Flash.html), and +[`Template`](@api/rocket_dyn_templates/struct.Template.html). + +The task of a `Responder` is to generate a +[`Response`](@api/rocket/response/struct.Response.html), if possible. +`Responder`s can fail with a status code. When they do, Rocket calls the +corresponding error catcher, a `catch` route, which can be declared as follows: + +```rust +#[catch(404)] +fn not_found() -> T { ... } +``` +''' + +[[panels]] +name = "Launching" +content = ''' +Finally, we get to launch our application! Rocket begins dispatching requests to +routes after they've been _mounted_ and the application has been _launched_. +These two steps, usually wrtten in a `rocket` function, look like: + +```rust +#[launch] +fn rocket() -> _ { + rocket::build().mount("/base", routes![index, another]) +} +``` + +The `mount` call takes a _base_ and a set of routes via the `routes!` macro. The +base path (`/base` above) is prepended to the path of every route in the list, +effectively namespacing the routes. `#[launch]` creates a `main` function that +starts the server. In development, Rocket prints useful information to the +console to let you know everything is okay. + +```sh +๐Ÿš€ Rocket has launched from http://127.0.0.1:8000 +``` +''' + +############################################################################### +# Sponsors +############################################################################### + +[sponsors.diamond] +name = "๐Ÿ’Ž Diamond" +tag = "$500/month" +color = "#addcde" +height = "70px" + +[sponsors.gold] +name = "๐Ÿ’› Gold" +tag = "$250/month" +color = "#fffbba" +height = "55px" + +[[sponsors.gold.sponsors]] +name = "ohne-makler" +url = "https://www.ohne-makler.net/" +img = "ohne-makler.svg" + +[[sponsors.gold.sponsors]] +name = "RWF2: Rocket Web Framework Foundation" +url = "https://rwf2.org" +img = "rwf2.gif" +width = "55px" +height = "55px" + +[sponsors.bronze] +name = "๐ŸคŽ Bronze" +tag = "$50/month" +color = "#c5a587" +height = "30px" diff --git a/site/overview.toml b/site/overview.toml index c37384c391..204c367109 100644 --- a/site/overview.toml +++ b/site/overview.toml @@ -1,163 +1,3 @@ -############################################################################### -# Panels: displayed in a tabbed arrangement. -############################################################################### - -[[panels]] -name = "Routing" -checked = true -content = ''' -Rocket's main task is to route incoming requests to the appropriate request -handler using your application's declared routes. Routes are declared using -Rocket's _route_ attributes. The attribute describes the requests that match the -route. The attribute is placed on top of a function that is the request handler -for that route. - -As an example, consider the simple route below: - -```rust -#[get("/")] -fn index() -> &'static str { - "Hello, world!" -} -``` - -This `index` route matches any incoming HTTP `GET` request to `/`, the index. -The handler returns a `String`. Rocket automatically converts the string into a -well-formed HTTP response that includes the appropriate `Content-Type` and body -encoding metadata. -''' - -[[panels]] -name = "Dynamic Params" -content = ''' -Rocket automatically parses dynamic data in path segments into any desired type. -To illustrate, let's use the following route: - -```rust -#[get("/hello//")] -fn hello(name: &str, age: u8) -> String { - format!("Hello, {} year old named {}!", age, name) -} -``` - -This `hello` route has two dynamic parameters, identified with angle brackets, -declared in the route URI: `` and ``. Rocket maps each parameter to -an identically named function argument: `name: &str` and `age: u8`. The dynamic -data in the incoming request is parsed automatically into a value of the -argument's type. The route is called only when parsing succeeds. - -Parsing is directed by the -[`FromParam`](@api/rocket/request/trait.FromParam.html) trait. Rocket implements -`FromParam` for many standard types, including both `&str` and `u8`. You can -implement it for your own types, too! -''' - -[[panels]] -name = "Handling Data" -content = ''' -Rocket can automatically parse body data, too! - -```rust -#[post("/login", data = "")] -fn login(login: Form) -> String { - format!("Hello, {}!", login.name) -} -``` - -The dynamic parameter declared in the `data` route attribute parameter again -maps to a function argument. Here, `login` maps to `login: Form`. -Parsing is again trait-directed, this time by the -[`FromData`](@api/rocket/data/trait.FromData.html) trait. - -The [`Form`](@api/rocket/form/struct.Form.html) type is Rocket's [robust form -data parser](@guide/requests/#forms). It automatically parses the request body into the internal type, -here `UserLogin`. Other built-in `FromData` types include -[`Data`](@api/rocket/struct.Data.html), -[`Json`](@api/rocket/serde/json/struct.Json.html), and -[`MsgPack`](@api/rocket/serde/msgpack/struct.MsgPack.html). As always, you can -implement `FromData` for your own types, too! -''' - -[[panels]] -name = "Request Guards" -content = ''' -In addition to dynamic path and data parameters, request handlers can also -contain a third type of parameter: _request guards_. Request guards aren't -declared in the route attribute, and any number of them can appear in the -request handler signature. - -Request guards _protect_ the handler from running unless some set of conditions -are met by the incoming request metadata. For instance, if you are writing an -API that requires sensitive calls to be accompanied by an API key in the request -header, Rocket can protect those calls via a custom `ApiKey` request guard: - -```rust -#[get("/sensitive")] -fn sensitive(key: ApiKey) { ... } -``` - -`ApiKey` protects the `sensitive` handler from running incorrectly. In order for -Rocket to call the `sensitive` handler, the `ApiKey` type needs to be derived -through a [`FromRequest`](@api/rocket/request/trait.FromRequest.html) -implementation, which in this case, validates the API key header. Request guards -are a powerful and unique Rocket concept; they centralize application policy and -invariants through types. -''' - -[[panels]] -name = "Responders" -content = ''' -The return type of a request handler can be any type that implements -[`Responder`](@api/rocket/response/trait.Responder.html): - -```rust -#[get("/")] -fn route() -> T { ... } -``` - -Above, T must implement `Responder`. Rocket implements `Responder` for many of -the standard library types including `&str`, `String`, `File`, `Option`, and -`Result`. Rocket also implements custom responders such as -[`Redirect`](@api/rocket/response/struct.Redirect.html), -[`Flash`](@api/rocket/response/struct.Flash.html), and -[`Template`](@api/rocket_dyn_templates/struct.Template.html). - -The task of a `Responder` is to generate a -[`Response`](@api/rocket/response/struct.Response.html), if possible. -`Responder`s can fail with a status code. When they do, Rocket calls the -corresponding error catcher, a `catch` route, which can be declared as follows: - -```rust -#[catch(404)] -fn not_found() -> T { ... } -``` -''' - -[[panels]] -name = "Launching" -content = ''' -Finally, we get to launch our application! Rocket begins dispatching requests to -routes after they've been _mounted_ and the application has been _launched_. -These two steps, usually wrtten in a `rocket` function, look like: - -```rust -#[launch] -fn rocket() -> _ { - rocket::build().mount("/base", routes![index, another]) -} -``` - -The `mount` call takes a _base_ and a set of routes via the `routes!` macro. The -base path (`/base` above) is prepended to the path of every route in the list, -effectively namespacing the routes. `#[launch]` creates a `main` function that -starts the server. In development, Rocket prints useful information to the -console to let you know everything is okay. - -```sh -๐Ÿš€ Rocket has launched from http://127.0.0.1:8000 -``` -''' - ############################################################################### # Steps to "How Rocket Works" ############################################################################### From 7f7d352e453e83f3d23ee12f8965ce75c977fcea Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 28 Nov 2023 10:22:22 +0100 Subject: [PATCH 043/178] Add '.rustfmt.toml' disabling formatting. --- .rustfmt.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000000..c7ad93bafe --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +disable_all_formatting = true From 8d9dfcecad9df6e1d2b9345f91f5d02e8eecfa06 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 5 Dec 2023 17:29:30 -0800 Subject: [PATCH 044/178] Silence more warnings in generated code. Resolves #2655. --- core/codegen/src/attribute/entry/launch.rs | 7 ++++--- core/codegen/src/derive/from_form.rs | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/core/codegen/src/attribute/entry/launch.rs b/core/codegen/src/attribute/entry/launch.rs index 90f5d6129d..122892198e 100644 --- a/core/codegen/src/attribute/entry/launch.rs +++ b/core/codegen/src/attribute/entry/launch.rs @@ -1,9 +1,10 @@ -use super::EntryAttr; - use devise::{Spanned, Result}; use devise::ext::SpanDiagnosticExt; use proc_macro2::{TokenStream, Span}; +use super::EntryAttr; +use crate::exports::mixed; + /// `#[rocket::launch]`: generates a `main` function that calls the attributed /// function to generate a `Rocket` instance. Then calls `.launch()` on the /// returned instance inside of an `rocket::async_main`. @@ -78,7 +79,7 @@ impl EntryAttr for Launch { }; let block = &f.block; - let rocket = quote_spanned!(ty.span() => { + let rocket = quote_spanned!(mixed(ty.span()) => { let ___rocket: #ty = #block; let ___rocket: ::rocket::Rocket<::rocket::Build> = ___rocket; ___rocket diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 19c4cc0267..bf55d3bbe1 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -120,12 +120,13 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { let (ctxt_ty, gen) = context_type(input)?; let (impl_gen, _, where_clause) = gen.split_for_impl(); let output = mapper::input_default(mapper, input)?; - Ok(quote_spanned! { input.span() => + Ok(quote_spanned! { mixed(input.span()) => /// Rocket generated FormForm context. #[doc(hidden)] #[allow(unknown_lints)] #[allow(renamed_and_removed_lints)] - #[allow(private_in_public, private_bounds)] + #[allow(private_in_public)] + #[allow(private_bounds)] #vis struct #ctxt_ty #impl_gen #where_clause { __opts: #_form::Options, __errors: #_form::Errors<'r>, @@ -150,6 +151,7 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { #[allow(unused_imports)] use #_http::uncased::AsUncased; }) + .outer_mapper(quote!(#[allow(clippy::all, clippy::pedantic, clippy::nursery)])) .outer_mapper(quote!(#[allow(renamed_and_removed_lints)])) .outer_mapper(quote!(#[allow(private_in_public)])) .outer_mapper(quote!(#[rocket::async_trait])) @@ -205,8 +207,8 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { // Without the `let _fut`, we get a wild lifetime error. It don't // make no sense, Rust async/await: it don't make no sense. .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { - let _fut = <#ty as #_form::FromForm<'r>>::push_data(#ctxt, __f.shift()); - _fut.await; + let __fut = <#ty as #_form::FromForm<'r>>::push_data(#ctxt, __f.shift()); + __fut.await; }))) ) .inner_mapper(MapperBuild::new() @@ -261,7 +263,6 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { <#ty as #_form::FromForm<'r>>::default(__opts) })); - let _err = _Err; Ok(quote_spanned! { ty.span() => { let __opts = __c.__opts; let __name = #name_buf_opt; @@ -271,8 +272,8 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { <#ty as #_form::FromForm<'r>>::finalize ) .map_err(|__e| match __name { - Some(__name) => __e.with_name(__name), - None => __e, + #_Some(__name) => __e.with_name(__name), + #_None => __e, }) .map_err(|__e| __e.is_empty() .then(|| #_form::ErrorKind::Unknown.into()) From 93591a87a331a3df46763a16cf91b837d80730cd Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 6 Dec 2023 16:47:10 -0800 Subject: [PATCH 045/178] Upgrade to GitHub issue forms. --- .github/ISSUE_TEMPLATE/bug-report.md | 28 ----- .github/ISSUE_TEMPLATE/bug-report.yml | 133 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 17 ++- .github/ISSUE_TEMPLATE/doc-problem.yml | 56 +++++++++ .github/ISSUE_TEMPLATE/feature-request.md | 31 ----- .github/ISSUE_TEMPLATE/feature-request.yml | 73 +++++++++++ .github/ISSUE_TEMPLATE/suggestion.md | 26 ---- .github/ISSUE_TEMPLATE/suggestion.yml | 57 +++++++++ 8 files changed, 332 insertions(+), 89 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/doc-problem.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml delete mode 100644 .github/ISSUE_TEMPLATE/suggestion.md create mode 100644 .github/ISSUE_TEMPLATE/suggestion.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 35055bb14d..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Bug Report -about: Report behavior that deviates from specification or expectation -title: '' -labels: triage -assignees: '' ---- - -**Description** - -A clear and concise description of what the bug is. This should include links to documentation that is contradicted or an explanation of why the present functionality doesn't match your expectation. - -**To Reproduce** - -How to reproduce the bug. A fully working code example (`main.rs` + `Cargo.toml`) is preferred. - -**Expected Behavior** - -A clear and concise description of what you expected to happen. - -**Environment:** - - - OS Distribution and Kernel: [e.g. Arch Linux 4.16.13, macOS 11.2.1] - - Rocket Version: [e.g. 0.4.12, master@abf996b] - -**Additional Context** - -Add any other context about the problem here, for example, how you uncovered the bug or if you have ideas about what/where Rocket is going wrong. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000000..f3e91bb9f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,133 @@ +name: Bug Report +description: Report a functionality issue that deviates from the documentation. +labels: ["triage"] +body: + - type: markdown + attributes: + value: > + **Thanks for taking the time to fill out this bug report!** Your report + helps make Rocket better. + + + Please only report issues related to _functionality_ that deviates from + published specification or reasonable expectation. Do not report issues + with documentation, infrastructure, or anything unrelated to functional + correctness here. + - type: input + attributes: + label: Rocket Version + description: > + Enter the exact version of Rocket (x.y.z) or git shorthash (8d9dfce) you're using. + + + Please ensure you're using the latest release before reporting a bug. + placeholder: "ex: 0.5.0" + validations: + required: true + - type: input + attributes: + label: Operating System + description: Which operating system and version are you running? + placeholder: "examples: macOS 13.6.2, Arch Linux 4.16.13" + validations: + required: true + - type: input + attributes: + label: Rust Toolchain Version + description: Which version of `rustc` are you using? (`rustc --version`) + placeholder: "ex: rustc 1.74.0 (79e9716c9 2023-11-13)" + validations: + required: true + - type: textarea + attributes: + label: What happened? + description: Provide a brief overview of what went wrong. + validations: + required: true + - type: textarea + attributes: + label: Test Case + description: > + Provide a Rocket application that elicits the bug. Ideally the program + contains a `#[test]` case using Rocket's + [`local`](https://api.rocket.rs/v0.5/rocket/local/index.html) testing + module. + placeholder: > + #[macro_use] extern crate rocket; + + + #[launch] + + fn rocket() -> _ { + rocket::build() + } + + + #[test] + + fn failing_test() { + use rocket::local::blocking::Client; + + let client = Client::tracked(rocket()).unwrap(); + let response = client.get("/").dispatch(); + assert!(response.status().class().is_success()); + } + render: rust + validations: + required: true + - type: textarea + attributes: + label: Log Output + description: > + Please provide the complete log output captured with + `ROCKET_LOG_LEVEL=debug` when the test case is run. + placeholder: > + โฏ ROCKET_LOG_LEVEL=debug cargo test + + running 1 test + + test failing_test ... FAILED + + failures: + + + ---- failing_test stdout ---- + + -- configuration trace information -- + >> "address" parameter source: rocket::Config::default() + >> "port" parameter source: rocket::Config::default() + >> "workers" parameter source: rocket::Config::default() + >> "max_blocking" parameter source: rocket::Config::default() + >> "keep_alive" parameter source: rocket::Config::default() + >> "ident" parameter source: rocket::Config::default() + >> "ip_header" parameter source: rocket::Config::default() + >> "limits" parameter source: rocket::Config::default() + >> "temp_dir" parameter source: rocket::Config::default() + >> "log_level" parameter source: `ROCKET_` environment variable(s) + >> "shutdown" parameter source: rocket::Config::default() + >> "cli_colors" parameter source: rocket::Config::default() + ๐Ÿ”ง Configured for debug. + >> address: 127.0.0.1 + >> port: 8000 + [...] + render: shell + validations: + required: true + - type: textarea + attributes: + label: Additional Context + description: > + Feel free to provide any additional context for your bug report. + - type: checkboxes + attributes: + label: System Checks + description: "Please confirm all of the following:" + options: + - label: My bug report relates to functionality. + required: true + - label: I have tested against the latest Rocket release or a recent git commit. + required: true + - label: I have tested against the latest stable `rustc` toolchain. + required: true + - label: I was unable to find this issue previously reported. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 506d5a1dc4..b6cbda93e4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,14 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - - name: Question - url: https://github.com/SergioBenitez/Rocket/discussions - about: Please ask questions or raise indefinite concerns on Discussions + - name: FAQ + url: https://rocket.rs/guide/faq/ + about: Please see our FAQ for answers to common questions. + - name: Questions + url: https://github.com/rwf2/Rocket/discussions/new?category=questions + about: For other questions or help, please use GitHub discussions. + - name: Feedback + url: https://github.com/rwf2/Rocket/discussions/new/choose + about: For general chat or feedback, please use GitHub discussions. + - name: Chat + url: https://chat.mozilla.org/#/room/#rocket:mozilla.org + about: Chat with us live on rocket:mozilla.org on Matrix. diff --git a/.github/ISSUE_TEMPLATE/doc-problem.yml b/.github/ISSUE_TEMPLATE/doc-problem.yml new file mode 100644 index 0000000000..7270f03d9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc-problem.yml @@ -0,0 +1,56 @@ +name: Documentation Problem +description: Report an issue with or suggest documentation. +labels: ["docs"] +body: + - type: markdown + attributes: + value: > + **Thanks for taking the time to report a documentation issue!** + + + Documentation problems include everything from typos to missing or + incorrect technical details in any of the following: + - [Rocket's Website](https://rocket.rs/) + - [The Rocket Programming Guide](https://rocket.rs/guide/) + - [API Docs](https://api.rocket.rs) + - [Content on GitHub](https://github.com/rwf2/Rocket) + + If we've written it, we want to know how it can be improved. + - type: dropdown + validations: + required: true + attributes: + label: What kind of documentation problem are you reporting? + multiple: true + options: + - Typo (PRs welcome!) + - Unclear Docs + - Undocumented Feature + - Broken Links + - Rendering Issue + - Grammar Issue + - Technical Problem + - Other + - type: input + validations: + required: true + attributes: + label: Where is the issue found? + description: Please provide a direct link to the documentation. + placeholder: "ex: https://rocket.rs/v0.5/guide/requests/#multiple-segments" + - type: textarea + validations: + required: true + attributes: + label: What's wrong? + description: > + Please describe what's wrong with the documentation. + - type: checkboxes + attributes: + label: System Checks + description: "Please confirm all of the following:" + options: + - label: I confirmed that the issue still exists on `master` on GitHub. + required: true + - label: I was unable to find a previous report of this problem. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 72401bf4a8..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Feature Request -about: Propose a change that introduces new functionality -title: '' -labels: request -assignees: '' ---- - -**Is your feature request motivated by a concrete problem? Please describe.** - -A clear and concise description of what the problem is. Examples: - -- "I frequently want to do X, but Rocket makes it hard. It would be nice if Rocket..." -- "I want to do X but Rocket makes it impossible because..." -- "Feature Z exists, but it has these drawbacks. What if..." - -**Why this feature can't or shouldn't live outside of Rocket** - -Rocket is designed to have a small but pluggable core. Feature requests that can be implemented outside of Rocket are typically declined. Make a case for why this feature can't or shouldn't be implemented outside of Rocket. - -**Ideal Solution** - -If you have an idea for a solution, propose it with a clear and concise description. - -**Alternatives Considered** - -A clear and concise description of any alternative solutions or features you've considered. - -**Additional Context** - -Add any other context here, for example, descriptions of elegant solutions in other software. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000000..62e8556ba8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,73 @@ +name: Feature Request +description: Propose a change that introduces new functionality. +labels: ["request"] +body: + - type: markdown + attributes: + value: > + **Thanks for taking the time to request a feature!** Your request helps + make Rocket better. + + + Please note that Rocket is designed to have a small but pluggable core. + Feature requests that can be implemented outside of Rocket _are + typically declined._ In your request, please make a strong case for why + this feature can't or shouldn't be implemented outside of Rocket. + - type: textarea + attributes: + label: What's missing? + description: > + Provide a brief overview of the functionality that's missing in Rocket + today, which problem(s) that functionality would solve for you, and what + the functionality would enable you to do. + placeholder: > + example: I frequently want to do X, but Rocket makes it hard. It would + be nice if Rocket... + + example: I want to do X but Rocket makes it impossible because... + + example: Feature Z exists, but it has these N drawbacks: [..]. What if... + validations: + required: true + - type: textarea + attributes: + label: Ideal Solution + description: > + If you already have an idea of how this feature can be implemented, + please describe it here. Hypothetical code examples are particularly + useful. + - type: textarea + attributes: + label: Why can't this be implemented outside of Rocket? + description: > + Please make a strong case for why this feature can't or shouldn't be + implemented outside of Rocket. We are likely to decline feature requests + that can exist outside of Rocket without compromise. + validations: + required: true + - type: textarea + attributes: + label: Are there workarounds usable today? + description: > + If the functionality being requested can be achieved today, please detail + how here. + - type: textarea + attributes: + label: Alternative Solutions + description: > + If you have other ideas about how this feature can be implemented, let + us know. + - type: textarea + attributes: + label: Additional Context + description: > + Feel free to provide any additional context for your request. + - type: checkboxes + attributes: + label: System Checks + description: "Please confirm all of the following:" + options: + - label: I do not believe that this feature can or should be implemented outside of Rocket. + required: true + - label: I was unable to find a previous request for this feature. + required: true diff --git a/.github/ISSUE_TEMPLATE/suggestion.md b/.github/ISSUE_TEMPLATE/suggestion.md deleted file mode 100644 index 8a07c2ae10..0000000000 --- a/.github/ISSUE_TEMPLATE/suggestion.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Suggestion -about: Suggest a change or improvement to existing functionality -title: '' -labels: suggestion -assignees: '' ---- - -**Existing Functionality** - -A clear and concise description of existing functionality and why it is insufficient. Examples: - -- "I frequently want to do X, but Rocket makes it hard. It would be nice if..." -- "Feature Z exists, but it has these drawbacks. What if..." - -**Suggested Changes** - -If you have a concrete idea for an improvement, propose it with a clear and concise description. - -**Alternatives Considered** - -A clear and concise description of any alternative solutions using existing features or new features you've considered. - -**Additional Context** - -Add any other context here, for example, descriptions of elegant solutions in other software. diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml new file mode 100644 index 0000000000..42e48f47ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -0,0 +1,57 @@ +name: Suggestion +description: Suggest a change or improvement to existing functionality. +labels: ["suggestion"] +body: + - type: markdown + attributes: + value: > + **Thanks for taking the time to make a suggestion!** + - type: input + validations: + required: true + attributes: + label: API Docs to Existing Functionality + description: Please provide a direct link to the API docs for the + functionality you'd like to change. + placeholder: "ex: https://api.rocket.rs/v0.5/rocket/trait.Sentinel.html" + - type: textarea + validations: + required: true + attributes: + label: Problems with Existing Functionality + description: Please let us know what you think is wrong with the existing functionality. + placeholder: > + example: Sentinels don't allow me to access `Foo`, but I'd like to because... + + example: Feature Z exists, but it has these drawbacks. What if... + - type: textarea + validations: + required: true + attributes: + label: Suggested Changes + description: > + How do you propose the existing functionality be changed? Code examples + are particular useful. + - type: textarea + validations: + required: true + attributes: + label: Alternatives Considered + description: > + Instead of making a change to Rocket, please describe alternative + solutions using existing features or new features you've considered. + - type: textarea + attributes: + label: Additional Context + description: Feel free to provide any additional context for your suggestion. + - type: checkboxes + attributes: + label: System Checks + description: "Please confirm all of the following:" + options: + - label: > + I do not believe that this suggestion can or should be implemented + outside of Rocket. + required: true + - label: I was unable to find a previous suggestion for this change. + required: true From bae8216cd44207d6981cc4e9ac5bbda6847c1985 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 6 Dec 2023 16:49:39 -0800 Subject: [PATCH 046/178] Enable templateless issue creation. --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b6cbda93e4..fa2bb1b687 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: FAQ url: https://rocket.rs/guide/faq/ From 92a2559a9a1e0dbb9cd44aa7151a4da2b541f133 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 8 Dec 2023 13:59:21 -0800 Subject: [PATCH 047/178] Set 'img' width, height to decrease layout jitter. --- site/index.toml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/site/index.toml b/site/index.toml index f3e1db7795..f2d1fec2b0 100644 --- a/site/index.toml +++ b/site/index.toml @@ -16,6 +16,8 @@ text = "Type safety turned up to 11 means security and robustness come at compil image = "helmet" button = "Learn More" url = "overview/#how-rocket-works" +width = "69px" +height = "71px" [[top_features]] title = "Boilerplate Free" @@ -23,6 +25,8 @@ text = "Spend your time writing code that really matters and let Rocket handle t image = "robot-free" button = "See Examples" url = "overview/#anatomy-of-a-rocket-application" +width = "78px" +height = "71px" [[top_features]] title = "Easy To Use" @@ -31,6 +35,8 @@ image = "sun" button = "Get Started" url = "guide" margin = 2 +width = "68px" +height = "69px" [[top_features]] title = "Extensible" @@ -39,6 +45,8 @@ image = "telescope" button = "See How" url = "overview/#anatomy-of-a-rocket-application" margin = 9 +width = "71px" +height = "62px" ############################################################################### # Sections: make sure there are an odd number so colors work out. @@ -138,11 +146,15 @@ image = 'templating-icon' url = 'guide/responses/#templates' button = 'Learn More' color = 'blue' +width = '101px' +height = '52px' [[bottom_features]] title = 'Cookies' text = "View, add, or remove cookies, with or without encryption, without hassle." image = 'cookies-icon' +width = '72px' +height = '58px' url = 'guide/requests/#cookies' button = 'Learn More' color = 'fucsia' @@ -155,6 +167,8 @@ image = 'streams-icon' url = 'guide/responses/#async-streams' button = 'Learn More' color = 'red' +width = '82px' +height = '81px' margin = -29 [[bottom_features]] @@ -164,6 +178,8 @@ image = 'config-icon' url = 'guide/configuration/#profiles' button = 'Learn More' color = 'yellow' +width = '57px' +height = '57px' margin = -3 [[bottom_features]] @@ -173,6 +189,7 @@ image = 'pencil-icon' url = 'guide/requests/#private-cookies' button = 'Learn More' color = 'orange' +width = '60px' height = '60px' margin = -3 @@ -183,6 +200,8 @@ image = 'ship-icon' url = 'guide/fairings/#fairings' button = 'Learn More' color = 'green' +width = '98px' +height = '74px' margin = -20 [[bottom_features]] @@ -192,6 +211,8 @@ image = 'query-icon' url = 'guide/state/#databases' button = 'Learn More' color = 'pink' +width = '73px' +height = '57px' margin = -3 [[bottom_features]] @@ -201,6 +222,8 @@ image = 'testing-icon' url = 'guide/testing#testing' button = 'Learn More' color = 'aqua' +width = '47px' +height = '54px' [[bottom_features]] title = 'Community' @@ -209,7 +232,9 @@ image = 'globe' url = 'https://github.com/rwf2/Rocket/network/dependents' button = 'See Dependents' color = 'purple' -height = '62px' +width = '55px' +height = '55px' +margin = -1 ############################################################################### # Panels: displayed in a tabbed arrangement. From a59f3c4c1f271bad6fafd0431568408e5f739b4e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 13 Dec 2023 17:49:05 -0800 Subject: [PATCH 048/178] Fix min dependency versions. Update MSRV to 1.64. Also includes a work-around for a buggy `format_args!` macro found in rustc 1.67 and 1.68. Resolves #2670. --- contrib/db_pools/codegen/Cargo.toml | 2 +- contrib/db_pools/lib/Cargo.toml | 2 +- contrib/dyn_templates/Cargo.toml | 2 +- contrib/sync_db_pools/codegen/Cargo.toml | 2 +- contrib/sync_db_pools/lib/Cargo.toml | 4 ++-- contrib/ws/Cargo.toml | 2 +- contrib/ws/src/websocket.rs | 4 ++-- core/codegen/Cargo.toml | 4 ++-- core/http/Cargo.toml | 4 ++-- core/lib/Cargo.toml | 6 +++--- core/lib/build.rs | 10 ++++++++-- core/lib/src/server.rs | 6 +++++- 12 files changed, 29 insertions(+), 19 deletions(-) diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index 35176dc1ee..713b5a1f6e 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -8,7 +8,7 @@ readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [lib] proc-macro = true diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 5beb44fb38..9dfdfa23e2 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -8,7 +8,7 @@ readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [package.metadata.docs.rs] all-features = true diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index d08fabdb24..11650b4e3f 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" keywords = ["rocket", "framework", "templates", "templating", "engine"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [features] tera = ["tera_"] diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index 007c8f2b4c..1f30eb32b6 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -8,7 +8,7 @@ readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [lib] proc-macro = true diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index 452be35b1a..b563e48dca 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -8,7 +8,7 @@ readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [features] diesel_sqlite_pool = ["diesel/sqlite", "diesel/r2d2"] @@ -31,7 +31,7 @@ r2d2_postgres = { version = "0.18", optional = true } rusqlite = { version = "0.29.0", optional = true } r2d2_sqlite = { version = "0.22.0", optional = true } -memcache = { version = "0.15", optional = true } +memcache = { version = "0.15.2", optional = true } r2d2-memcache = { version = "0.6", optional = true } [dependencies.rocket_sync_db_pools_codegen] diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index 2bb2a03134..91fdf67f33 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" keywords = ["rocket", "web", "framework", "websocket"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [features] default = ["tungstenite"] diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index 32b598e818..63414a111c 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -245,8 +245,8 @@ impl<'r, S> IoHandler for MessageStream<'r, S> { async fn io(self: Pin>, io: IoStream) -> io::Result<()> { let (mut sink, source) = DuplexStream::new(io, self.ws.config).await.split(); - let handler = Pin::into_inner(self).handler; - let mut stream = std::pin::pin!((handler)(source)); + let stream = (Pin::into_inner(self).handler)(source); + rocket::tokio::pin!(stream); while let Some(msg) = stream.next().await { let result = match msg { Ok(msg) => sink.send(msg).await, diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 9fffcb3cc2..d967b0a499 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -10,7 +10,7 @@ readme = "../../README.md" keywords = ["rocket", "web", "framework", "code", "generation"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [lib] proc-macro = true @@ -19,7 +19,7 @@ proc-macro = true indexmap = "2" quote = "1.0" syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits"] } -proc-macro2 = "1.0.27" +proc-macro2 = "1.0.60" devise = "0.4" rocket_http = { version = "0.6.0-dev", path = "../http/" } unicode-xid = "0.2" diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 7200b6bd1e..9aa6a72645 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["rocket", "web", "framework", "http"] license = "MIT OR Apache-2.0" categories = ["web-programming"] edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [features] default = [] @@ -38,7 +38,7 @@ log = "0.4" ref-cast = "1.0" uncased = "0.9.6" either = "1" -pear = "0.2.3" +pear = "0.2.8" pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index d7becfe235..01b2b370b1 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT OR Apache-2.0" build = "build.rs" categories = ["web-programming::http-server"] edition = "2021" -rust-version = "1.56" +rust-version = "1.64" [package.metadata.docs.rs] all-features = true @@ -46,7 +46,7 @@ binascii = "0.1" ref-cast = "1.0" atomic = "0.5" parking_lot = "0.12" -ubyte = {version = "0.10", features = ["serde"] } +ubyte = {version = "0.10.2", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } figment = { version = "0.10.6", features = ["toml", "env"] } rand = "0.8" @@ -56,7 +56,7 @@ indexmap = { version = "2", features = ["serde"] } tempfile = "3" async-trait = "0.1.43" async-stream = "0.3.2" -multer = { version = "2", features = ["tokio-io"] } +multer = { version = "3.0.0", features = ["tokio-io"] } tokio-stream = { version = "0.1.6", features = ["signal", "time"] } state = "0.6" diff --git a/core/lib/build.rs b/core/lib/build.rs index 99369685a4..c72f4955cb 100644 --- a/core/lib/build.rs +++ b/core/lib/build.rs @@ -1,5 +1,11 @@ fn main() { - if let Some(true) = version_check::is_feature_flaggable() { - println!("cargo:rustc-cfg=nightly"); + if let Some((version, channel, _)) = version_check::triple() { + if channel.supports_features() { + println!("cargo:rustc-cfg=nightly"); + } + + if version.at_least("1.67") && version.at_most("1.68.2") { + println!("cargo:rustc-cfg=broken_fmt"); + } } } diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index 800c9dbe63..a8b5d6a4f1 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -73,7 +73,9 @@ async fn hyper_service_fn( // sends the response metadata (and a body channel) prior. let (tx, rx) = oneshot::channel(); - debug!("Received request: {:#?}", hyp_req); + #[cfg(not(broken_fmt))] + debug!("received request: {:#?}", hyp_req); + tokio::spawn(async move { // We move the request next, so get the upgrade future now. let pending_upgrade = hyper::upgrade::on(&mut hyp_req); @@ -160,7 +162,9 @@ impl Rocket { let hyp_response = hyp_res.body(hyp_body) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + #[cfg(not(broken_fmt))] debug!("sending response: {:#?}", hyp_response); + tx.send(hyp_response).map_err(|_| { let msg = "client disconnect before response started"; io::Error::new(io::ErrorKind::BrokenPipe, msg) From 9c2b74b23c0e725ab9f161ea47fb36d80e8eb7c2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 14 Dec 2023 18:42:02 -0800 Subject: [PATCH 049/178] Upgrade 'rustls' to '0.22'. In the process, the following improvements were also made: * Error messages related to TLS were improved. * 'Redirector' in 'tls' example was improved. --- core/http/Cargo.toml | 6 +-- core/http/src/listener.rs | 33 +++++++----- core/http/src/tls/error.rs | 95 ++++++++++++++++++++++++++++++++++ core/http/src/tls/listener.rs | 73 +++++++++++++------------- core/http/src/tls/mod.rs | 3 ++ core/http/src/tls/mtls.rs | 6 +-- core/http/src/tls/util.rs | 90 +++++++++++++++----------------- core/lib/src/config/tls.rs | 2 +- core/lib/src/error.rs | 17 ++++-- core/lib/src/local/request.rs | 4 +- core/lib/src/server.rs | 2 +- examples/tls/Cargo.toml | 1 + examples/tls/src/main.rs | 2 +- examples/tls/src/redirector.rs | 39 +++++++++++--- 14 files changed, 253 insertions(+), 120 deletions(-) create mode 100644 core/http/src/tls/error.rs diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 9aa6a72645..a3537d7bfb 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -30,9 +30,9 @@ percent-encoding = "2" http = "0.2" time = { version = "0.3", features = ["formatting", "macros"] } indexmap = "2" -rustls = { version = "0.21", optional = true } -tokio-rustls = { version = "0.24", optional = true } -rustls-pemfile = { version = "1.0.2", optional = true } +rustls = { version = "0.22", optional = true } +tokio-rustls = { version = "0.25", optional = true } +rustls-pemfile = { version = "2.0.0", optional = true } tokio = { version = "1.6.1", features = ["net", "sync", "time"] } log = "0.4" ref-cast = "1.0" diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs index f898a10883..956c8ec4a2 100644 --- a/core/http/src/listener.rs +++ b/core/http/src/listener.rs @@ -17,36 +17,45 @@ use state::InitCell; pub use tokio::net::TcpListener; /// A thin wrapper over raw, DER-encoded X.509 client certificate data. -// NOTE: `rustls::Certificate` is exactly isomorphic to `CertificateData`. -#[doc(inline)] -#[cfg(feature = "tls")] -pub use rustls::Certificate as CertificateData; +#[cfg(not(feature = "tls"))] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CertificateDer(pub(crate) Vec); /// A thin wrapper over raw, DER-encoded X.509 client certificate data. -#[cfg(not(feature = "tls"))] -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct CertificateData(pub Vec); +#[cfg(feature = "tls")] +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(transparent)] +pub struct CertificateDer(pub(crate) rustls::pki_types::CertificateDer<'static>); /// A collection of raw certificate data. #[derive(Clone, Default)] -pub struct Certificates(Arc>>); +pub struct Certificates(Arc>>); -impl From> for Certificates { - fn from(value: Vec) -> Self { +impl From> for Certificates { + fn from(value: Vec) -> Self { + Certificates(Arc::new(value.into())) + } +} + +#[cfg(feature = "tls")] +impl From>> for Certificates { + fn from(value: Vec>) -> Self { + let value: Vec<_> = value.into_iter().map(CertificateDer).collect(); Certificates(Arc::new(value.into())) } } +#[doc(hidden)] impl Certificates { /// Set the the raw certificate chain data. Only the first call actually /// sets the data; the remaining do nothing. #[cfg(feature = "tls")] - pub(crate) fn set(&self, data: Vec) { + pub(crate) fn set(&self, data: Vec) { self.0.set(data); } /// Returns the raw certificate chain data, if any is available. - pub fn chain_data(&self) -> Option<&[CertificateData]> { + pub fn chain_data(&self) -> Option<&[CertificateDer]> { self.0.try_get().map(|v| v.as_slice()) } } diff --git a/core/http/src/tls/error.rs b/core/http/src/tls/error.rs new file mode 100644 index 0000000000..429f4a9d1f --- /dev/null +++ b/core/http/src/tls/error.rs @@ -0,0 +1,95 @@ +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum KeyError { + BadKeyCount(usize), + Io(std::io::Error), + Unsupported(rustls::Error), + BadItem(rustls_pemfile::Item), +} + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + Tls(rustls::Error), + Mtls(rustls::server::VerifierBuilderError), + CertChain(std::io::Error), + PrivKey(KeyError), + CertAuth(rustls::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; + + match self { + Io(e) => write!(f, "i/o error during tls binding: {e}"), + Tls(e) => write!(f, "tls configuration error: {e}"), + Mtls(e) => write!(f, "mtls verifier error: {e}"), + CertChain(e) => write!(f, "failed to process certificate chain: {e}"), + PrivKey(e) => write!(f, "failed to process private key: {e}"), + CertAuth(e) => write!(f, "failed to process certificate authority: {e}"), + } + } +} + +impl std::fmt::Display for KeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use KeyError::*; + + match self { + Io(e) => write!(f, "error reading key file: {e}"), + BadKeyCount(0) => write!(f, "no valid keys found. is the file malformed?"), + BadKeyCount(n) => write!(f, "expected exactly 1 key, found {n}"), + Unsupported(e) => write!(f, "key is valid but is unsupported: {e}"), + BadItem(i) => write!(f, "found unexpected item in key file: {i:#?}"), + } + } +} + +impl std::error::Error for KeyError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + KeyError::Io(e) => Some(e), + KeyError::Unsupported(e) => Some(e), + _ => None, + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(e) => Some(e), + Error::Tls(e) => Some(e), + Error::Mtls(e) => Some(e), + Error::CertChain(e) => Some(e), + Error::PrivKey(e) => Some(e), + Error::CertAuth(e) => Some(e), + } + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: rustls::Error) -> Self { + Error::Tls(e) + } +} + +impl From for Error { + fn from(value: rustls::server::VerifierBuilderError) -> Self { + Error::Mtls(value) + } +} + +impl From for Error { + fn from(value: KeyError) -> Self { + Error::PrivKey(value) + } +} diff --git a/core/http/src/tls/listener.rs b/core/http/src/tls/listener.rs index 8c675d6110..7ef76ebd8d 100644 --- a/core/http/src/tls/listener.rs +++ b/core/http/src/tls/listener.rs @@ -8,9 +8,10 @@ use std::net::SocketAddr; use tokio::net::{TcpListener, TcpStream}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_rustls::{Accept, TlsAcceptor, server::TlsStream as BareTlsStream}; +use rustls::server::{ServerSessionMemoryCache, ServerConfig, WebPkiClientVerifier}; -use crate::tls::util::{load_certs, load_private_key, load_ca_certs}; -use crate::listener::{Connection, Listener, Certificates}; +use crate::tls::util::{load_cert_chain, load_key, load_ca_certs}; +use crate::listener::{Connection, Listener, Certificates, CertificateDer}; /// A TLS listener over TCP. pub struct TlsListener { @@ -40,7 +41,7 @@ pub struct TlsListener { /// /// To work around this, we "lie" when `peer_certificates()` are requested and /// always return `Some(Certificates)`. Internally, `Certificates` is an -/// `Arc>>`, effectively a shared, thread-safe, +/// `Arc>>`, effectively a shared, thread-safe, /// `OnceCell`. The cell is initially empty and is filled as soon as the /// handshake is complete. If the certificate data were to be requested prior to /// this point, it would be empty. However, in Rocket, we only request @@ -72,49 +73,43 @@ pub struct Config { } impl TlsListener { - pub async fn bind(addr: SocketAddr, mut c: Config) -> io::Result + pub async fn bind(addr: SocketAddr, mut c: Config) -> crate::tls::Result where R: io::BufRead { - use rustls::server::{AllowAnyAuthenticatedClient, AllowAnyAnonymousOrAuthenticatedClient}; - use rustls::server::{NoClientAuth, ServerSessionMemoryCache, ServerConfig}; - - let cert_chain = load_certs(&mut c.cert_chain) - .map_err(|e| io::Error::new(e.kind(), format!("bad TLS cert chain: {}", e)))?; - - let key = load_private_key(&mut c.private_key) - .map_err(|e| io::Error::new(e.kind(), format!("bad TLS private key: {}", e)))?; + let provider = rustls::crypto::CryptoProvider { + cipher_suites: c.ciphersuites, + ..rustls::crypto::ring::default_provider() + }; - let client_auth = match c.ca_certs { - Some(ref mut ca_certs) => match load_ca_certs(ca_certs) { - Ok(ca) if c.mandatory_mtls => AllowAnyAuthenticatedClient::new(ca).boxed(), - Ok(ca) => AllowAnyAnonymousOrAuthenticatedClient::new(ca).boxed(), - Err(e) => return Err(io::Error::new(e.kind(), format!("bad CA cert(s): {}", e))), + let verifier = match c.ca_certs { + Some(ref mut ca_certs) => { + let ca_roots = Arc::new(load_ca_certs(ca_certs)?); + let verifier = WebPkiClientVerifier::builder(ca_roots); + match c.mandatory_mtls { + true => verifier.build()?, + false => verifier.allow_unauthenticated().build()?, + } }, - None => NoClientAuth::boxed(), + None => WebPkiClientVerifier::no_client_auth(), }; - let mut tls_config = ServerConfig::builder() - .with_cipher_suites(&c.ciphersuites) - .with_safe_default_kx_groups() - .with_safe_default_protocol_versions() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))? - .with_client_cert_verifier(client_auth) - .with_single_cert(cert_chain, key) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS config: {}", e)))?; - - tls_config.ignore_client_order = c.prefer_server_order; - - tls_config.alpn_protocols = vec![b"http/1.1".to_vec()]; + let key = load_key(&mut c.private_key)?; + let cert_chain = load_cert_chain(&mut c.cert_chain)?; + let mut config = ServerConfig::builder_with_provider(Arc::new(provider)) + .with_safe_default_protocol_versions()? + .with_client_cert_verifier(verifier) + .with_single_cert(cert_chain, key)?; + + config.ignore_client_order = c.prefer_server_order; + config.session_storage = ServerSessionMemoryCache::new(1024); + config.ticketer = rustls::crypto::ring::Ticketer::new()?; + config.alpn_protocols = vec![b"http/1.1".to_vec()]; if cfg!(feature = "http2") { - tls_config.alpn_protocols.insert(0, b"h2".to_vec()); + config.alpn_protocols.insert(0, b"h2".to_vec()); } - tls_config.session_storage = ServerSessionMemoryCache::new(1024); - tls_config.ticketer = rustls::Ticketer::new() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bad TLS ticketer: {}", e)))?; - let listener = TcpListener::bind(addr).await?; - let acceptor = TlsAcceptor::from(Arc::new(tls_config)); + let acceptor = TlsAcceptor::from(Arc::new(config)); Ok(TlsListener { listener, acceptor }) } } @@ -179,8 +174,10 @@ impl TlsStream { TlsState::Handshaking(ref mut accept) => { match futures::ready!(Pin::new(accept).poll(cx)) { Ok(stream) => { - if let Some(cert_chain) = stream.get_ref().1.peer_certificates() { - self.certs.set(cert_chain.to_vec()); + if let Some(peer_certs) = stream.get_ref().1.peer_certificates() { + self.certs.set(peer_certs.into_iter() + .map(|v| CertificateDer(v.clone().into_owned())) + .collect()); } self.state = TlsState::Streaming(stream); diff --git a/core/http/src/tls/mod.rs b/core/http/src/tls/mod.rs index 04959ba23e..8d3bcb3d67 100644 --- a/core/http/src/tls/mod.rs +++ b/core/http/src/tls/mod.rs @@ -6,3 +6,6 @@ pub mod mtls; pub use rustls; pub use listener::{TlsListener, Config}; pub mod util; +pub mod error; + +pub use error::Result; diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 7a7cd1697c..417db2f87d 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -41,7 +41,7 @@ use x509_parser::nom; use x509::{ParsedExtension, X509Name, X509Certificate, TbsCertificate, X509Error, FromDer}; use oid::OID_X509_EXT_SUBJECT_ALT_NAME as SUBJECT_ALT_NAME; -use crate::listener::CertificateData; +use crate::listener::CertificateDer; /// A type alias for [`Result`](std::result::Result) with the error type set to /// [`Error`]. @@ -144,7 +144,7 @@ pub type Result = std::result::Result; #[derive(Debug, PartialEq)] pub struct Certificate<'a> { x509: X509Certificate<'a>, - data: &'a CertificateData, + data: &'a CertificateDer, } /// An X.509 Distinguished Name (DN) found in a [`Certificate`]. @@ -224,7 +224,7 @@ impl<'a> Certificate<'a> { /// PRIVATE: For internal Rocket use only! #[doc(hidden)] - pub fn parse(chain: &[CertificateData]) -> Result> { + pub fn parse(chain: &[CertificateDer]) -> Result> { let data = chain.first().ok_or_else(|| Error::Empty)?; let x509 = Certificate::parse_one(&data.0)?; Ok(Certificate { x509, data }) diff --git a/core/http/src/tls/util.rs b/core/http/src/tls/util.rs index 8eb54c5d45..c07135adc7 100644 --- a/core/http/src/tls/util.rs +++ b/core/http/src/tls/util.rs @@ -1,55 +1,47 @@ -use std::io::{self, Cursor, Read}; +use std::io; -use rustls::{Certificate, PrivateKey, RootCertStore}; +use rustls::RootCertStore; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -fn err(message: impl Into>) -> io::Error { - io::Error::new(io::ErrorKind::Other, message.into()) -} +use crate::tls::error::{Result, Error, KeyError}; /// Loads certificates from `reader`. -pub fn load_certs(reader: &mut dyn io::BufRead) -> io::Result> { - let certs = rustls_pemfile::certs(reader).map_err(|_| err("invalid certificate"))?; - Ok(certs.into_iter().map(Certificate).collect()) +pub fn load_cert_chain(reader: &mut dyn io::BufRead) -> Result>> { + rustls_pemfile::certs(reader) + .collect::>() + .map_err(Error::CertChain) } /// Load and decode the private key from `reader`. -pub fn load_private_key(reader: &mut dyn io::BufRead) -> io::Result { - // "rsa" (PKCS1) PEM files have a different first-line header than PKCS8 - // PEM files, use that to determine the parse function to use. - let mut header = String::new(); - let private_keys_fn = loop { - header.clear(); - if reader.read_line(&mut header)? == 0 { - return Err(err("failed to find key header; supported formats are: RSA, PKCS8, SEC1")); - } - - break match header.trim_end() { - "-----BEGIN RSA PRIVATE KEY-----" => rustls_pemfile::rsa_private_keys, - "-----BEGIN PRIVATE KEY-----" => rustls_pemfile::pkcs8_private_keys, - "-----BEGIN EC PRIVATE KEY-----" => rustls_pemfile::ec_private_keys, - _ => continue, - }; - }; - - let key = private_keys_fn(&mut Cursor::new(header).chain(reader)) - .map_err(|_| err("invalid key file")) - .and_then(|mut keys| match keys.len() { - 0 => Err(err("no valid keys found; is the file malformed?")), - 1 => Ok(PrivateKey(keys.remove(0))), - n => Err(err(format!("expected 1 key, found {}", n))), - })?; +pub fn load_key(reader: &mut dyn io::BufRead) -> Result> { + use rustls_pemfile::Item::*; + + let mut keys: Vec> = rustls_pemfile::read_all(reader) + .map(|result| result.map_err(KeyError::Io) + .and_then(|item| match item { + Pkcs1Key(key) => Ok(key.into()), + Pkcs8Key(key) => Ok(key.into()), + Sec1Key(key) => Ok(key.into()), + _ => Err(KeyError::BadItem(item)) + }) + ) + .collect::>()?; + + if keys.len() != 1 { + return Err(KeyError::BadKeyCount(keys.len()).into()); + } // Ensure we can use the key. - rustls::sign::any_supported_type(&key) - .map_err(|_| err("key parsed but is unusable")) - .map(|_| key) + let key = keys.remove(0); + rustls::crypto::ring::sign::any_supported_type(&key).map_err(KeyError::Unsupported)?; + Ok(key) } /// Load and decode CA certificates from `reader`. -pub fn load_ca_certs(reader: &mut dyn io::BufRead) -> io::Result { +pub fn load_ca_certs(reader: &mut dyn io::BufRead) -> Result { let mut roots = rustls::RootCertStore::empty(); - for cert in load_certs(reader)? { - roots.add(&cert).map_err(|e| err(format!("CA cert error: {}", e)))?; + for cert in load_cert_chain(reader)? { + roots.add(cert).map_err(Error::CertAuth)?; } Ok(roots) @@ -66,31 +58,31 @@ mod test { } #[test] - fn verify_load_private_keys_of_different_types() -> io::Result<()> { + fn verify_load_private_keys_of_different_types() -> Result<()> { let rsa_sha256_key = tls_example_key!("rsa_sha256_key.pem"); let ecdsa_nistp256_sha256_key = tls_example_key!("ecdsa_nistp256_sha256_key_pkcs8.pem"); let ecdsa_nistp384_sha384_key = tls_example_key!("ecdsa_nistp384_sha384_key_pkcs8.pem"); let ed2551_key = tls_example_key!("ed25519_key.pem"); - load_private_key(&mut Cursor::new(rsa_sha256_key))?; - load_private_key(&mut Cursor::new(ecdsa_nistp256_sha256_key))?; - load_private_key(&mut Cursor::new(ecdsa_nistp384_sha384_key))?; - load_private_key(&mut Cursor::new(ed2551_key))?; + load_key(&mut &rsa_sha256_key[..])?; + load_key(&mut &ecdsa_nistp256_sha256_key[..])?; + load_key(&mut &ecdsa_nistp384_sha384_key[..])?; + load_key(&mut &ed2551_key[..])?; Ok(()) } #[test] - fn verify_load_certs_of_different_types() -> io::Result<()> { + fn verify_load_certs_of_different_types() -> Result<()> { let rsa_sha256_cert = tls_example_key!("rsa_sha256_cert.pem"); let ecdsa_nistp256_sha256_cert = tls_example_key!("ecdsa_nistp256_sha256_cert.pem"); let ecdsa_nistp384_sha384_cert = tls_example_key!("ecdsa_nistp384_sha384_cert.pem"); let ed2551_cert = tls_example_key!("ed25519_cert.pem"); - load_certs(&mut Cursor::new(rsa_sha256_cert))?; - load_certs(&mut Cursor::new(ecdsa_nistp256_sha256_cert))?; - load_certs(&mut Cursor::new(ecdsa_nistp384_sha384_cert))?; - load_certs(&mut Cursor::new(ed2551_cert))?; + load_cert_chain(&mut &rsa_sha256_cert[..])?; + load_cert_chain(&mut &ecdsa_nistp256_sha256_cert[..])?; + load_cert_chain(&mut &ecdsa_nistp384_sha384_cert[..])?; + load_cert_chain(&mut &ed2551_cert[..])?; Ok(()) } diff --git a/core/lib/src/config/tls.rs b/core/lib/src/config/tls.rs index 41e88082a7..12face0015 100644 --- a/core/lib/src/config/tls.rs +++ b/core/lib/src/config/tls.rs @@ -631,7 +631,7 @@ mod with_tls_feature { use crate::http::tls::Config; use crate::http::tls::rustls::SupportedCipherSuite as RustlsCipher; - use crate::http::tls::rustls::cipher_suite; + use crate::http::tls::rustls::crypto::ring::cipher_suite; use yansi::Paint; diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 28e71fc216..ff3ef79f61 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -76,6 +76,9 @@ pub struct Error { pub enum ErrorKind { /// Binding to the provided address/port failed. Bind(io::Error), + /// Binding via TLS to the provided address/port failed. + #[cfg(feature = "tls")] + TlsBind(crate::http::tls::error::Error), /// An I/O error occurred during launch. Io(io::Error), /// A valid [`Config`](crate::Config) could not be extracted from the @@ -234,6 +237,12 @@ impl Error { "aborting due to failed shutdown" } + #[cfg(feature = "tls")] + ErrorKind::TlsBind(e) => { + error!("Rocket failed to bind via TLS to network socket."); + info_!("{}", e); + "aborting due to TLS bind error" + } } } } @@ -244,15 +253,17 @@ impl fmt::Display for ErrorKind { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ErrorKind::Bind(e) => write!(f, "binding failed: {}", e), - ErrorKind::Io(e) => write!(f, "I/O error: {}", e), + ErrorKind::Bind(e) => write!(f, "binding failed: {e}"), + ErrorKind::Io(e) => write!(f, "I/O error: {e}"), ErrorKind::Collisions(_) => "collisions detected".fmt(f), ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f), ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f), ErrorKind::Config(_) => "failed to extract configuration".fmt(f), ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f), - ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {}", e), + ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {e}"), ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f), + #[cfg(feature = "tls")] + ErrorKind::TlsBind(e) => write!(f, "TLS bind failed: {e}"), } } } diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index eabe933ee7..78e975957c 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -228,10 +228,10 @@ macro_rules! pub_request_impl { #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] pub fn identity(mut self, reader: C) -> Self { - use crate::http::{tls::util::load_certs, private::Certificates}; + use crate::http::{tls::util::load_cert_chain, private::Certificates}; let mut reader = std::io::BufReader::new(reader); - let certs = load_certs(&mut reader).map(Certificates::from); + let certs = load_cert_chain(&mut reader).map(Certificates::from); self._request_mut().connection.client_certificates = certs.ok(); self } diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index a8b5d6a4f1..da2626e327 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -427,7 +427,7 @@ impl Rocket { use crate::http::tls::TlsListener; let conf = config.to_native_config().map_err(ErrorKind::Io)?; - let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::Bind)?; + let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::TlsBind)?; addr = l.local_addr().unwrap_or(addr); self.config.address = addr.ip(); self.config.port = addr.port(); diff --git a/examples/tls/Cargo.toml b/examples/tls/Cargo.toml index 11bcc1ff2f..9c72493908 100644 --- a/examples/tls/Cargo.toml +++ b/examples/tls/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] rocket = { path = "../../core/lib", features = ["tls", "mtls", "secrets"] } +yansi = "1.0.0-rc.1" diff --git a/examples/tls/src/main.rs b/examples/tls/src/main.rs index 8ecb685cc9..4ce4254c24 100644 --- a/examples/tls/src/main.rs +++ b/examples/tls/src/main.rs @@ -22,5 +22,5 @@ fn rocket() -> _ { // Run `./private/gen_certs.sh` to generate a CA and key pairs. rocket::build() .mount("/", routes![hello, mutual]) - .attach(redirector::Redirector { port: 3000 }) + .attach(redirector::Redirector::on(3000)) } diff --git a/examples/tls/src/redirector.rs b/examples/tls/src/redirector.rs index 0aafddf9ed..aeffe9ad3a 100644 --- a/examples/tls/src/redirector.rs +++ b/examples/tls/src/redirector.rs @@ -1,22 +1,35 @@ //! Redirect all HTTP requests to HTTPs. +use std::sync::OnceLock; + use rocket::http::Status; use rocket::log::LogLevel; use rocket::{route, Error, Request, Data, Route, Orbit, Rocket, Ignite, Config}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::response::Redirect; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct Redirector { - pub port: u16 + pub listen_port: u16, + pub tls_port: OnceLock, } impl Redirector { - // Route function that gets call on every single request. + pub fn on(port: u16) -> Self { + Redirector { listen_port: port, tls_port: OnceLock::new() } + } + + // Route function that gets called on every single request. fn redirect<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { // FIXME: Check the host against a whitelist! + let redirector = req.rocket().state::().expect("managed Self"); if let Some(host) = req.host() { - let https_uri = format!("https://{}{}", host, req.uri()); + let domain = host.domain(); + let https_uri = match redirector.tls_port.get() { + Some(443) | None => format!("https://{domain}{}", req.uri()), + Some(port) => format!("https://{domain}:{port}{}", req.uri()), + }; + route::Outcome::from(req, Redirect::permanent(https_uri)).pin() } else { route::Outcome::from(req, Status::BadRequest).pin() @@ -25,13 +38,21 @@ impl Redirector { // Launch an instance of Rocket than handles redirection on `self.port`. pub async fn try_launch(self, mut config: Config) -> Result, Error> { + use yansi::Paint; use rocket::http::Method::*; + // Determine the port TLS is being served on. + let tls_port = self.tls_port.get_or_init(|| config.port); + // Adjust config for redirector: disable TLS, set port, disable logging. config.tls = None; - config.port = self.port; + config.port = self.listen_port; config.log_level = LogLevel::Critical; + info!("{}{}", "๐Ÿ”’ ".mask(), "HTTP -> HTTPS Redirector:".magenta()); + info_!("redirecting on insecure port {} to TLS port {}", + self.listen_port.yellow(), tls_port.green()); + // Build a vector of routes to `redirect` on `` for each method. let redirects = [Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch] .into_iter() @@ -39,6 +60,7 @@ impl Redirector { .collect::>(); rocket::custom(config) + .manage(self) .mount("/", redirects) .launch() .await @@ -48,11 +70,14 @@ impl Redirector { #[rocket::async_trait] impl Fairing for Redirector { fn info(&self) -> Info { - Info { name: "HTTP -> HTTPS Redirector", kind: Kind::Liftoff } + Info { + name: "HTTP -> HTTPS Redirector", + kind: Kind::Liftoff | Kind::Singleton + } } async fn on_liftoff(&self, rkt: &Rocket) { - let (this, shutdown, config) = (*self, rkt.shutdown(), rkt.config().clone()); + let (this, shutdown, config) = (self.clone(), rkt.shutdown(), rkt.config().clone()); let _ = rocket::tokio::spawn(async move { if let Err(e) = this.try_launch(config).await { error!("Failed to start HTTP -> HTTPS redirector."); From 634ba40d380cc36123829521150e14c4b5c860fa Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 15 Dec 2023 16:58:00 -0800 Subject: [PATCH 050/178] Update 'tungstenite' to '0.21'. --- contrib/ws/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index 91fdf67f33..dab3dd890e 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -17,7 +17,7 @@ default = ["tungstenite"] tungstenite = ["tokio-tungstenite"] [dependencies] -tokio-tungstenite = { version = "0.20", optional = true } +tokio-tungstenite = { version = "0.21", optional = true } [dependencies.rocket] version = "0.6.0-dev" From b5278de795ea5735043f55bf0c551b4249c3a0e4 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 15 Dec 2023 17:23:24 -0800 Subject: [PATCH 051/178] Update 'deadpool' to 0.10. Also updates: * 'deadpool-postgres' to 0.12 * 'deadpool-redis' to 0.14 --- contrib/db_pools/lib/Cargo.toml | 17 +++++++---- contrib/db_pools/lib/src/lib.rs | 4 +-- contrib/db_pools/lib/src/pool.rs | 48 ++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 9dfdfa23e2..a6ab036418 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -23,8 +23,8 @@ sqlx_postgres = ["sqlx", "sqlx/postgres"] sqlx_sqlite = ["sqlx", "sqlx/sqlite"] sqlx_macros = ["sqlx/macros"] # diesel features -diesel_postgres = ["diesel-async/postgres", "diesel-async/deadpool", "diesel", "deadpool"] -diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpool"] +diesel_postgres = ["diesel-async/postgres", "diesel-async/deadpool", "diesel", "deadpool_09"] +diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpool_09"] # implicit features: mongodb [dependencies.rocket] @@ -36,20 +36,27 @@ default-features = false path = "../codegen" version = "0.1.0" -[dependencies.deadpool] +[dependencies.deadpool_09] +package = "deadpool" version = "0.9" default-features = false features = ["rt_tokio_1", "managed"] optional = true [dependencies.deadpool-postgres] -version = "0.10" +version = "0.12" default-features = false features = ["rt_tokio_1"] optional = true +[dependencies.deadpool] +version = "0.10" +default-features = false +features = ["rt_tokio_1", "managed"] +optional = true + [dependencies.deadpool-redis] -version = "0.12" +version = "0.14" default-features = false features = ["rt_tokio_1"] optional = true diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index 6e6b886add..ae5118fb2c 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -107,8 +107,8 @@ //! //! | Database | Feature | [`Pool`] Type | [`Connection`] Deref | //! |----------|-----------------------------|-----------------------------|--------------------------------------| -//! | Postgres | `deadpool_postgres` (v0.10) | [`deadpool_postgres::Pool`] | [`deadpool_postgres::ClientWrapper`] | -//! | Redis | `deadpool_redis` (v0.11) | [`deadpool_redis::Pool`] | [`deadpool_redis::Connection`] | +//! | Postgres | `deadpool_postgres` (v0.12) | [`deadpool_postgres::Pool`] | [`deadpool_postgres::ClientWrapper`] | +//! | Redis | `deadpool_redis` (v0.14) | [`deadpool_redis::Pool`] | [`deadpool_redis::Connection`] | //! //! On shutdown, new connections are denied. Shutdown _does not_ wait for //! connections to be returned. diff --git a/contrib/db_pools/lib/src/pool.rs b/contrib/db_pools/lib/src/pool.rs index 694a20648e..4e3ef35d92 100644 --- a/contrib/db_pools/lib/src/pool.rs +++ b/contrib/db_pools/lib/src/pool.rs @@ -156,9 +156,7 @@ pub trait Pool: Sized + Send + Sync + 'static { mod deadpool_postgres { use deadpool::{managed::{Manager, Pool, PoolError, Object, BuildError}, Runtime}; use super::{Duration, Error, Config, Figment}; - - #[cfg(any(feature = "diesel_postgres", feature = "diesel_mysql"))] - use diesel_async::pooled_connection::AsyncDieselConnectionManager; + use rocket::Either; pub trait DeadManager: Manager + Sized + Send + Sync + 'static { fn new(config: &Config) -> Result; @@ -178,6 +176,50 @@ mod deadpool_postgres { } } + #[rocket::async_trait] + impl>> crate::Pool for Pool + where M::Type: Send, C: Send + Sync + 'static, M::Error: std::error::Error + { + type Error = Error, PoolError>; + + type Connection = C; + + async fn init(figment: &Figment) -> Result { + let config: Config = figment.extract()?; + let manager = M::new(&config).map_err(|e| Error::Init(Either::Left(e)))?; + + Pool::builder(manager) + .max_size(config.max_connections) + .wait_timeout(Some(Duration::from_secs(config.connect_timeout))) + .create_timeout(Some(Duration::from_secs(config.connect_timeout))) + .recycle_timeout(config.idle_timeout.map(Duration::from_secs)) + .runtime(Runtime::Tokio1) + .build() + .map_err(|e| Error::Init(Either::Right(e))) + } + + async fn get(&self) -> Result { + self.get().await.map_err(Error::Get) + } + + async fn close(&self) { + >::close(self) + } + } +} + +// TODO: Remove when new release of diesel-async with deadpool 0.10 is out. +#[cfg(all(feature = "deadpool_09", any(feature = "diesel_postgres", feature = "diesel_mysql")))] +mod deadpool_old { + use deadpool_09::{managed::{Manager, Pool, PoolError, Object, BuildError}, Runtime}; + use diesel_async::pooled_connection::AsyncDieselConnectionManager; + + use super::{Duration, Error, Config, Figment}; + + pub trait DeadManager: Manager + Sized + Send + Sync + 'static { + fn new(config: &Config) -> Result; + } + #[cfg(feature = "diesel_postgres")] impl DeadManager for AsyncDieselConnectionManager { fn new(config: &Config) -> Result { From 485c490b1f7b04c049f89f3a26c36d563fe624c2 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Thu, 2 Nov 2023 09:01:58 +0800 Subject: [PATCH 052/178] Fix typos: 'preceeding', 'occured'. --- core/lib/src/router/collider.rs | 2 +- core/lib/src/router/matcher.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 500af5c99f..acf4d7c937 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -31,7 +31,7 @@ impl Route { /// `0..n`, either `a.uri[i]` is dynamic _or_ `b.uri[i]` is dynamic /// _or_ they're both static with the same value. /// - One URI has fewer segments _and_ ends with a trailing dynamic - /// parameter _and_ the preceeding segments in both routes match the + /// parameter _and_ the preceding segments in both routes match the /// conditions above. /// /// Collisions are symmetric: for any routes `a` and `b`, diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs index 83d30c876b..00c9dec368 100644 --- a/core/lib/src/router/matcher.rs +++ b/core/lib/src/router/matcher.rs @@ -74,7 +74,7 @@ impl Route { } impl Catcher { - /// Returns `true` if `self` matches errors with `status` that occured + /// Returns `true` if `self` matches errors with `status` that occurred /// during `request`. /// /// A [_match_](Catcher#routing) between a `Catcher` and a (`Status`, From 1b089bdb63fa40976543f8a82e57f02e795bf8e2 Mon Sep 17 00:00:00 2001 From: Mathew Horner Date: Sat, 18 Nov 2023 11:57:25 -0600 Subject: [PATCH 053/178] Fix git repository URLs in contrib crates. --- contrib/db_pools/codegen/Cargo.toml | 2 +- contrib/db_pools/lib/Cargo.toml | 2 +- contrib/sync_db_pools/codegen/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/db_pools/codegen/Cargo.toml b/contrib/db_pools/codegen/Cargo.toml index 713b5a1f6e..35f2b6d35c 100644 --- a/contrib/db_pools/codegen/Cargo.toml +++ b/contrib/db_pools/codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_db_pools_codegen" version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Procedural macros for rocket_db_pools." -repository = "https://github.com/rwf2/Rocket/contrib/db_pools" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index a6ab036418..ece1c0507d 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_db_pools" version = "0.1.0" authors = ["Sergio Benitez ", "Jeb Rosen "] description = "Rocket async database pooling support" -repository = "https://github.com/rwf2/Rocket/contrib/db_pools" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index 1f30eb32b6..306e4219da 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "rocket_sync_db_pools_codegen" version = "0.1.0" authors = ["Sergio Benitez "] description = "Procedural macros for rocket_sync_db_pools." -repository = "https://github.com/rwf2/Rocket/contrib/sync_db_pools" +repository = "https://github.com/rwf2/Rocket/tree/master/contrib/sync_db_pools" readme = "../README.md" keywords = ["rocket", "framework", "database", "pools"] license = "MIT OR Apache-2.0" From f4e8987a463c6bf96b650672d3e79c3c3eb9c82b Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 29 Nov 2023 13:33:25 -0800 Subject: [PATCH 054/178] Make 'cli_colors' an enum, add "always" option. Resolves #2649. --- benchmarks/src/routing.rs | 4 +- core/lib/fuzz/targets/collision-matching.rs | 3 +- core/lib/src/config/cli_colors.rs | 80 +++++++++++++ core/lib/src/config/config.rs | 9 +- core/lib/src/config/mod.rs | 120 +++++++++++++++++++- core/lib/src/log.rs | 9 +- 6 files changed, 212 insertions(+), 13 deletions(-) create mode 100644 core/lib/src/config/cli_colors.rs diff --git a/benchmarks/src/routing.rs b/benchmarks/src/routing.rs index 8f842896fe..d5ab63280d 100644 --- a/benchmarks/src/routing.rs +++ b/benchmarks/src/routing.rs @@ -2,7 +2,7 @@ use std::collections::hash_set::HashSet; use criterion::{criterion_group, Criterion}; -use rocket::{route, config, Request, Data, Route, Config}; +use rocket::{route, config::{self, CliColors}, Request, Data, Route, Config}; use rocket::http::{Method, RawStr, ContentType, Accept, Status}; use rocket::local::blocking::{Client, LocalRequest}; @@ -81,7 +81,7 @@ fn client(routes: Vec) -> Client { let config = Config { profile: Config::RELEASE_PROFILE, log_level: rocket::config::LogLevel::Off, - cli_colors: false, + cli_colors: CliColors::Never, shutdown: config::Shutdown { ctrlc: false, #[cfg(unix)] diff --git a/core/lib/fuzz/targets/collision-matching.rs b/core/lib/fuzz/targets/collision-matching.rs index 8b9db5640e..a478bc4260 100644 --- a/core/lib/fuzz/targets/collision-matching.rs +++ b/core/lib/fuzz/targets/collision-matching.rs @@ -6,6 +6,7 @@ use rocket::http::QMediaType; use rocket::local::blocking::{LocalRequest, Client}; use rocket::http::{Method, Accept, ContentType, MediaType, uri::Origin}; use rocket::route::{Route, RouteUri, dummy_handler}; +use rocket::config::CliColors; #[derive(Arbitrary)] struct ArbitraryRequestData<'a> { @@ -186,7 +187,7 @@ fn fuzz((route_a, route_b, req): TestData<'_>) { let rocket = rocket::custom(rocket::Config { workers: 2, log_level: rocket::log::LogLevel::Off, - cli_colors: false, + cli_colors: CliColors::Never, ..rocket::Config::debug_default() }); diff --git a/core/lib/src/config/cli_colors.rs b/core/lib/src/config/cli_colors.rs new file mode 100644 index 0000000000..863104e245 --- /dev/null +++ b/core/lib/src/config/cli_colors.rs @@ -0,0 +1,80 @@ +use core::fmt; +use serde::{ + de::{self, Unexpected::{Signed, Str}}, + Deserialize, Serialize +}; + +/// Configure color output for logging. +#[derive(Clone, Serialize, PartialEq, Debug, Default)] +pub enum CliColors { + /// Always use colors in logs. + Always, + + /// Use colors in logs if the terminal supports it. + #[default] + Auto, + + /// Never use colors in logs. + Never +} + +impl fmt::Display for CliColors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + CliColors::Always => write!(f, "always"), + CliColors::Auto => write!(f, "auto"), + CliColors::Never => write!(f, "never") + } + } +} + + +impl<'de> Deserialize<'de> for CliColors { + fn deserialize>(de: D) -> Result { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = CliColors; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("0, 1, false, true, always, auto, never") + } + + fn visit_str(self, val: &str) -> Result { + match val.to_lowercase().as_str() { + "true" => Ok(CliColors::Auto), + "false" => Ok(CliColors::Never), + "1" => Ok(CliColors::Auto), + "0" => Ok(CliColors::Never), + "auto" => Ok(CliColors::Auto), + "never" => Ok(CliColors::Never), + "always" => Ok(CliColors::Always), + _ => Err(E::invalid_value( + Str(val), + &"0, 1, false, true, always, auto, never", + )) + } + } + + fn visit_bool(self, val: bool) -> Result { + match val { + true => Ok(CliColors::Auto), + false => Ok(CliColors::Never) + } + } + + fn visit_i64(self, val: i64) -> Result { + match val { + 0 => Ok(CliColors::Never), + 1 => Ok(CliColors::Auto), + _ => Err(E::invalid_value( + Signed(val), + &"0, 1, false, true, always, auto, never", + )) + } + } + } + + de.deserialize_any(Visitor) + } +} diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 635f2801b6..1f86ad3430 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use yansi::{Paint, Style, Color::Primary}; use crate::log::PaintExt; -use crate::config::{LogLevel, Shutdown, Ident}; +use crate::config::{LogLevel, Shutdown, Ident, CliColors}; use crate::request::{self, Request, FromRequest}; use crate::http::uncased::Uncased; use crate::data::Limits; @@ -116,9 +116,8 @@ pub struct Config { pub shutdown: Shutdown, /// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)** pub log_level: LogLevel, - /// Whether to use colors and emoji when logging. **(default: `true`)** - #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] - pub cli_colors: bool, + /// Whether to use colors and emoji when logging. **(default: `auto`)** + pub cli_colors: CliColors, /// PRIVATE: This structure may grow (but never change otherwise) in a /// non-breaking release. As such, constructing this structure should /// _always_ be done using a public constructor or update syntax: @@ -198,7 +197,7 @@ impl Config { secret_key: SecretKey::zero(), shutdown: Shutdown::default(), log_level: LogLevel::Normal, - cli_colors: true, + cli_colors: CliColors::Auto, __non_exhaustive: (), } } diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 7ea4c653c4..9049ba86ac 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -115,6 +115,7 @@ mod ident; mod config; mod shutdown; mod ip_header; +mod cli_colors; #[cfg(feature = "tls")] mod tls; @@ -129,6 +130,7 @@ pub use config::Config; pub use crate::log::LogLevel; pub use shutdown::Shutdown; pub use ident::Ident; +pub use cli_colors::CliColors; #[cfg(feature = "tls")] pub use tls::{TlsConfig, CipherSuite}; @@ -150,7 +152,7 @@ mod tests { use crate::log::LogLevel; use crate::data::{Limits, ToByteUnit}; - use crate::config::Config; + use crate::config::{Config, CliColors}; #[test] fn test_figment_is_default() { @@ -217,7 +219,7 @@ mod tests { ident: ident!("Something Cool"), keep_alive: 10, log_level: LogLevel::Off, - cli_colors: false, + cli_colors: CliColors::Never, ..Config::default() }); @@ -240,7 +242,7 @@ mod tests { ident: ident!("Something Else Cool"), keep_alive: 10, log_level: LogLevel::Off, - cli_colors: false, + cli_colors: CliColors::Never, ..Config::default() }); @@ -262,7 +264,117 @@ mod tests { workers: 20, keep_alive: 10, log_level: LogLevel::Off, - cli_colors: false, + cli_colors: CliColors::Never, + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = "never" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: CliColors::Never, + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = "auto" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: CliColors::Auto, + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = "always" + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: CliColors::Always, + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = true + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: CliColors::Auto, + ..Config::default() + }); + + jail.set_env("ROCKET_CONFIG", "Other.toml"); + jail.create_file("Other.toml", r#" + [default] + address = "1.2.3.4" + port = 1234 + workers = 20 + keep_alive = 10 + log_level = "off" + cli_colors = 1 + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config, Config { + address: Ipv4Addr::new(1, 2, 3, 4).into(), + port: 1234, + workers: 20, + keep_alive: 10, + log_level: LogLevel::Off, + cli_colors: CliColors::Auto, ..Config::default() }); diff --git a/core/lib/src/log.rs b/core/lib/src/log.rs index 2b48dd04ec..e114392485 100644 --- a/core/lib/src/log.rs +++ b/core/lib/src/log.rs @@ -7,6 +7,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use serde::{de, Serialize, Serializer, Deserialize, Deserializer}; use yansi::{Paint, Painted, Condition}; +use crate::config::CliColors; + /// Reexport the `log` crate as `private`. pub use log as private; @@ -167,7 +169,12 @@ pub(crate) fn init(config: &crate::Config) { } // Always disable colors if requested or if the stdout/err aren't TTYs. - let should_color = config.cli_colors && Condition::stdouterr_are_tty(); + let should_color = match config.cli_colors { + CliColors::Always => true, + CliColors::Auto => Condition::stdouterr_are_tty(), + CliColors::Never => false + }; + yansi::whenever(Condition::cached(should_color)); // Set Rocket-logger specific settings only if Rocket's logger is set. From 7cf8b1368ff997417ba9a6ec9fd96feea9a7100e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 18 Dec 2023 16:33:04 -0800 Subject: [PATCH 055/178] Doc, fix, and test 'cli_colors' deserialization. --- benchmarks/src/routing.rs | 4 +- core/lib/fuzz/targets/collision-matching.rs | 3 +- core/lib/src/config/cli_colors.rs | 66 +++++---- core/lib/src/config/config.rs | 3 +- core/lib/src/config/mod.rs | 147 +++++++++----------- core/lib/src/log.rs | 10 +- site/guide/9-configuration.md | 3 +- 7 files changed, 119 insertions(+), 117 deletions(-) diff --git a/benchmarks/src/routing.rs b/benchmarks/src/routing.rs index d5ab63280d..89dd4e5ca6 100644 --- a/benchmarks/src/routing.rs +++ b/benchmarks/src/routing.rs @@ -2,7 +2,7 @@ use std::collections::hash_set::HashSet; use criterion::{criterion_group, Criterion}; -use rocket::{route, config::{self, CliColors}, Request, Data, Route, Config}; +use rocket::{route, config, Request, Data, Route, Config}; use rocket::http::{Method, RawStr, ContentType, Accept, Status}; use rocket::local::blocking::{Client, LocalRequest}; @@ -81,7 +81,7 @@ fn client(routes: Vec) -> Client { let config = Config { profile: Config::RELEASE_PROFILE, log_level: rocket::config::LogLevel::Off, - cli_colors: CliColors::Never, + cli_colors: config::CliColors::Never, shutdown: config::Shutdown { ctrlc: false, #[cfg(unix)] diff --git a/core/lib/fuzz/targets/collision-matching.rs b/core/lib/fuzz/targets/collision-matching.rs index a478bc4260..2af226ac02 100644 --- a/core/lib/fuzz/targets/collision-matching.rs +++ b/core/lib/fuzz/targets/collision-matching.rs @@ -6,7 +6,6 @@ use rocket::http::QMediaType; use rocket::local::blocking::{LocalRequest, Client}; use rocket::http::{Method, Accept, ContentType, MediaType, uri::Origin}; use rocket::route::{Route, RouteUri, dummy_handler}; -use rocket::config::CliColors; #[derive(Arbitrary)] struct ArbitraryRequestData<'a> { @@ -187,7 +186,7 @@ fn fuzz((route_a, route_b, req): TestData<'_>) { let rocket = rocket::custom(rocket::Config { workers: 2, log_level: rocket::log::LogLevel::Off, - cli_colors: CliColors::Never, + cli_colors: rocket::config::CliColors::Never, ..rocket::Config::debug_default() }); diff --git a/core/lib/src/config/cli_colors.rs b/core/lib/src/config/cli_colors.rs index 863104e245..94dc62e77c 100644 --- a/core/lib/src/config/cli_colors.rs +++ b/core/lib/src/config/cli_colors.rs @@ -1,26 +1,43 @@ -use core::fmt; -use serde::{ - de::{self, Unexpected::{Signed, Str}}, - Deserialize, Serialize -}; +use std::fmt; -/// Configure color output for logging. -#[derive(Clone, Serialize, PartialEq, Debug, Default)] +use serde::{de, Deserialize, Serialize}; + +/// Enable or disable coloring when logging. +/// +/// Valid configuration values are: +/// +/// * `"always"` - [`CliColors::Always`] +/// * `"auto"`, `1`, or `true` - [`CliColors::Auto`] _(default)_ +/// * `"never"`, `0`, or `false` - [`CliColors::Never`] +#[derive(Debug, Copy, Clone, Default, Serialize, PartialEq, Eq, Hash)] pub enum CliColors { - /// Always use colors in logs. + /// Always enable colors, irrespective of `stdout` and `stderr`. + /// + /// Case-insensitive string values of `"always"` parse as this value. Always, - /// Use colors in logs if the terminal supports it. + /// Enable colors _only if_ `stdout` and `stderr` support coloring. + /// + /// Case-insensitive string values of `"auto"`, the boolean `true`, and the + /// integer `1` all parse as this value. + /// + /// Only Unix-like systems (Linux, macOS, BSD, etc.), this is equivalent to + /// checking if `stdout` and `stderr` are both TTYs. On Windows, the console + /// is queried for ANSI escape sequence based coloring support and enabled + /// if support is successfully enabled. #[default] Auto, - /// Never use colors in logs. - Never + /// Never enable colors, even if `stdout` and `stderr` support them. + /// + /// Case-insensitive string values of `"never"`, the boolean `false`, and + /// the integer `0` all parse as this value. + Never, } impl fmt::Display for CliColors { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { + match self { CliColors::Always => write!(f, "always"), CliColors::Auto => write!(f, "auto"), CliColors::Never => write!(f, "never") @@ -28,7 +45,6 @@ impl fmt::Display for CliColors { } } - impl<'de> Deserialize<'de> for CliColors { fn deserialize>(de: D) -> Result { struct Visitor; @@ -37,7 +53,7 @@ impl<'de> Deserialize<'de> for CliColors { type Value = CliColors; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("0, 1, false, true, always, auto, never") + f.write_str("0, 1, false, true, always, auto, or never") } fn visit_str(self, val: &str) -> Result { @@ -46,31 +62,33 @@ impl<'de> Deserialize<'de> for CliColors { "false" => Ok(CliColors::Never), "1" => Ok(CliColors::Auto), "0" => Ok(CliColors::Never), + "always" => Ok(CliColors::Always), "auto" => Ok(CliColors::Auto), "never" => Ok(CliColors::Never), - "always" => Ok(CliColors::Always), - _ => Err(E::invalid_value( - Str(val), - &"0, 1, false, true, always, auto, never", - )) + _ => Err(E::invalid_value(de::Unexpected::Str(val), &self)), } } fn visit_bool(self, val: bool) -> Result { match val { true => Ok(CliColors::Auto), - false => Ok(CliColors::Never) + false => Ok(CliColors::Never), } } fn visit_i64(self, val: i64) -> Result { match val { + 1 => Ok(CliColors::Auto), 0 => Ok(CliColors::Never), + _ => Err(E::invalid_value(de::Unexpected::Signed(val), &self)), + } + } + + fn visit_u64(self, val: u64) -> Result { + match val { 1 => Ok(CliColors::Auto), - _ => Err(E::invalid_value( - Signed(val), - &"0, 1, false, true, always, auto, never", - )) + 0 => Ok(CliColors::Never), + _ => Err(E::invalid_value(de::Unexpected::Unsigned(val), &self)), } } } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 1f86ad3430..6f69b85d6f 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -116,7 +116,8 @@ pub struct Config { pub shutdown: Shutdown, /// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)** pub log_level: LogLevel, - /// Whether to use colors and emoji when logging. **(default: `auto`)** + /// Whether to use colors and emoji when logging. **(default: + /// [`CliColors::Auto`])** pub cli_colors: CliColors, /// PRIVATE: This structure may grow (but never change otherwise) in a /// non-breaking release. As such, constructing this structure should diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 9049ba86ac..07286fc1d6 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -268,118 +268,103 @@ mod tests { ..Config::default() }); - jail.set_env("ROCKET_CONFIG", "Other.toml"); - jail.create_file("Other.toml", r#" + Ok(()) + }); + } + + #[test] + fn test_cli_colors() { + figment::Jail::expect_with(|jail| { + jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" - port = 1234 - workers = 20 - keep_alive = 10 - log_level = "off" cli_colors = "never" "#)?; let config = Config::from(Config::figment()); - assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, - workers: 20, - keep_alive: 10, - log_level: LogLevel::Off, - cli_colors: CliColors::Never, - ..Config::default() - }); + assert_eq!(config.cli_colors, CliColors::Never); - jail.set_env("ROCKET_CONFIG", "Other.toml"); - jail.create_file("Other.toml", r#" + jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" - port = 1234 - workers = 20 - keep_alive = 10 - log_level = "off" cli_colors = "auto" "#)?; let config = Config::from(Config::figment()); - assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, - workers: 20, - keep_alive: 10, - log_level: LogLevel::Off, - cli_colors: CliColors::Auto, - ..Config::default() - }); + assert_eq!(config.cli_colors, CliColors::Auto); - jail.set_env("ROCKET_CONFIG", "Other.toml"); - jail.create_file("Other.toml", r#" + jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" - port = 1234 - workers = 20 - keep_alive = 10 - log_level = "off" cli_colors = "always" "#)?; let config = Config::from(Config::figment()); - assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, - workers: 20, - keep_alive: 10, - log_level: LogLevel::Off, - cli_colors: CliColors::Always, - ..Config::default() - }); + assert_eq!(config.cli_colors, CliColors::Always); - jail.set_env("ROCKET_CONFIG", "Other.toml"); - jail.create_file("Other.toml", r#" + jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" - port = 1234 - workers = 20 - keep_alive = 10 - log_level = "off" cli_colors = true "#)?; let config = Config::from(Config::figment()); - assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, - workers: 20, - keep_alive: 10, - log_level: LogLevel::Off, - cli_colors: CliColors::Auto, - ..Config::default() - }); + assert_eq!(config.cli_colors, CliColors::Auto); - jail.set_env("ROCKET_CONFIG", "Other.toml"); - jail.create_file("Other.toml", r#" + jail.create_file("Rocket.toml", r#" + [default] + cli_colors = false + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Never); + + jail.create_file("Rocket.toml", r#"[default]"#)?; + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Auto); + + jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" - port = 1234 - workers = 20 - keep_alive = 10 - log_level = "off" cli_colors = 1 "#)?; let config = Config::from(Config::figment()); - assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, - workers: 20, - keep_alive: 10, - log_level: LogLevel::Off, - cli_colors: CliColors::Auto, - ..Config::default() - }); + assert_eq!(config.cli_colors, CliColors::Auto); + + jail.create_file("Rocket.toml", r#" + [default] + cli_colors = 0 + "#)?; + + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Never); + + jail.set_env("ROCKET_CLI_COLORS", 1); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Auto); + + jail.set_env("ROCKET_CLI_COLORS", 0); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Never); + + jail.set_env("ROCKET_CLI_COLORS", true); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Auto); + + jail.set_env("ROCKET_CLI_COLORS", false); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Never); + + jail.set_env("ROCKET_CLI_COLORS", "always"); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Always); + + jail.set_env("ROCKET_CLI_COLORS", "NEveR"); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Never); + + jail.set_env("ROCKET_CLI_COLORS", "auTO"); + let config = Config::from(Config::figment()); + assert_eq!(config.cli_colors, CliColors::Auto); Ok(()) - }); + }) } #[test] diff --git a/core/lib/src/log.rs b/core/lib/src/log.rs index e114392485..cfd529a2fd 100644 --- a/core/lib/src/log.rs +++ b/core/lib/src/log.rs @@ -7,8 +7,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use serde::{de, Serialize, Serializer, Deserialize, Deserializer}; use yansi::{Paint, Painted, Condition}; -use crate::config::CliColors; - /// Reexport the `log` crate as `private`. pub use log as private; @@ -170,12 +168,12 @@ pub(crate) fn init(config: &crate::Config) { // Always disable colors if requested or if the stdout/err aren't TTYs. let should_color = match config.cli_colors { - CliColors::Always => true, - CliColors::Auto => Condition::stdouterr_are_tty(), - CliColors::Never => false + crate::config::CliColors::Always => Condition::ALWAYS, + crate::config::CliColors::Auto => Condition::DEFAULT, + crate::config::CliColors::Never => Condition::NEVER, }; - yansi::whenever(Condition::cached(should_color)); + yansi::whenever(should_color); // Set Rocket-logger specific settings only if Rocket's logger is set. if ROCKET_LOGGER_SET.load(Ordering::Acquire) { diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 230d345bac..76b14a9a0f 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -27,7 +27,7 @@ values: | `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` | | `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | | `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | -| `cli_colors` | `bool` | Whether to use colors and emoji when logging. | `true` | +| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` | | `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` | | `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` | | `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] | @@ -68,6 +68,7 @@ profile supplant any values with the same name in any profile. [`Limits`]: @api/rocket/data/struct.Limits.html [`Limits::default()`]: @api/rocket/data/struct.Limits.html#impl-Default [`SecretKey`]: @api/rocket/config/struct.SecretKey.html +[`CliColors`]: @api/rocket/config/enum.CliColors.html [`TlsConfig`]: @api/rocket/config/struct.TlsConfig.html [`Shutdown`]: @api/rocket/config/struct.Shutdown.html [`Shutdown::default()`]: @api/rocket/config/struct.Shutdown.html#fields From 3690412aba548d6206c6d3eef78ec0ef0ecb7cc9 Mon Sep 17 00:00:00 2001 From: Failpark Date: Thu, 30 Nov 2023 12:29:20 +0100 Subject: [PATCH 056/178] Fix fairing naming in database MySQL example. --- examples/databases/src/diesel_mysql.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/databases/src/diesel_mysql.rs b/examples/databases/src/diesel_mysql.rs index a09db8b151..19d6132e63 100644 --- a/examples/databases/src/diesel_mysql.rs +++ b/examples/databases/src/diesel_mysql.rs @@ -90,7 +90,7 @@ async fn destroy(mut db: Connection) -> Result<()> { } pub fn stage() -> AdHoc { - AdHoc::on_ignite("Diesel SQLite Stage", |rocket| async { + AdHoc::on_ignite("Diesel MySQL Stage", |rocket| async { rocket.attach(Db::init()) .mount("/diesel-async", routes![list, read, create, delete, destroy]) }) From ae7e0040e86e4df7ffd5e04beef061a85787b11b Mon Sep 17 00:00:00 2001 From: Martyn Date: Wed, 6 Dec 2023 21:57:06 +0000 Subject: [PATCH 057/178] Document "reconnect ad-infinitum" SSE pitfall. --- core/lib/src/response/stream/sse.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/lib/src/response/stream/sse.rs b/core/lib/src/response/stream/sse.rs index ae2445795f..27044e27af 100644 --- a/core/lib/src/response/stream/sse.rs +++ b/core/lib/src/response/stream/sse.rs @@ -475,6 +475,16 @@ impl Event { /// /// To send messages losslessly, they must be encoded first, for instance, by /// using [`Event::json()`]. +/// +/// * **Clients reconnect ad-infinitum** +/// +/// The [SSE standard] stipulates: _"Clients will reconnect if the connection +/// is closed; a client can be told to stop reconnecting using the HTTP 204 +/// No Content response code."_ As a result, clients will typically reconnect +/// exhaustively until either they choose to disconnect or they receive a +/// `204 No Content` response. +/// +/// [SSE standard]: https://html.spec.whatwg.org/multipage/server-sent-events.html pub struct EventStream { stream: S, heartbeat: Option, From 67ad8316dc081ff0cb2451bf9b9c405e02567940 Mon Sep 17 00:00:00 2001 From: Paul Lietar Date: Fri, 8 Dec 2023 15:31:59 +0000 Subject: [PATCH 058/178] Ensure 'TempFile' flushes when persisted. Tokio's `File::write_all()` method has an unexpected quirk: it doesn't actually write all the requested content to the file when the returned future resolves. Instead, the write is attempted and queued. This means that the `persist()` method can resolve without the data being persisted to the file system. Subsequent reads of the ostensibly written-to file can thus fail to contain the expected data. An call to `flush()` following `write_all()` would circumvent the issue. Alternatively, calling `fs::write()` actually writes to the file system before returning and requires fewer lines of code. This commit thus swaps the call to `write_all()` with `fs::write()`. --- core/lib/src/fs/temp_file.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/lib/src/fs/temp_file.rs b/core/lib/src/fs/temp_file.rs index fe969e7e9c..95ca53a1dc 100644 --- a/core/lib/src/fs/temp_file.rs +++ b/core/lib/src/fs/temp_file.rs @@ -10,7 +10,7 @@ use crate::fs::FileName; use tokio::task; use tokio::fs::{self, File}; -use tokio::io::{AsyncWriteExt, AsyncBufRead, BufReader}; +use tokio::io::{AsyncBufRead, BufReader}; use tempfile::{NamedTempFile, TempPath}; use either::Either; @@ -189,8 +189,7 @@ impl<'v> TempFile<'v> { } } TempFile::Buffered { content } => { - let mut file = File::create(&new_path).await?; - file.write_all(content).await?; + fs::write(&new_path, &content).await?; *self = TempFile::File { file_name: None, content_type: None, @@ -228,10 +227,12 @@ impl<'v> TempFile<'v> { /// # let some_other_path = std::env::temp_dir().join("some-other.txt"); /// file.copy_to(&some_other_path).await?; /// assert_eq!(file.path(), Some(&*some_path)); + /// # assert_eq!(std::fs::read(some_path).unwrap(), b"hi"); + /// # assert_eq!(std::fs::read(some_other_path).unwrap(), b"hi"); /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; + /// # let file = TempFile::Buffered { content: b"hi" }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub async fn copy_to

    (&mut self, path: P) -> io::Result<()> @@ -257,8 +258,7 @@ impl<'v> TempFile<'v> { } TempFile::Buffered { content } => { let path = path.as_ref(); - let mut file = File::create(path).await?; - file.write_all(content).await?; + fs::write(&path, &content).await?; *self = TempFile::File { file_name: None, content_type: None, @@ -393,10 +393,11 @@ impl<'v> TempFile<'v> { /// # let some_path = std::env::temp_dir().join("some-path.txt"); /// file.persist_to(&some_path).await?; /// assert_eq!(file.path(), Some(&*some_path)); + /// # assert_eq!(std::fs::read(some_path).unwrap(), b"hi"); /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; + /// # let file = TempFile::Buffered { content: b"hi" }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub fn path(&self) -> Option<&Path> { From 3e33cfe37c7221850737c4559fc5fb0d0d49aac2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 22 Dec 2023 23:42:55 -0800 Subject: [PATCH 059/178] Remove custom 'docify!' macro: use '#[doc]`. --- core/http/src/docify.rs | 76 -------------------- core/http/src/header/content_type.rs | 87 +++++++++++------------ core/http/src/header/media_type.rs | 100 +++++++++++++-------------- core/http/src/lib.rs | 3 - 4 files changed, 91 insertions(+), 175 deletions(-) delete mode 100644 core/http/src/docify.rs diff --git a/core/http/src/docify.rs b/core/http/src/docify.rs deleted file mode 100644 index c38d894f30..0000000000 --- a/core/http/src/docify.rs +++ /dev/null @@ -1,76 +0,0 @@ -macro_rules! docify { - ([$($doc:tt)*]; $($tt:tt)*) => { - docify!([$($doc)*] [] $($tt)*); - }; - - // FIXME: Treat $a just like everywhere else. What if we start with @[]? - ([$a:tt $($b:tt)*] [] $($tt:tt)*) => { - docify!([$($b)*] [stringify!($a), " "] $($tt)*); - }; - - ([@fence @$lang:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, "\n\n```", stringify!($lang), "\n"] $($tt)*); - }; - - ([@fence $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, "\n\n```\n"] $($tt)*); - }; - - ([@{$($a:tt),*}! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, $($a),*] $($tt)*); - }; - - ([@{$($a:tt),*} $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, $($a),*, " "] $($tt)*); - }; - - ([@code{$($a:tt)+}! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, "`", $(stringify!($a)),*, "`"] $($tt)*); - }; - - ([@code{$($a:tt)+} $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, "`", $(stringify!($a)),*, "` "] $($tt)*); - }; - - ([@[$($a:tt)*]! $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, $(stringify!($a)),*] $($tt)*); - }; - - ([@[$($a:tt)*] $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, $(stringify!($a)),*, " "] $($tt)*); - }; - - ([@nl $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, "\n"] $($tt)*); - }; - - (@punct [$a:tt $p:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, stringify!($a), stringify!($p), " "] $($tt)*); - }; - - (@upunct [$a:tt $p:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, stringify!($a), stringify!($p)] $($tt)*); - }; - - ([$a:tt . $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a . $($b)*] $($rest)+); }; - ([$a:tt , $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a , $($b)*] $($rest)+); }; - ([$a:tt ; $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ; $($b)*] $($rest)+); }; - ([$a:tt : $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a : $($b)*] $($rest)+); }; - ([$a:tt ! $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ! $($b)*] $($rest)+); }; - ([$a:tt ! $($b:tt)*] $($rest:tt)+) => { docify!(@punct [$a ! $($b)*] $($rest)+); }; - - ([$a:tt :: $($b:tt)*] $($rest:tt)+) => { docify!(@upunct [$a :: $($b)*] $($rest)+); }; - - ([$a:tt $($b:tt)*] [$($c:tt)+] $($tt:tt)*) => { - docify!([$($b)*] [$($c)+, stringify!($a), " "] $($tt)*); - }; - - ([] [$($doc:expr),*] $($tt:tt)*) => { - docify!(concat!($($doc),*), $($tt)*); - }; - - ($x:expr, $($tt:tt)*) => { - #[doc = $x] - $($tt)* - }; -} diff --git a/core/http/src/header/content_type.rs b/core/http/src/header/content_type.rs index afa2ede206..f7394ab597 100644 --- a/core/http/src/header/content_type.rs +++ b/core/http/src/header/content_type.rs @@ -44,33 +44,37 @@ use crate::ext::IntoCollection; pub struct ContentType(pub MediaType); macro_rules! content_types { - ($($name:ident ($check:ident): $str:expr, $t:expr, - $s:expr $(; $k:expr => $v:expr)*,)+) => { + ( + $( + $name:ident ($check:ident): $str:expr, + $t:expr, $s:expr $(; $k:expr => $v:expr)*, + )+ + ) => { $( - docify!([ - Content Type for @{"**"}! @{$str}! @{"**"}!: @{"`"} @{$t}! @[/]! @{$s}! - $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. - ]; - #[allow(non_upper_case_globals)] - pub const $name: ContentType = ContentType(MediaType::$name); - ); + + /// Content Type for + #[doc = concat!("**", $str, "**: ")] + #[doc = concat!("`", $t, "/", $s, $("; ", $k, "=", $v,)* "`")] + + #[allow(non_upper_case_globals)] + pub const $name: ContentType = ContentType(MediaType::$name); )+ }} macro_rules! from_extension { ($($ext:expr => $name:ident,)*) => ( - docify!([ - Returns the @[Content-Type] associated with the extension @code{ext}. - Not all extensions are recognized. If an extensions is not recognized, - @code{None} is returned. The currently recognized extensions are: - - @nl - $(* @{$ext} - @{"`ContentType::"}! @[$name]! @{"`"} @nl)* - @nl - - This list is likely to grow. Extensions are matched - @[case-insensitively.] - ]; + /// Returns the Content-Type associated with the extension `ext`. + /// + /// Extensions are matched case-insensitively. Not all extensions are + /// recognized. If an extensions is not recognized, `None` is returned. + /// The currently recognized extensions are: + /// + $( + #[doc = concat!("* ", $ext, " - [`ContentType::", stringify!($name), "`]")] + )* + /// + /// This list is likely to grow. + /// /// # Example /// /// Recognized content types: @@ -99,19 +103,19 @@ macro_rules! from_extension { pub fn from_extension(ext: &str) -> Option { MediaType::from_extension(ext).map(ContentType) } - );) + ) } macro_rules! extension { ($($ext:expr => $name:ident,)*) => ( - docify!([ - Returns the most common file extension associated with the - @[Content-Type] @code{self} if it is known. Otherwise, returns - @code{None}. The currently recognized extensions are identical to those - in @{"[`ContentType::from_extension()`]"} with the @{"most common"} - extension being the first extension appearing in the list for a given - @[Content-Type]. - ]; + /// Returns the most common file extension associated with the + /// Content-Type `self` if it is known. Otherwise, returns `None`. + /// + /// The currently recognized extensions are identical to those in + /// [`ContentType::from_extension()`] with the most common extension + /// being the first extension appearing in the list for a given + /// Content-Type. + /// /// # Example /// /// Known extension: @@ -140,23 +144,20 @@ macro_rules! extension { $(if self == &ContentType::$name { return Some($ext.into()) })* None } - );) + ) } macro_rules! parse_flexible { ($($short:expr => $name:ident,)*) => ( - docify!([ - Flexibly parses @code{name} into a @code{ContentType}. The parse is - @[_flexible_] because, in addition to strictly correct content types, it - recognizes the following shorthands: - - @nl - $(* $short - @{"`ContentType::"}! @[$name]! @{"`"} @nl)* - @nl - ]; + /// Flexibly parses `name` into a [`ContentType`]. The parse is + /// _flexible_ because, in addition to strictly correct content types, + /// it recognizes the following shorthands: + /// + $( + #[doc = concat!("* ", $short, " - [`ContentType::", stringify!($name), "`]")] + )* /// - /// For regular parsing, use the - /// [`ContentType::from_str()`](#impl-FromStr) method. + /// For regular parsing, use [`ContentType::from_str()`]. /// /// # Example /// @@ -205,7 +206,7 @@ macro_rules! parse_flexible { pub fn parse_flexible(name: &str) -> Option { MediaType::parse_flexible(name).map(ContentType) } - );) + ) } impl ContentType { diff --git a/core/http/src/header/media_type.rs b/core/http/src/header/media_type.rs index b764b7da8b..0d637a8180 100644 --- a/core/http/src/header/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -85,15 +85,13 @@ macro_rules! media_types { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) => { $( - docify!([ - Media Type for @{"**"}! @{$str}! @{"**"}!: @{"`"} @{$t}! @[/]! @{$s}! - $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. - ]; - #[allow(non_upper_case_globals)] - pub const $name: MediaType = MediaType::new_known( - concat!($t, "/", $s, $("; ", $k, "=", $v),*), - $t, $s, &[$(($k, $v)),*] - ); + /// Media Type for + #[doc = concat!("**", $str, "**: ")] + #[doc = concat!("`", $t, "/", $s, $("; ", $k, "=", $v,)* "`")] + #[allow(non_upper_case_globals)] + pub const $name: MediaType = MediaType::new_known( + concat!($t, "/", $s, $("; ", $k, "=", $v),*), + $t, $s, &[$(($k, $v)),*] ); )+ @@ -109,33 +107,32 @@ macro_rules! media_types { } $( - docify!([ - Returns @code{true} if the @[top-level] and sublevel types of - @code{self} are the same as those of @{"`MediaType::"}! $name - @{"`"}!, i.e @{"`"} @{$t}! @[/]! @{$s}! $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. - ]; - #[inline(always)] - pub fn $check(&self) -> bool { - *self == MediaType::$name - } - ); + /// Returns `true` if the top-level and sublevel types of + /// `self` are the same as those of + #[doc = concat!("`MediaType::", stringify!($name), "`, ")] + /// i.e + #[doc = concat!("`", $t, "/", $s, "`.")] + #[inline(always)] + pub fn $check(&self) -> bool { + *self == MediaType::$name + } )+ }} macro_rules! from_extension { ($($ext:expr => $name:ident,)*) => ( - docify!([ - Returns the @[Media-Type] associated with the extension @code{ext}. Not - all extensions are recognized. If an extensions is not recognized, - @code{None} is returned. The currently recognized extensions are: - - @nl - $(* @{$ext} - @{"`MediaType::"}! @[$name]! @{"`"} @nl)* - @nl - - This list is likely to grow. Extensions are matched - @[case-insensitively.] - ]; + /// Returns the Media Type associated with the extension `ext`. + /// + /// Extensions are matched case-insensitively. Not all extensions are + /// recognized. If an extensions is not recognized, `None` is returned. + /// The currently recognized extensions are: + /// + $( + #[doc = concat!("* ", $ext, " - [`MediaType::", stringify!($name), "`]")] + )* + /// + /// This list is likely to grow. + /// /// # Example /// /// Recognized media types: @@ -166,19 +163,18 @@ macro_rules! from_extension { _ => None } } - );) + ) } macro_rules! extension { ($($ext:expr => $name:ident,)*) => ( - docify!([ - Returns the most common file extension associated with the @[Media-Type] - @code{self} if it is known. Otherwise, returns @code{None}. The - currently recognized extensions are identical to those in - @{"[`MediaType::from_extension()`]"} with the @{"most common"} extension - being the first extension appearing in the list for a given - @[Media-Type]. - ]; + /// Returns the most common file extension associated with the + /// Media-Type `self` if it is known. Otherwise, returns `None`. + /// + /// The currently recognized extensions are identical to those in + /// [`MediaType::from_extension()`] with the most common extension being + /// the first extension appearing in the list for a given Content-Type. + /// /// # Example /// /// Known extension: @@ -207,22 +203,20 @@ macro_rules! extension { $(if self == &MediaType::$name { return Some($ext.into()) })* None } - );) + ) } macro_rules! parse_flexible { ($($short:expr => $name:ident,)*) => ( - docify!([ - Flexibly parses @code{name} into a @code{MediaType}. The parse is - @[_flexible_] because, in addition to strictly correct media types, it - recognizes the following shorthands: - - @nl - $(* $short - @{"`MediaType::"}! @[$name]! @{"`"} @nl)* - @nl - ]; - /// For regular parsing, use the - /// [`MediaType::from_str()`](#impl-FromStr) method. + /// Flexibly parses `name` into a [`MediaType`]. The parse is + /// _flexible_ because, in addition to strictly correct content types, + /// it recognizes the following shorthands: + /// + $( + #[doc = concat!("* ", $short, " - [`MediaType::", stringify!($name), "`]")] + )* + /// + /// For regular parsing, use [`MediaType::from_str()`]. /// /// # Example /// @@ -273,7 +267,7 @@ macro_rules! parse_flexible { _ => MediaType::from_str(name).ok(), } } - );) + ) } impl MediaType { diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 33a0028e68..bdafb2834b 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -18,9 +18,6 @@ pub mod hyper; pub mod uri; pub mod ext; -#[macro_use] -mod docify; - #[macro_use] mod header; mod method; From a285625f8019473f64031f0c038ea0b80136dbb2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 23 Dec 2023 22:08:40 -0800 Subject: [PATCH 060/178] Upgrade 'smallvec' to simplify 'Accept' impl. --- core/http/Cargo.toml | 2 +- core/http/src/ext.rs | 6 ----- core/http/src/header/accept.rs | 42 ++++++---------------------------- core/http/src/lib.rs | 2 -- 4 files changed, 8 insertions(+), 44 deletions(-) diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index a3537d7bfb..6c29fa1762 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -25,7 +25,7 @@ serde = ["uncased/with-serde-alloc", "serde_"] uuid = ["uuid_"] [dependencies] -smallvec = "1.0" +smallvec = { version = "1.11", features = ["const_generics", "const_new"] } percent-encoding = "2" http = "0.2" time = { version = "0.3", features = ["formatting", "macros"] } diff --git a/core/http/src/ext.rs b/core/http/src/ext.rs index e78012e5d2..65629c58d2 100644 --- a/core/http/src/ext.rs +++ b/core/http/src/ext.rs @@ -12,12 +12,6 @@ pub trait IntoCollection: Sized { #[doc(hidden)] fn mapped U, A: Array>(self, f: F) -> SmallVec; - - #[doc(hidden)] - fn mapped_vec U>(self, f: F) -> Vec { - let small = self.mapped::(f); - small.into_vec() - } } impl IntoCollection for T { diff --git a/core/http/src/header/accept.rs b/core/http/src/header/accept.rs index 305272a6ea..c38e4932db 100644 --- a/core/http/src/header/accept.rs +++ b/core/http/src/header/accept.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use std::fmt; use smallvec::SmallVec; -use either::Either; use crate::{Header, MediaType}; use crate::ext::IntoCollection; @@ -53,30 +52,21 @@ use crate::parse::parse_accept; /// let response = Response::build().header(Accept::JSON).finalize(); /// ``` #[derive(Debug, Clone)] -pub struct Accept(pub(crate) AcceptParams); +pub struct Accept(pub(crate) SmallVec<[QMediaType; 1]>); /// A `MediaType` with an associated quality value. #[derive(Debug, Clone, PartialEq)] pub struct QMediaType(pub MediaType, pub Option); -// NOTE: `Static` is needed for `const` items. Need `const SmallVec::new`. -#[derive(Debug, Clone)] -pub enum AcceptParams { - Static(QMediaType), - Dynamic(SmallVec<[QMediaType; 1]>) -} - macro_rules! accept_constructor { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) => { $( - #[doc="An `Accept` header with the single media type for "] - #[doc=$str] #[doc=": "] - #[doc=$t] #[doc="/"] #[doc=$s] - #[doc=""] + #[doc="An `Accept` header with the single media type for"] + #[doc=concat!("**", $str, "**: ", "_", $t, "/", $s, "_")] #[allow(non_upper_case_globals)] pub const $name: Accept = Accept( - AcceptParams::Static(QMediaType(MediaType::$name, None)) + SmallVec::from_const([QMediaType(MediaType::$name, None)]) ); )+ }; @@ -111,7 +101,7 @@ impl Accept { /// ``` #[inline(always)] pub fn new>(items: T) -> Accept { - Accept(AcceptParams::Dynamic(items.into_collection())) + Accept(items.into_collection()) } // TODO: Implement this. @@ -211,10 +201,7 @@ impl Accept { /// ``` #[inline(always)] pub fn iter(&self) -> impl Iterator + '_ { - match self.0 { - AcceptParams::Static(ref val) => Either::Left(Some(val).into_iter()), - AcceptParams::Dynamic(ref vec) => Either::Right(vec.iter()) - } + self.0.iter() } /// Returns an iterator over all of the (bare) media types in `self`. Media @@ -249,7 +236,7 @@ impl Accept { impl> From for Accept { #[inline(always)] fn from(items: T) -> Accept { - Accept(AcceptParams::Dynamic(items.mapped(|item| item.into()))) + Accept(items.mapped(|item| item.into())) } } @@ -361,21 +348,6 @@ impl Deref for QMediaType { } } -impl Default for AcceptParams { - fn default() -> Self { - AcceptParams::Dynamic(SmallVec::new()) - } -} - -impl Extend for AcceptParams { - fn extend>(&mut self, iter: T) { - match self { - AcceptParams::Static(..) => panic!("can't add to static collection!"), - AcceptParams::Dynamic(ref mut v) => v.extend(iter) - } - } -} - #[cfg(test)] mod test { use crate::{Accept, MediaType}; diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index bdafb2834b..7ab89758d6 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -1,5 +1,3 @@ -#![recursion_limit="512"] - #![warn(rust_2018_idioms)] #![warn(missing_docs)] From b3abc760aee8556f5b455f13aaa4a1c7ae70bd85 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 12 Dec 2023 13:08:49 -0800 Subject: [PATCH 061/178] Introduce chainable data transformers. Resolves #775. --- core/lib/src/data/data.rs | 132 ++++++++------ core/lib/src/data/data_stream.rs | 232 +++++++++++++---------- core/lib/src/data/mod.rs | 5 +- core/lib/src/data/peekable.rs | 48 +++++ core/lib/src/data/transform.rs | 304 +++++++++++++++++++++++++++++++ core/lib/src/fairing/ad_hoc.rs | 4 +- 6 files changed, 578 insertions(+), 147 deletions(-) create mode 100644 core/lib/src/data/peekable.rs create mode 100644 core/lib/src/data/transform.rs diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs index bbd2bb75a4..c54fa00d80 100644 --- a/core/lib/src/data/data.rs +++ b/core/lib/src/data/data.rs @@ -1,9 +1,10 @@ -use crate::tokio::io::AsyncReadExt; -use crate::data::data_stream::DataStream; -use crate::data::{ByteUnit, StreamReader}; +use std::io; +use std::pin::Pin; -/// The number of bytes to read into the "peek" buffer. -pub const PEEK_BYTES: usize = 512; +use crate::data::ByteUnit; +use crate::data::data_stream::{DataStream, RawReader, RawStream}; +use crate::data::peekable::Peekable; +use crate::data::transform::{Transform, TransformBuf, Inspect, InPlaceMap}; /// Type representing the body data of a request. /// @@ -38,31 +39,27 @@ pub const PEEK_BYTES: usize = 512; /// body data. This enables partially or fully reading from a `Data` object /// without consuming the `Data` object. pub struct Data<'r> { - buffer: Vec, - is_complete: bool, - stream: StreamReader<'r>, + stream: Peekable<512, RawReader<'r>>, + transforms: Vec>>, } +// TODO: Before `async`, we had a read timeout of 5s. Such a short read timeout +// is likely no longer necessary, but an idle timeout should be implemented. impl<'r> Data<'r> { - /// Create a `Data` from a recognized `stream`. - pub(crate) fn from>>(stream: S) -> Data<'r> { - // TODO.async: This used to also set the read timeout to 5 seconds. - // Such a short read timeout is likely no longer necessary, but some - // kind of idle timeout should be implemented. + #[inline] + pub(crate) fn new(stream: Peekable<512, RawReader<'r>>) -> Self { + Self { stream, transforms: Vec::new() } + } - let stream = stream.into(); - let buffer = Vec::with_capacity(PEEK_BYTES / 8); - Data { buffer, stream, is_complete: false } + #[inline] + pub(crate) fn from>>(stream: S) -> Data<'r> { + Data::new(Peekable::new(RawReader::new(stream.into()))) } /// This creates a `data` object from a local data source `data`. #[inline] pub(crate) fn local(data: Vec) -> Data<'r> { - Data { - buffer: data, - stream: StreamReader::empty(), - is_complete: true, - } + Data::new(Peekable::with_buffer(data, true, RawReader::new(RawStream::Empty))) } /// Returns the raw data stream, limited to `limit` bytes. @@ -82,18 +79,31 @@ impl<'r> Data<'r> { /// let stream = data.open(2.mebibytes()); /// } /// ``` + #[inline(always)] pub fn open(self, limit: ByteUnit) -> DataStream<'r> { - DataStream::new(self.buffer, self.stream, limit.into()) + DataStream::new(self.transforms, self.stream, limit.into()) } - /// Retrieve at most `num` bytes from the `peek` buffer without consuming - /// `self`. - /// - /// The peek buffer contains at most 512 bytes of the body of the request. - /// The actual size of the returned buffer is the `min` of the request's - /// body, `num` and `512`. The [`peek_complete`](#method.peek_complete) - /// method can be used to determine if this buffer contains _all_ of the - /// data in the body of the request. + /// Fills the peek buffer with body data until it contains at least `num` + /// bytes (capped to 512), or the complete body data, whichever is less, and + /// returns it. If the buffer already contains either at least `num` bytes + /// or all of the body data, no I/O is performed and the buffer is simply + /// returned. If `num` is greater than `512`, it is artificially capped to + /// `512`. + /// + /// No guarantees are made about the actual size of the returned buffer + /// except that it will not exceed the length of the body data. It may be: + /// + /// * Less than `num` if `num > 512` or the complete body data is `< 512` + /// or an error occurred while reading the body. + /// * Equal to `num` if `num` is `<= 512` and exactly `num` bytes of the + /// body data were successfully read. + /// * Greater than `num` if `> num` bytes of the body data have + /// successfully been read, either by this request, a previous request, + /// or opportunistically. + /// + /// [`Data::peek_complete()`] can be used to determine if this buffer + /// contains the complete body data. /// /// # Examples /// @@ -147,30 +157,13 @@ impl<'r> Data<'r> { /// } /// } /// ``` + #[inline(always)] pub async fn peek(&mut self, num: usize) -> &[u8] { - let num = std::cmp::min(PEEK_BYTES, num); - let mut len = self.buffer.len(); - if len >= num { - return &self.buffer[..num]; - } - - while len < num { - match self.stream.read_buf(&mut self.buffer).await { - Ok(0) => { self.is_complete = true; break }, - Ok(n) => len += n, - Err(e) => { - error_!("Failed to read into peek buffer: {:?}.", e); - break; - } - } - } - - &self.buffer[..std::cmp::min(len, num)] + self.stream.peek(num).await } /// Returns true if the `peek` buffer contains all of the data in the body - /// of the request. Returns `false` if it does not or if it is not known if - /// it does. + /// of the request. Returns `false` if it does not or it is not known. /// /// # Example /// @@ -185,6 +178,43 @@ impl<'r> Data<'r> { /// ``` #[inline(always)] pub fn peek_complete(&self) -> bool { - self.is_complete + self.stream.complete + } + + /// Chains the [`Transform`] `transform` to `self`. + /// + /// Note that transforms do nothing until the data is + /// [`open()`ed](Data::open()) and read. + #[inline(always)] + pub fn chain_transform(&mut self, transform: T) -> &mut Self + where T: Transform + Send + Sync + 'static + { + self.transforms.push(Box::pin(transform)); + self + } + + /// Chain a [`Transform`] that can inspect the data as it streams. + pub fn chain_inspect(&mut self, f: F) -> &mut Self + where F: FnMut(&[u8]) + Send + Sync + 'static + { + self.chain_transform(Inspect(Box::new(f))) + } + + /// Chain a [`Transform`] that can in-place map the data as it streams. + /// Unlike [`Data::chain_try_inplace_map()`], this version assumes the + /// mapper is infallible. + pub fn chain_inplace_map(&mut self, mut f: F) -> &mut Self + where F: FnMut(&mut TransformBuf<'_, '_>) + Send + Sync + 'static + { + self.chain_transform(InPlaceMap(Box::new(move |buf| Ok(f(buf))))) + } + + /// Chain a [`Transform`] that can in-place map the data as it streams. + /// Unlike [`Data::chain_inplace_map()`], this version allows the mapper to + /// be infallible. + pub fn chain_try_inplace_map(&mut self, f: F) -> &mut Self + where F: FnMut(&mut TransformBuf<'_, '_>) -> io::Result<()> + Send + Sync + 'static + { + self.chain_transform(InPlaceMap(Box::new(f))) } } diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs index a9a9e07ad7..f30f046e3b 100644 --- a/core/lib/src/data/data_stream.rs +++ b/core/lib/src/data/data_stream.rs @@ -5,13 +5,17 @@ use std::io::{self, Cursor}; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, ReadBuf, Take}; -use futures::stream::Stream; -use futures::ready; -use yansi::Paint; +use tokio_util::io::StreamReader; +use futures::{ready, stream::Stream}; use crate::http::hyper; use crate::ext::{PollExt, Chain}; use crate::data::{Capped, N}; +use crate::http::hyper::body::Bytes; +use crate::data::transform::Transform; + +use super::peekable::Peekable; +use super::transform::TransformBuf; /// Raw data stream of a request body. /// @@ -40,47 +44,101 @@ use crate::data::{Capped, N}; /// /// [`DataStream::stream_to(&mut vec)`]: DataStream::stream_to() /// [`DataStream::stream_to(&mut file)`]: DataStream::stream_to() -pub struct DataStream<'r> { - pub(crate) chain: Take>, StreamReader<'r>>>, +#[non_exhaustive] +pub enum DataStream<'r> { + #[doc(hidden)] + Base(BaseReader<'r>), + #[doc(hidden)] + Transform(TransformReader<'r>), } -/// An adapter: turns a `T: Stream` (in `StreamKind`) into a `tokio::AsyncRead`. -pub struct StreamReader<'r> { - state: State, - inner: StreamKind<'r>, +/// A data stream that has a `transformer` applied to it. +pub struct TransformReader<'r> { + transformer: Pin>, + stream: Pin>>, + inner_done: bool, } -/// The current state of `StreamReader` `AsyncRead` adapter. -enum State { - Pending, - Partial(Cursor), - Done, -} +/// Limited, pre-buffered reader to the underlying data stream. +pub type BaseReader<'r> = Take>, RawReader<'r>>>; -/// The kinds of streams we accept as `Data`. -enum StreamKind<'r> { +/// Direct reader to the underlying data stream. Not limited in any manner. +pub type RawReader<'r> = StreamReader, Bytes>; + +/// Raw underlying data stream. +pub enum RawStream<'r> { Empty, Body(&'r mut hyper::Body), - Multipart(multer::Field<'r>) + Multipart(multer::Field<'r>), +} + +impl<'r> TransformReader<'r> { + /// Returns the underlying `BaseReader`. + fn base_mut(&mut self) -> &mut BaseReader<'r> { + match self.stream.as_mut().get_mut() { + DataStream::Base(base) => base, + DataStream::Transform(inner) => inner.base_mut(), + } + } + + /// Returns the underlying `BaseReader`. + fn base(&self) -> &BaseReader<'r> { + match self.stream.as_ref().get_ref() { + DataStream::Base(base) => base, + DataStream::Transform(inner) => inner.base(), + } + } } impl<'r> DataStream<'r> { - pub(crate) fn new(buf: Vec, stream: StreamReader<'r>, limit: u64) -> Self { - let chain = Chain::new(Cursor::new(buf), stream).take(limit).into(); - Self { chain } + pub(crate) fn new( + transformers: Vec>>, + Peekable { buffer, reader, .. }: Peekable<512, RawReader<'r>>, + limit: u64 + ) -> Self { + let mut stream = DataStream::Base(Chain::new(Cursor::new(buffer), reader).take(limit)); + for transformer in transformers { + stream = DataStream::Transform(TransformReader { + transformer, + stream: Box::pin(stream), + inner_done: false, + }); + } + + stream + } + + /// Returns the underlying `BaseReader`. + fn base_mut(&mut self) -> &mut BaseReader<'r> { + match self { + DataStream::Base(base) => base, + DataStream::Transform(transform) => transform.base_mut(), + } + } + + /// Returns the underlying `BaseReader`. + fn base(&self) -> &BaseReader<'r> { + match self { + DataStream::Base(base) => base, + DataStream::Transform(transform) => transform.base(), + } } /// Whether a previous read exhausted the set limit _and then some_. async fn limit_exceeded(&mut self) -> io::Result { + let base = self.base_mut(); + #[cold] - async fn _limit_exceeded(stream: &mut DataStream<'_>) -> io::Result { + async fn _limit_exceeded(base: &mut BaseReader<'_>) -> io::Result { // Read one more byte after reaching limit to see if we cut early. - stream.chain.set_limit(1); + base.set_limit(1); let mut buf = [0u8; 1]; - Ok(stream.read(&mut buf).await? != 0) + let exceeded = base.read(&mut buf).await? != 0; + base.set_limit(0); + Ok(exceeded) } - Ok(self.chain.limit() == 0 && _limit_exceeded(self).await?) + Ok(base.limit() == 0 && _limit_exceeded(base).await?) } /// Number of bytes a full read from `self` will _definitely_ read. @@ -95,8 +153,9 @@ impl<'r> DataStream<'r> { /// } /// ``` pub fn hint(&self) -> usize { - let buf_len = self.chain.get_ref().get_ref().0.get_ref().len(); - std::cmp::min(buf_len, self.chain.limit() as usize) + let base = self.base(); + let buf_len = base.get_ref().get_ref().0.get_ref().len(); + std::cmp::min(buf_len, base.limit() as usize) } /// A helper method to write the body of the request to any `AsyncWrite` @@ -227,97 +286,86 @@ impl<'r> DataStream<'r> { } } -// TODO.async: Consider implementing `AsyncBufRead`. - -impl StreamReader<'_> { - pub fn empty() -> Self { - Self { inner: StreamKind::Empty, state: State::Done } - } -} - -impl<'r> From<&'r mut hyper::Body> for StreamReader<'r> { - fn from(body: &'r mut hyper::Body) -> Self { - Self { inner: StreamKind::Body(body), state: State::Pending } - } -} - -impl<'r> From> for StreamReader<'r> { - fn from(field: multer::Field<'r>) -> Self { - Self { inner: StreamKind::Multipart(field), state: State::Pending } +impl AsyncRead for DataStream<'_> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + DataStream::Base(inner) => Pin::new(inner).poll_read(cx, buf), + DataStream::Transform(inner) => Pin::new(inner).poll_read(cx, buf), + } } } -impl AsyncRead for DataStream<'_> { - #[inline(always)] +impl AsyncRead for TransformReader<'_> { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - if self.chain.limit() == 0 { - let stream: &StreamReader<'_> = &self.chain.get_ref().get_ref().1; - let kind = match stream.inner { - StreamKind::Empty => "an empty stream (vacuous)", - StreamKind::Body(_) => "the request body", - StreamKind::Multipart(_) => "a multipart form field", - }; - - warn_!("Data limit reached while reading {}.", kind.primary().bold()); + let init_fill = buf.filled().len(); + if !self.inner_done { + ready!(Pin::new(&mut self.stream).poll_read(cx, buf))?; + self.inner_done = init_fill == buf.filled().len(); + } + + if self.inner_done { + return self.transformer.as_mut().poll_finish(cx, buf); + } + + let mut tbuf = TransformBuf { buf, cursor: init_fill }; + self.transformer.as_mut().transform(&mut tbuf)?; + if buf.filled().len() == init_fill { + cx.waker().wake_by_ref(); + return Poll::Pending; } - Pin::new(&mut self.chain).poll_read(cx, buf) + Poll::Ready(Ok(())) } } -impl Stream for StreamKind<'_> { - type Item = io::Result; +impl Stream for RawStream<'_> { + type Item = io::Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - StreamKind::Body(body) => Pin::new(body).poll_next(cx) + RawStream::Body(body) => Pin::new(body).poll_next(cx) .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), - StreamKind::Multipart(mp) => Pin::new(mp).poll_next(cx) + RawStream::Multipart(mp) => Pin::new(mp).poll_next(cx) .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), - StreamKind::Empty => Poll::Ready(None), + RawStream::Empty => Poll::Ready(None), } } fn size_hint(&self) -> (usize, Option) { match self { - StreamKind::Body(body) => body.size_hint(), - StreamKind::Multipart(mp) => mp.size_hint(), - StreamKind::Empty => (0, Some(0)), + RawStream::Body(body) => body.size_hint(), + RawStream::Multipart(mp) => mp.size_hint(), + RawStream::Empty => (0, Some(0)), } } } -impl AsyncRead for StreamReader<'_> { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - loop { - self.state = match self.state { - State::Pending => { - match ready!(Pin::new(&mut self.inner).poll_next(cx)) { - Some(Err(e)) => return Poll::Ready(Err(e)), - Some(Ok(bytes)) => State::Partial(Cursor::new(bytes)), - None => State::Done, - } - }, - State::Partial(ref mut cursor) => { - let rem = buf.remaining(); - match ready!(Pin::new(cursor).poll_read(cx, buf)) { - Ok(()) if rem == buf.remaining() => State::Pending, - result => return Poll::Ready(result), - } - } - State::Done => return Poll::Ready(Ok(())), - } +impl std::fmt::Display for RawStream<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RawStream::Empty => f.write_str("empty stream"), + RawStream::Body(_) => f.write_str("request body"), + RawStream::Multipart(_) => f.write_str("multipart form field"), } } } + +impl<'r> From<&'r mut hyper::Body> for RawStream<'r> { + fn from(value: &'r mut hyper::Body) -> Self { + Self::Body(value) + } +} + +impl<'r> From> for RawStream<'r> { + fn from(value: multer::Field<'r>) -> Self { + Self::Multipart(value) + } +} diff --git a/core/lib/src/data/mod.rs b/core/lib/src/data/mod.rs index 9c7a331478..e3eebdd23c 100644 --- a/core/lib/src/data/mod.rs +++ b/core/lib/src/data/mod.rs @@ -7,6 +7,8 @@ mod data_stream; mod from_data; mod limits; mod io_stream; +mod transform; +mod peekable; pub use self::data::Data; pub use self::data_stream::DataStream; @@ -15,5 +17,4 @@ pub use self::limits::Limits; pub use self::capped::{N, Capped}; pub use self::io_stream::{IoHandler, IoStream}; pub use ubyte::{ByteUnit, ToByteUnit}; - -pub(crate) use self::data_stream::StreamReader; +pub use self::transform::{Transform, TransformBuf}; diff --git a/core/lib/src/data/peekable.rs b/core/lib/src/data/peekable.rs new file mode 100644 index 0000000000..58daac705b --- /dev/null +++ b/core/lib/src/data/peekable.rs @@ -0,0 +1,48 @@ +use tokio::io::{AsyncRead, AsyncReadExt}; + +pub struct Peekable { + pub(crate) buffer: Vec, + pub(crate) complete: bool, + pub(crate) reader: R, +} + +impl Peekable { + pub fn new(reader: R) -> Self { + Self { buffer: Vec::new(), complete: false, reader } + } + + pub fn with_buffer(buffer: Vec, complete: bool, reader: R) -> Self { + Self { buffer, complete, reader } + } + + pub async fn peek(&mut self, num: usize) -> &[u8] { + if self.complete { + return self.buffer.as_slice(); + } + + let to_read = std::cmp::min(N, num); + if self.buffer.len() >= to_read { + return &self.buffer.as_slice(); + } + + if self.buffer.capacity() == 0 { + self.buffer.reserve(N); + } + + while self.buffer.len() < to_read { + match self.reader.read_buf::>(&mut self.buffer).await { + Ok(0) => { + self.complete = self.buffer.capacity() > self.buffer.len(); + break; + }, + Ok(_) => { /* continue */ }, + Err(e) => { + error_!("Failed to read into peek buffer: {:?}.", e); + break; + } + } + } + + self.buffer.as_slice() + } +} diff --git a/core/lib/src/data/transform.rs b/core/lib/src/data/transform.rs new file mode 100644 index 0000000000..e3be992c76 --- /dev/null +++ b/core/lib/src/data/transform.rs @@ -0,0 +1,304 @@ +use std::io; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::task::{Poll, Context}; + +use tokio::io::ReadBuf; + +/// Chainable, in-place, streaming data transformer. +/// +/// [`Transform`] operates on [`TransformBuf`]s similar to how [`AsyncRead`] +/// operats on [`ReadBuf`]. A [`Transform`] sits somewhere in a chain of +/// transforming readers. The head (most upstream part) of the chain is _always_ +/// an [`AsyncRead`]: the data source. The tail (all downstream parts) is +/// composed _only_ of other [`Transform`]s: +/// +/// ```text +/// downstream ---> +/// AsyncRead | Transform | .. | Transform +/// <---- upstream +/// ``` +/// +/// When the upstream source makes data available, the +/// [`Transform::transform()`] method is called. [`Transform`]s may obtain the +/// subset of the filled section added by an upstream data source with +/// [`TransformBuf::fresh()`]. They may modify this data at will, potentially +/// changing the size of the filled section. For example, +/// [`TransformBuf::spoil()`] "removes" all of the fresh data, and +/// [`TransformBuf::fresh_mut()`] can be used to modify the data in-place. +/// +/// Additionally, new data may be added in-place via the traditional approach: +/// write to (or overwrite) the initialized section of the buffer and mark it as +/// filled. All of the remaining filled data will be passed to downstream +/// transforms as "fresh" data. To add data to the end of the (potentially +/// rewritten) stream, the [`Transform::poll_finish()`] method can be +/// implemented. +/// +/// [`AsyncRead`]: tokio::io::AsyncRead +pub trait Transform { + /// Called when data is read from the upstream source. For any given fresh + /// data, this method is called only once. [`TransformBuf::fresh()`] is + /// guaranteed to contain at least one byte. + /// + /// While this method is not _async_ (it does not return [`Poll`]), it is + /// nevertheless executed in an async context and should respect all such + /// restrictions including not blocking. + fn transform( + self: Pin<&mut Self>, + buf: &mut TransformBuf<'_, '_>, + ) -> io::Result<()>; + + /// Called when the upstream is finished, that is, it has no more data to + /// fill. At this point, the transform becomes an async reader. This method + /// thus has identical semantics to [`AsyncRead::poll_read()`]. This method + /// may never be called if the upstream does not finish. + /// + /// The default implementation returns `Poll::Ready(Ok(()))`. + /// + /// [`AsyncRead::poll_read()`]: tokio::io::AsyncRead::poll_read() + fn poll_finish( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let (_, _) = (cx, buf); + Poll::Ready(Ok(())) + } +} + +/// A buffer of transformable streaming data. +/// +/// # Overview +/// +/// A byte buffer, similar to a [`ReadBuf`], with a "fresh" dimension. Fresh +/// data is always a subset of the filled data, filled data is always a subset +/// of initialized data, and initialized data is always a subset of the buffer +/// itself. Both the filled and initialized data sections are guaranteed to be +/// at the start of the buffer, but the fresh subset is likely to begin +/// somewhere inside the filled section. +/// +/// To visualize this, the diagram below represents a possible state for the +/// byte buffer being tracked. The square `[ ]` brackets represent the complete +/// buffer, while the curly `{ }` represent the named subset. +/// +/// ```text +/// [ { !! fresh !! } ] +/// { +++ filled +++ } unfilled ] +/// { ----- initialized ------ } uninitialized ] +/// [ capacity ] +/// ``` +/// +/// The same buffer represented in its true single dimension is below: +/// +/// ```text +/// [ ++!!!!!!!!!!!!!!---------xxxxxxxxxxxxxxxxxxxxxxxx] +/// ``` +/// +/// * `+`: filled (implies initialized) +/// * `!`: fresh (implies filled) +/// * `-`: unfilled / initialized (implies initialized) +/// * `x`: uninitialized (implies unfilled) +/// +/// As with [`ReadBuf`], [`AsyncRead`] readers fill the initialized portion of a +/// [`TransformBuf`] to indicate that data is available. _Filling_ initialized +/// portions of the byte buffers is what increases the size of the _filled_ +/// section. Because a [`ReadBuf`] may already be partially filled when a reader +/// adds bytes to it, a mechanism to track where the _newly_ filled portion +/// exists is needed. This is exactly what the "fresh" section tracks. +/// +/// [`AsyncRead`]: tokio::io::AsyncRead +pub struct TransformBuf<'a, 'b> { + pub(crate) buf: &'a mut ReadBuf<'b>, + pub(crate) cursor: usize, +} + +impl TransformBuf<'_, '_> { + /// Returns a borrow to the fresh data: data filled by the upstream source. + pub fn fresh(&self) -> &[u8] { + &self.filled()[self.cursor..] + } + + /// Returns a mutable borrow to the fresh data: data filled by the upstream + /// source. + pub fn fresh_mut(&mut self) -> &mut [u8] { + let cursor = self.cursor; + &mut self.filled_mut()[cursor..] + } + + /// Spoils the fresh data by resetting the filled section to its value + /// before any new data was added. As a result, the data will never be seen + /// by any downstream consumer unless it is returned via another mechanism. + pub fn spoil(&mut self) { + let cursor = self.cursor; + self.set_filled(cursor); + } +} + +pub struct Inspect(pub(crate) Box); + +impl Transform for Inspect { + fn transform(mut self: Pin<&mut Self>, buf: &mut TransformBuf<'_, '_>) -> io::Result<()> { + (self.0)(buf.fresh()); + Ok(()) + } +} + +pub struct InPlaceMap( + pub(crate) Box) -> io::Result<()> + Send + Sync + 'static> +); + +impl Transform for InPlaceMap { + fn transform(mut self: Pin<&mut Self>, buf: &mut TransformBuf<'_, '_>,) -> io::Result<()> { + (self.0)(buf) + } +} + +impl<'a, 'b> Deref for TransformBuf<'a, 'b> { + type Target = ReadBuf<'b>; + + fn deref(&self) -> &Self::Target { + &self.buf + } +} + +impl<'a, 'b> DerefMut for TransformBuf<'a, 'b> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.buf + } +} + +// TODO: Test chaining various transform combinations: +// * consume | consume +// * add | consume +// * consume | add +// * add | add +// Where `add` is a transformer that adds data to the stream, and `consume` is +// one that removes data. +#[cfg(test)] +#[allow(deprecated)] +mod tests { + use std::hash::SipHasher; + use std::sync::{Arc, atomic::{AtomicU64, AtomicU8}}; + + use parking_lot::Mutex; + use ubyte::ToByteUnit; + + use crate::http::Method; + use crate::local::blocking::Client; + use crate::fairing::AdHoc; + use crate::{route, Route, Data, Response, Request}; + + mod hash_transform { + use std::io::Cursor; + use std::hash::Hasher; + + use tokio::io::AsyncRead; + + use super::super::*; + + pub struct HashTransform { + pub(crate) hasher: H, + pub(crate) hash: Option> + } + + impl Transform for HashTransform { + fn transform( + mut self: Pin<&mut Self>, + buf: &mut TransformBuf<'_, '_>, + ) -> io::Result<()> { + self.hasher.write(buf.fresh()); + buf.spoil(); + Ok(()) + } + + fn poll_finish( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if self.hash.is_none() { + let hash = self.hasher.finish(); + self.hash = Some(Cursor::new(hash.to_be_bytes())); + } + + let cursor = self.hash.as_mut().unwrap(); + Pin::new(cursor).poll_read(cx, buf) + } + } + + impl crate::Data<'_> { + /// Chain an in-place hash [`Transform`] to `self`. + pub fn chain_hash_transform(&mut self, hasher: H) -> &mut Self + where H: Unpin + Send + Sync + 'static + { + self.chain_transform(HashTransform { hasher, hash: None }) + } + } + } + + #[test] + fn test_transform_series() { + fn handler<'r>(_: &'r Request<'_>, data: Data<'r>) -> route::BoxFuture<'r> { + Box::pin(async move { + data.open(128.bytes()).stream_to(tokio::io::sink()).await.expect("read ok"); + route::Outcome::Success(Response::new()) + }) + } + + let inspect2: Arc = Arc::new(AtomicU8::new(0)); + let raw_data: Arc>> = Arc::new(Mutex::new(Vec::new())); + let hash: Arc = Arc::new(AtomicU64::new(0)); + let rocket = crate::build() + .manage(hash.clone()) + .manage(raw_data.clone()) + .manage(inspect2.clone()) + .mount("/", vec![Route::new(Method::Post, "/", handler)]) + .attach(AdHoc::on_request("transforms", |req, data| Box::pin(async { + let hash1 = req.rocket().state::>().cloned().unwrap(); + let hash2 = req.rocket().state::>().cloned().unwrap(); + let raw_data = req.rocket().state::>>>().cloned().unwrap(); + let inspect2 = req.rocket().state::>().cloned().unwrap(); + data.chain_inspect(move |bytes| { *raw_data.lock() = bytes.to_vec(); }) + .chain_hash_transform(SipHasher::new()) + .chain_inspect(move |bytes| { + assert_eq!(bytes.len(), 8); + let bytes: [u8; 8] = bytes.try_into().expect("[u8; 8]"); + let value = u64::from_be_bytes(bytes); + hash1.store(value, atomic::Ordering::Release); + }) + .chain_inspect(move |bytes| { + assert_eq!(bytes.len(), 8); + let bytes: [u8; 8] = bytes.try_into().expect("[u8; 8]"); + let value = u64::from_be_bytes(bytes); + let prev = hash2.load(atomic::Ordering::Acquire); + assert_eq!(prev, value); + inspect2.fetch_add(1, atomic::Ordering::Release); + }); + }))); + + // Make sure nothing has happened yet. + assert!(raw_data.lock().is_empty()); + assert_eq!(hash.load(atomic::Ordering::Acquire), 0); + assert_eq!(inspect2.load(atomic::Ordering::Acquire), 0); + + // Check that nothing happens if the data isn't read. + let client = Client::debug(rocket).unwrap(); + client.get("/").body("Hello, world!").dispatch(); + assert!(raw_data.lock().is_empty()); + assert_eq!(hash.load(atomic::Ordering::Acquire), 0); + assert_eq!(inspect2.load(atomic::Ordering::Acquire), 0); + + // Check inspect + hash + inspect + inspect. + client.post("/").body("Hello, world!").dispatch(); + assert_eq!(raw_data.lock().as_slice(), "Hello, world!".as_bytes()); + assert_eq!(hash.load(atomic::Ordering::Acquire), 0xae5020d7cf49d14f); + assert_eq!(inspect2.load(atomic::Ordering::Acquire), 1); + + // Check inspect + hash + inspect + inspect, round 2. + let string = "Rocket, Rocket, where art thee? Oh, tis in the sky, I see!"; + client.post("/").body(string).dispatch(); + assert_eq!(raw_data.lock().as_slice(), string.as_bytes()); + assert_eq!(hash.load(atomic::Ordering::Acquire), 0x323f9aa98f907faf); + assert_eq!(inspect2.load(atomic::Ordering::Acquire), 2); + } +} diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index e689cdfaf7..a83d6e58ac 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -59,7 +59,7 @@ enum AdHocKind { Liftoff(Once FnOnce(&'a Rocket) -> BoxFuture<'a, ()> + Send + 'static>), /// An ad-hoc **request** fairing. Called when a request is received. - Request(Box Fn(&'a mut Request<'_>, &'a Data<'_>) + Request(Box Fn(&'a mut Request<'_>, &'a mut Data<'_>) -> BoxFuture<'a, ()> + Send + Sync + 'static>), /// An ad-hoc **response** fairing. Called when a response is ready to be @@ -153,7 +153,7 @@ impl AdHoc { /// }); /// ``` pub fn on_request(name: &'static str, f: F) -> AdHoc - where F: for<'a> Fn(&'a mut Request<'_>, &'a Data<'_>) -> BoxFuture<'a, ()> + where F: for<'a> Fn(&'a mut Request<'_>, &'a mut Data<'_>) -> BoxFuture<'a, ()> { AdHoc { name, kind: AdHocKind::Request(Box::new(f)) } } From de6a4c50ecf5ccbe4464ede186d304b3fd0c0297 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 17 Jan 2024 10:52:00 -0800 Subject: [PATCH 062/178] Clarify route ranking in requests guide. Closes #2687. --- site/guide/4-requests.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index b6247a929f..2ae2f146a1 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -211,10 +211,11 @@ other matching routes to try. When there are no remaining routes, the [error catcher](#error-catchers) associated with the status set by the last forwarding guard is called. -Routes are attempted in increasing _rank_ order. Rocket chooses a default -ranking from -12 to -1, detailed in the next section, but a route's rank can also -be manually set with the `rank` attribute. To illustrate, consider the following -routes: +Routes are attempted in increasing _rank_ order. Every route has an associated +rank. If one is not specified, Rocket chooses a default rank designed to avoid +collisions based on a route's _color_, detailed in the [next +section](#default-ranking), but a route's rank can also be manually set with the +`rank` attribute. To illustrate, consider the following routes: ```rust # #[macro_use] extern crate rocket; @@ -272,17 +273,21 @@ parameter resolves this collision. ### Default Ranking -If a rank is not explicitly specified, Rocket assigns a default rank. The +If a rank is not explicitly specified, Rocket assigns a default rank to a route. +Default ranks range from `-12` to `-1`. This differs from ranks that can be +assigned manually in a route attribute, which must be positive numbers. The default rank prefers static segments over dynamic segments in both paths and queries: the _more_ static a route's path and query are, the higher its precedence. -There are three "colors" to paths and queries: +There are three "colors" assignable to each path and query: 1. `static`, meaning all components are static 2. `partial`, meaning at least one component is dynamic 3. `wild`, meaning all components are dynamic +Additionally, a query may have _no_ color (`none`) if there is no query. + Static paths carry more weight than static queries. The same is true for partial and wild paths. This results in the following default ranking table: From 915c1181da73d3612505925e5fd0965758a927b5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 17 Jan 2024 12:15:01 -0800 Subject: [PATCH 063/178] Forward whole-form errors in 'FromForm' derive. Resolves #2672. --- core/codegen/src/derive/from_form.rs | 6 ++++ core/lib/src/form/parser.rs | 9 ++++- core/lib/tests/multipart-limit.rs | 54 ++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 core/lib/tests/multipart-limit.rs diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index bf55d3bbe1..93ca2ec8b0 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -211,6 +211,12 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { __fut.await; }))) ) + .inner_mapper(MapperBuild::new() + .with_output(|_, _| quote! { + fn push_error(__c: &mut Self::Context, __e: #_form::Error<'r>) { + __c.__errors.push(__e); + } + })) .inner_mapper(MapperBuild::new() .with_output(|_, output| quote! { fn finalize(mut __c: Self::Context) -> #_Result> { diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index b53e2a5008..4b5fe0a727 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -67,10 +67,17 @@ impl<'r, 'i> Parser<'r, 'i> { .get("data-form") .unwrap_or(Limits::DATA_FORM); + // Increase internal limit by 1 so multer can limit to `form_limit`. + let stream = data.open(form_limit + 1); + let constraints = multer::Constraints::new() + .size_limit(multer::SizeLimit::new() + .whole_stream(form_limit.into()) + .per_field(form_limit.into())); + Ok(Parser::Multipart(MultipartParser { request: req, buffer: local_cache_once!(req, SharedStack::new()), - source: Multipart::with_reader(data.open(form_limit), boundary), + source: Multipart::with_reader_with_constraints(stream, boundary, constraints), done: false, })) } diff --git a/core/lib/tests/multipart-limit.rs b/core/lib/tests/multipart-limit.rs new file mode 100644 index 0000000000..31a13dc11a --- /dev/null +++ b/core/lib/tests/multipart-limit.rs @@ -0,0 +1,54 @@ +#[macro_use] extern crate rocket; + +use rocket::{Config, Build, Rocket}; +use rocket::{data::Limits, form::Form}; +use rocket::http::{ContentType, Status}; +use ubyte::{ToByteUnit, ByteUnit}; + +#[derive(FromForm)] +struct Data<'r> { + foo: Option<&'r str>, +} + +#[rocket::post("/", data = "

    ")] +fn form<'r>(form: Form>) -> &'r str { + form.foo.unwrap_or("missing") +} + +fn rocket_with_form_data_limit(limit: ByteUnit) -> Rocket { + rocket::custom(Config { + limits: Limits::default().limit("data-form", limit), + ..Config::debug_default() + }).mount("/", routes![form]) +} + +#[test] +fn test_multipart_limit() { + use rocket::local::blocking::Client; + + let body = &[ + "--X-BOUNDARY", + r#"Content-Disposition: form-data; name="foo"; filename="foo.txt""#, + "Content-Type: text/plain", + "", + "hi", + "--X-BOUNDARY--", + "", + ].join("\r\n"); + + let client = Client::debug(rocket_with_form_data_limit(body.len().bytes())).unwrap(); + let response = client.post("/") + .header("multipart/form-data; boundary=X-BOUNDARY".parse::().unwrap()) + .body(body) + .dispatch(); + + assert_eq!(response.into_string().unwrap(), "hi"); + + let client = Client::debug(rocket_with_form_data_limit(body.len().bytes() - 1)).unwrap(); + let response = client.post("/") + .header("multipart/form-data; boundary=X-BOUNDARY".parse::().unwrap()) + .body(body) + .dispatch(); + + assert_eq!(response.status(), Status::PayloadTooLarge); +} From 105f058e9d04f56fa1a3240e3fa83049689ea36e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 12 Dec 2023 18:10:49 -0800 Subject: [PATCH 064/178] Update project sponsors. --- site/index.toml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/site/index.toml b/site/index.toml index f2d1fec2b0..5656452bbe 100644 --- a/site/index.toml +++ b/site/index.toml @@ -404,7 +404,14 @@ console to let you know everything is okay. name = "๐Ÿ’Ž Diamond" tag = "$500/month" color = "#addcde" -height = "70px" +height = "110px" + +[[sponsors.diamond.sponsors]] +name = "Kindness" +url = "https://kindness.ai" +img = "kindness.png" +blurb = "Supporting customers with Kindness" +width = "110px" [sponsors.gold] name = "๐Ÿ’› Gold" @@ -416,16 +423,24 @@ height = "55px" name = "ohne-makler" url = "https://www.ohne-makler.net/" img = "ohne-makler.svg" +width = "173px" [[sponsors.gold.sponsors]] -name = "RWF2: Rocket Web Framework Foundation" +name = "RWF2" url = "https://rwf2.org" img = "rwf2.gif" +blurb = "Rocket Web Framework Foundation" width = "55px" -height = "55px" [sponsors.bronze] name = "๐ŸคŽ Bronze" tag = "$50/month" -color = "#c5a587" +color = "#c7a483" height = "30px" + +[[sponsors.bronze.sponsors]] +name = "1Password" +url = "https://1password.com" +img = "1password.svg" +blurb = "The worldโ€™s most-loved password manager" +width = "30px" From dca3afcd77d3510a91eb9899df70af41e4712f3d Mon Sep 17 00:00:00 2001 From: Alessandro Campeis <12007735+campeis@users.noreply.github.com> Date: Sat, 13 Jan 2024 05:23:24 +0100 Subject: [PATCH 065/178] Update 'handlebars' to v5.1. --- contrib/dyn_templates/Cargo.toml | 2 +- examples/templating/src/hbs.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 11650b4e3f..b20b3a3e80 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -33,7 +33,7 @@ optional = true [dependencies.handlebars_] package = "handlebars" -version = "4.1" +version = "5.1" optional = true [package.metadata.docs.rs] diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs index 5dd3b9eaca..c3edcdb221 100644 --- a/examples/templating/src/hbs.rs +++ b/examples/templating/src/hbs.rs @@ -35,7 +35,7 @@ pub fn not_found(req: &Request<'_>) -> Template { } fn wow_helper( - h: &handlebars::Helper<'_, '_>, + h: &handlebars::Helper<'_>, _: &handlebars::Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext<'_, '_>, From 5034ff0d1afb160682206fda6d19e693ad392635 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 17 Jan 2024 21:37:20 -0800 Subject: [PATCH 066/178] Update handlebars version in dyn_templates docs. --- contrib/dyn_templates/README.md | 2 +- contrib/dyn_templates/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/dyn_templates/README.md b/contrib/dyn_templates/README.md index 604ea27239..317ce0e69e 100644 --- a/contrib/dyn_templates/README.md +++ b/contrib/dyn_templates/README.md @@ -13,7 +13,7 @@ and automatically reloads templates when compiled in debug mode. At present, it supports [Handlebars] and [Tera]. [Tera]: https://docs.rs/crate/tera/1 -[Handlebars]: https://docs.rs/crate/handlebars/3 +[Handlebars]: https://docs.rs/crate/handlebars/5 # Usage diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 488f802d1f..d239005c2a 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -76,10 +76,10 @@ //! | Engine | Version | Extension | //! |--------------|---------|-----------| //! | [Tera] | 1 | `.tera` | -//! | [Handlebars] | 4 | `.hbs` | +//! | [Handlebars] | 5 | `.hbs` | //! //! [Tera]: https://docs.rs/crate/tera/1 -//! [Handlebars]: https://docs.rs/crate/handlebars/4 +//! [Handlebars]: https://docs.rs/crate/handlebars/5 //! //! Any file that ends with one of these extension will be discovered and //! rendered with the corresponding templating engine. The _name_ of the From 5c85ea3db53beece11dbc855d0f76a880d1e268a Mon Sep 17 00:00:00 2001 From: Arjen <4867268+arjentz@users.noreply.github.com> Date: Tue, 11 Apr 2023 23:43:12 +0200 Subject: [PATCH 067/178] Support configurable 'X-Forwarded-Proto'. Co-authored-by: Sergio Benitez --- core/http/src/header/mod.rs | 2 + core/http/src/header/proxy_proto.rs | 51 ++++++++ core/lib/src/config/config.rs | 39 ++++-- .../config/{ip_header.rs => http_header.rs} | 0 core/lib/src/config/mod.rs | 2 +- core/lib/src/cookies.rs | 105 ++++++++-------- core/lib/src/local/asynchronous/response.rs | 5 +- core/lib/src/local/client.rs | 3 +- core/lib/src/request/from_request.rs | 20 ++- core/lib/src/request/request.rs | 110 +++++++++++++--- core/lib/tests/config-proxy-proto-header.rs | 118 ++++++++++++++++++ site/guide/9-configuration.md | 60 ++++++--- 12 files changed, 420 insertions(+), 95 deletions(-) create mode 100644 core/http/src/header/proxy_proto.rs rename core/lib/src/config/{ip_header.rs => http_header.rs} (100%) create mode 100644 core/lib/tests/config-proxy-proto-header.rs diff --git a/core/http/src/header/mod.rs b/core/http/src/header/mod.rs index b91521ff38..653b786348 100644 --- a/core/http/src/header/mod.rs +++ b/core/http/src/header/mod.rs @@ -4,10 +4,12 @@ mod media_type; mod content_type; mod accept; mod header; +mod proxy_proto; pub use self::content_type::ContentType; pub use self::accept::{Accept, QMediaType}; pub use self::media_type::MediaType; pub use self::header::{Header, HeaderMap}; +pub use self::proxy_proto::ProxyProto; pub(crate) use self::media_type::Source; diff --git a/core/http/src/header/proxy_proto.rs b/core/http/src/header/proxy_proto.rs new file mode 100644 index 0000000000..6c475263cb --- /dev/null +++ b/core/http/src/header/proxy_proto.rs @@ -0,0 +1,51 @@ +use std::fmt; + +use uncased::{UncasedStr, AsUncased}; + +/// A protocol used to identify a specific protocol forwarded by an HTTP proxy. +/// Value are case-insensitive. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProxyProto<'a> { + /// `http` value, Hypertext Transfer Protocol. + Http, + /// `https` value, Hypertext Transfer Protocol Secure. + Https, + /// Any protocol name other than `http` or `https`. + Unknown(&'a UncasedStr), +} + +impl ProxyProto<'_> { + /// Returns `true` if `self` is `ProxyProto::Https` and `false` otherwise. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::ProxyProto; + /// + /// assert!(ProxyProto::Https.is_https()); + /// assert!(!ProxyProto::Http.is_https()); + /// ``` + pub fn is_https(&self) -> bool { + self == &ProxyProto::Https + } +} + +impl<'a> From<&'a str> for ProxyProto<'a> { + fn from(value: &'a str) -> ProxyProto<'a> { + match value.as_uncased() { + v if v == "http" => ProxyProto::Http, + v if v == "https" => ProxyProto::Https, + v => ProxyProto::Unknown(v) + } + } +} + +impl fmt::Display for ProxyProto<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + ProxyProto::Http => "http", + ProxyProto::Https => "https", + ProxyProto::Unknown(s) => s.as_str(), + }) + } +} diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 6f69b85d6f..aaa62c4173 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -85,13 +85,28 @@ pub struct Config { /// client. Used internally and by [`Request::client_ip()`] and /// [`Request::real_ip()`]. /// - /// To disable using any header for this purpose, set this value to `false`. - /// Deserialization semantics are identical to those of [`Ident`] except - /// that the value must syntactically be a valid HTTP header name. + /// To disable using any header for this purpose, set this value to `false` + /// or `None`. Deserialization semantics are identical to those of [`Ident`] + /// except that the value must syntactically be a valid HTTP header name. /// /// **(default: `"X-Real-IP"`)** - #[serde(deserialize_with = "crate::config::ip_header::deserialize")] + #[serde(deserialize_with = "crate::config::http_header::deserialize")] pub ip_header: Option>, + /// The name of a header, whose value is typically set by an intermediary + /// server or proxy, which contains the protocol (HTTP or HTTPS) used by the + /// connecting client. This should probably be [`X-Forwarded-Proto`], as + /// that is the de facto standard. Used by [`Request::forwarded_proto()`] + /// to determine the forwarded protocol and [`Request::forwarded_secure()`] + /// to determine whether a request is handled in a secure context. + /// + /// [`X-Forwarded-Proto`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + /// + /// To disable using any header for this purpose, set this value to `false` + /// or `None`. Deserialization semantics are identical to those of [`ip_header`]. + /// + /// **(default: `None`)** + #[serde(deserialize_with = "crate::config::http_header::deserialize")] + pub proxy_proto_header: Option>, /// Streaming read size limits. **(default: [`Limits::default()`])** pub limits: Limits, /// Directory to store temporary files in. **(default: @@ -189,6 +204,7 @@ impl Config { max_blocking: 512, ident: Ident::default(), ip_header: Some(Uncased::from_borrowed("X-Real-IP")), + proxy_proto_header: None, limits: Limits::default(), temp_dir: std::env::temp_dir().into(), keep_alive: 5, @@ -409,6 +425,11 @@ impl Config { None => launch_meta_!("IP header: {}", "disabled".paint(VAL)) } + match self.proxy_proto_header.as_ref() { + Some(name) => launch_meta_!("Proxy-Proto header: {}", name.paint(VAL)), + None => launch_meta_!("Proxy-Proto header: {}", "disabled".paint(VAL)) + } + launch_meta_!("limits: {}", (&self.limits).paint(VAL)); launch_meta_!("temp dir: {}", self.temp_dir.relative().display().paint(VAL)); launch_meta_!("http/2: {}", (cfg!(feature = "http2").paint(VAL))); @@ -513,6 +534,9 @@ impl Config { /// The stringy parameter name for setting/extracting [`Config::ip_header`]. pub const IP_HEADER: &'static str = "ip_header"; + /// The stringy parameter name for setting/extracting [`Config::proxy_proto_header`]. + pub const PROXY_PROTO_HEADER: &'static str = "proxy_proto_header"; + /// The stringy parameter name for setting/extracting [`Config::limits`]. pub const LIMITS: &'static str = "limits"; @@ -536,10 +560,9 @@ impl Config { /// An array of all of the stringy parameter names. pub const PARAMETERS: &'static [&'static str] = &[ - Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, - Self::KEEP_ALIVE, Self::IDENT, Self::IP_HEADER, Self::LIMITS, Self::TLS, - Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, - Self::CLI_COLORS, + Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, Self::KEEP_ALIVE, + Self::IDENT, Self::IP_HEADER, Self::PROXY_PROTO_HEADER, Self::LIMITS, Self::TLS, + Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, Self::CLI_COLORS, ]; } diff --git a/core/lib/src/config/ip_header.rs b/core/lib/src/config/http_header.rs similarity index 100% rename from core/lib/src/config/ip_header.rs rename to core/lib/src/config/http_header.rs diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 07286fc1d6..2bf83cd264 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -114,8 +114,8 @@ mod ident; mod config; mod shutdown; -mod ip_header; mod cli_colors; +mod http_header; #[cfg(feature = "tls")] mod tls; diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 368172cd61..926241fd7c 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -2,8 +2,8 @@ use std::fmt; use parking_lot::Mutex; -use crate::Config; use crate::http::private::cookie; +use crate::{Rocket, Orbit}; #[doc(inline)] pub use self::cookie::{Cookie, SameSite, Iter}; @@ -154,17 +154,14 @@ pub use self::cookie::{Cookie, SameSite, Iter}; pub struct CookieJar<'a> { jar: cookie::CookieJar, ops: Mutex>, - config: &'a Config, + pub(crate) state: CookieState<'a>, } -impl<'a> Clone for CookieJar<'a> { - fn clone(&self) -> Self { - CookieJar { - jar: self.jar.clone(), - ops: Mutex::new(self.ops.lock().clone()), - config: self.config, - } - } +#[derive(Copy, Clone)] +pub(crate) struct CookieState<'a> { + pub secure: bool, + #[cfg_attr(not(feature = "secrets"), allow(unused))] + pub config: &'a crate::Config, } #[derive(Clone)] @@ -173,22 +170,17 @@ enum Op { Remove(Cookie<'static>, bool), } -impl Op { - fn cookie(&self) -> &Cookie<'static> { - match self { - Op::Add(c, _) | Op::Remove(c, _) => c - } - } -} - impl<'a> CookieJar<'a> { - #[inline(always)] - pub(crate) fn new(config: &'a Config) -> Self { - CookieJar::from(cookie::CookieJar::new(), config) - } - - pub(crate) fn from(jar: cookie::CookieJar, config: &'a Config) -> Self { - CookieJar { jar, config, ops: Mutex::new(Vec::new()) } + pub(crate) fn new(base: Option, rocket: &'a Rocket) -> Self { + CookieJar { + jar: base.unwrap_or_default(), + ops: Mutex::new(Vec::new()), + state: CookieState { + // This is updated dynamically when headers are received. + secure: rocket.config().tls_enabled(), + config: rocket.config(), + } + } } /// Returns a reference to the _original_ `Cookie` inside this container @@ -236,7 +228,7 @@ impl<'a> CookieJar<'a> { #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub fn get_private(&self, name: &str) -> Option> { - self.jar.private(&self.config.secret_key.key).get(name) + self.jar.private(&self.state.config.secret_key.key).get(name) } /// Returns a reference to the _original or pending_ `Cookie` inside this @@ -287,7 +279,10 @@ impl<'a> CookieJar<'a> { /// * `path`: `"/"` /// * `SameSite`: `Strict` /// - /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. + /// Furthermore, if TLS is enabled or handled by a proxy (as determined by + /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// These defaults ensure maximum usability and security. For additional + /// security, you may wish to set the `secure` flag explicitly. /// /// # Example /// @@ -309,7 +304,7 @@ impl<'a> CookieJar<'a> { /// ``` pub fn add>>(&self, cookie: C) { let mut cookie = cookie.into(); - Self::set_defaults(self.config, &mut cookie); + self.set_defaults(&mut cookie); self.ops.lock().push(Op::Add(cookie, false)); } @@ -327,9 +322,10 @@ impl<'a> CookieJar<'a> { /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now /// - /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. These - /// defaults ensure maximum usability and security. For additional security, - /// you may wish to set the `secure` flag. + /// Furthermore, if TLS is enabled or handled by a proxy (as determined by + /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// These defaults ensure maximum usability and security. For additional + /// security, you may wish to set the `secure` flag explicitly. /// /// # Example /// @@ -346,7 +342,7 @@ impl<'a> CookieJar<'a> { #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub fn add_private>>(&self, cookie: C) { let mut cookie = cookie.into(); - Self::set_private_defaults(self.config, &mut cookie); + self.set_private_defaults(&mut cookie); self.ops.lock().push(Op::Add(cookie, true)); } @@ -476,7 +472,7 @@ impl<'a> CookieJar<'a> { Op::Add(c, false) => jar.add(c), #[cfg(feature = "secrets")] Op::Add(c, true) => { - jar.private_mut(&self.config.secret_key.key).add(c); + jar.private_mut(&self.state.config.secret_key.key).add(c); } Op::Remove(mut c, _) => { if self.jar.get(c.name()).is_some() { @@ -505,7 +501,7 @@ impl<'a> CookieJar<'a> { #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] #[inline(always)] pub(crate) fn add_original_private(&mut self, cookie: Cookie<'static>) { - self.jar.private_mut(&self.config.secret_key.key).add_original(cookie); + self.jar.private_mut(&self.state.config.secret_key.key).add_original(cookie); } /// For each property mentioned below, this method checks if there is a @@ -515,8 +511,9 @@ impl<'a> CookieJar<'a> { /// * `path`: `"/"` /// * `SameSite`: `Strict` /// - /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. - fn set_defaults(config: &Config, cookie: &mut Cookie<'static>) { + /// Furthermore, if TLS is enabled or handled by a proxy (as determined by + /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + fn set_defaults(&self, cookie: &mut Cookie<'static>) { if cookie.path().is_none() { cookie.set_path("/"); } @@ -525,7 +522,7 @@ impl<'a> CookieJar<'a> { cookie.set_same_site(SameSite::Strict); } - if cookie.secure().is_none() && config.tls_enabled() { + if cookie.secure().is_none() && self.state.secure { cookie.set_secure(true); } } @@ -554,17 +551,12 @@ impl<'a> CookieJar<'a> { /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now /// - /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. + /// Furthermore, if TLS is enabled or handled by a proxy (as determined by + /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] - fn set_private_defaults(config: &Config, cookie: &mut Cookie<'static>) { - if cookie.path().is_none() { - cookie.set_path("/"); - } - - if cookie.same_site().is_none() { - cookie.set_same_site(SameSite::Strict); - } + fn set_private_defaults(&self, cookie: &mut Cookie<'static>) { + self.set_defaults(cookie); if cookie.http_only().is_none() { cookie.set_http_only(true); @@ -573,10 +565,6 @@ impl<'a> CookieJar<'a> { if cookie.expires().is_none() { cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1)); } - - if cookie.secure().is_none() && config.tls_enabled() { - cookie.set_secure(true); - } } } @@ -593,5 +581,22 @@ impl fmt::Debug for CookieJar<'_> { .field("pending", &pending) .finish() } +} +impl<'a> Clone for CookieJar<'a> { + fn clone(&self) -> Self { + CookieJar { + jar: self.jar.clone(), + ops: Mutex::new(self.ops.lock().clone()), + state: self.state, + } + } +} + +impl Op { + fn cookie(&self) -> &Cookie<'static> { + match self { + Op::Add(c, _) | Op::Remove(c, _) => c + } + } } diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index cabbdccc21..e0fbd83669 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -87,8 +87,11 @@ impl<'c> LocalResponse<'c> { let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; async move { + // NOTE: The new `secure` cookie jar state will not reflect the last + // known value in `request.cookies()`. This is okay as new cookies + // should never be added to the resulting jar. let response: Response<'c> = f(request).await; - let mut cookies = CookieJar::new(request.rocket().config()); + let mut cookies = CookieJar::new(None, request.rocket()); for cookie in response.cookies() { cookies.add_original(cookie.into_owned()); } diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index 48001f05d6..f2b3b922d0 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -181,9 +181,8 @@ macro_rules! pub_client_impl { /// ``` #[inline(always)] pub fn cookies(&self) -> crate::http::CookieJar<'_> { - let config = &self.rocket().config(); let jar = self._with_raw_cookies(|jar| jar.clone()); - crate::http::CookieJar::from(jar, config) + crate::http::CookieJar::new(Some(jar), self.rocket()) } req_method!($import, "GET", get, Method::Get); diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 0d1879ccb4..e183d4e480 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -6,7 +6,7 @@ use crate::{Request, Route}; use crate::outcome::{self, Outcome::*}; use crate::http::uri::{Host, Origin}; -use crate::http::{Status, ContentType, Accept, Method, CookieJar}; +use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar}; /// Type alias for the `Outcome` of a `FromRequest` conversion. pub type Outcome = outcome::Outcome; @@ -160,6 +160,12 @@ pub type Outcome = outcome::Outcome; /// via [`Request::client_ip()`]. If the client's IP address is not known, /// the request is forwarded with a 500 Internal Server Error status. /// +/// * **ProxyProto** +/// +/// Extracts the protocol of the incoming request as a [`ProxyProto`] via +/// [`Request::proxy_proto()`] (HTTP or HTTPS). If value of the header is +/// not known, the request is forwarded with a 404 Not Found status. +/// /// * **SocketAddr** /// /// Extracts the remote address of the incoming request as a [`SocketAddr`] @@ -470,6 +476,18 @@ impl<'r> FromRequest<'r> for IpAddr { } } +#[crate::async_trait] +impl<'r> FromRequest<'r> for ProxyProto<'r> { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match request.proxy_proto() { + Some(proto) => Success(proto), + None => Forward(Status::InternalServerError), + } + } +} + #[crate::async_trait] impl<'r> FromRequest<'r> for SocketAddr { type Error = Infallible; diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 7f7e50e7eb..44630b26ae 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -13,9 +13,8 @@ use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; use crate::form::{self, ValueField, FromForm}; use crate::data::Limits; -use crate::http::{hyper, Method, Header, HeaderMap}; +use crate::http::{hyper, Method, Header, HeaderMap, ProxyProto}; use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie}; -use crate::http::uncased::UncasedStr; use crate::http::private::Certificates; use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority}; @@ -97,7 +96,7 @@ impl<'r> Request<'r> { state: RequestState { rocket, route: Atomic::new(None), - cookies: CookieJar::new(rocket.config()), + cookies: CookieJar::new(None, rocket), accept: InitCell::new(), content_type: InitCell::new(), cache: Arc::new(::new()), @@ -386,6 +385,85 @@ impl<'r> Request<'r> { }) } + /// Returns the [`ProxyProto`] associated with the current request. + /// + /// The value is determined by inspecting the header named + /// [`proxy_proto_header`](crate::Config::proxy_proto_header), if + /// configured. If parameter isn't configured or the request doesn't contain + /// a header named as indicated, this method returns `None`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{Header, ProxyProto}; + /// + /// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap(); + /// # let req = c.get("/"); + /// // By default, no `proxy_proto_header` is configured. + /// let req = req.header(Header::new("x-forwarded-proto", "https")); + /// assert_eq!(req.proxy_proto(), None); + /// + /// // We can configure one by setting the `proxy_proto_header` parameter. + /// // Here we set it to `x-forwarded-proto`, considered de-facto standard. + /// # let figment = rocket::figment::Figment::from(rocket::Config::debug_default()); + /// let figment = figment.merge(("proxy_proto_header", "x-forwarded-proto")); + /// # let c = rocket::local::blocking::Client::debug(rocket::custom(figment)).unwrap(); + /// # let req = c.get("/"); + /// let req = req.header(Header::new("x-forwarded-proto", "https")); + /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https)); + /// + /// # let req = c.get("/"); + /// let req = req.header(Header::new("x-forwarded-proto", "http")); + /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Http)); + /// + /// # let req = c.get("/"); + /// let req = req.header(Header::new("x-forwarded-proto", "xproto")); + /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Unknown("xproto".into()))); + /// ``` + pub fn proxy_proto(&self) -> Option> { + self.rocket() + .config + .proxy_proto_header + .as_ref() + .and_then(|header| self.headers().get_one(header.as_str())) + .map(ProxyProto::from) + } + + /// Returns whether we are *likely* in a secure context. + /// + /// A request is in a "secure context" if it was initially sent over a + /// secure (TLS, via HTTPS) connection. If TLS is configured and enabled, + /// then the request is guaranteed to be in a secure context. Otherwise, if + /// [`Request::proxy_proto()`] evaluates to `Https`, then we are _likely_ to + /// be in a secure context. We say _likely_ because it is entirely possible + /// for the header to indicate that the connection is being proxied via + /// HTTPS while reality differs. As such, this value should not be trusted + /// when security is a concern. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{Header, ProxyProto}; + /// + /// # let client = rocket::local::blocking::Client::debug_with(vec![]).unwrap(); + /// # let req = client.get("/"); + /// // If TLS and proxy_proto are disabled, we are not in a secure context. + /// assert_eq!(req.context_is_likely_secure(), false); + /// + /// // Configuring proxy_proto and receiving a header value of `https` is + /// // interpreted as likely being in a secure context. + /// // Here we set it to `x-forwarded-proto`, considered de-facto standard. + /// # let figment = rocket::figment::Figment::from(rocket::Config::debug_default()); + /// let figment = figment.merge(("proxy_proto_header", "x-forwarded-proto")); + /// # let c = rocket::local::blocking::Client::debug(rocket::custom(figment)).unwrap(); + /// # let req = c.get("/"); + /// let req = req.header(Header::new("x-forwarded-proto", "https")); + /// assert_eq!(req.context_is_likely_secure(), true); + /// ``` + pub fn context_is_likely_secure(&self) -> bool { + self.cookies().state.secure + } + /// Attempts to return the client's IP address by first inspecting the /// [`ip_header`](crate::Config::ip_header) and then using the remote /// connection's IP address. Note that the built-in `IpAddr` request guard @@ -497,7 +575,7 @@ impl<'r> Request<'r> { #[inline] pub fn add_header<'h: 'r, H: Into>>(&mut self, header: H) { let header = header.into(); - self.bust_header_cache(header.name(), false); + self.bust_header_cache(&header, false); self.headers.add(header); } @@ -526,7 +604,7 @@ impl<'r> Request<'r> { #[inline] pub fn replace_header<'h: 'r, H: Into>>(&mut self, header: H) { let header = header.into(); - self.bust_header_cache(header.name(), true); + self.bust_header_cache(&header, true); self.headers.replace(header); } @@ -547,9 +625,9 @@ impl<'r> Request<'r> { /// ``` #[inline] pub fn content_type(&self) -> Option<&ContentType> { - self.state.content_type.get_or_init(|| { - self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()) - }).as_ref() + self.state.content_type + .get_or_init(|| self.headers().get_one("Content-Type").and_then(|v| v.parse().ok())) + .as_ref() } /// Returns the Accept header of `self`. If the header is not present, @@ -567,9 +645,9 @@ impl<'r> Request<'r> { /// ``` #[inline] pub fn accept(&self) -> Option<&Accept> { - self.state.accept.get_or_init(|| { - self.headers().get_one("Accept").and_then(|v| v.parse().ok()) - }).as_ref() + self.state.accept + .get_or_init(|| self.headers().get_one("Accept").and_then(|v| v.parse().ok())) + .as_ref() } /// Returns the media type "format" of the request. @@ -925,15 +1003,19 @@ impl<'r> Request<'r> { #[doc(hidden)] impl<'r> Request<'r> { /// Resets the cached value (if any) for the header with name `name`. - fn bust_header_cache(&mut self, name: &UncasedStr, replace: bool) { - if name == "Content-Type" { + fn bust_header_cache(&mut self, header: &Header<'_>, replace: bool) { + if header.name() == "Content-Type" { if self.content_type().is_none() || replace { self.state.content_type = InitCell::new(); } - } else if name == "Accept" { + } else if header.name() == "Accept" { if self.accept().is_none() || replace { self.state.accept = InitCell::new(); } + } else if Some(header.name()) == self.rocket().config.proxy_proto_header.as_deref() { + if !self.cookies().state.secure || replace { + self.cookies_mut().state.secure |= ProxyProto::from(header.value()).is_https(); + } } } diff --git a/core/lib/tests/config-proxy-proto-header.rs b/core/lib/tests/config-proxy-proto-header.rs new file mode 100644 index 0000000000..48dc5af37a --- /dev/null +++ b/core/lib/tests/config-proxy-proto-header.rs @@ -0,0 +1,118 @@ +use rocket::http::ProxyProto; + +#[macro_use] +extern crate rocket; + +#[get("/")] +fn inspect_proto(proto: Option) -> String { + proto + .map(|proto| match proto { + ProxyProto::Http => "http".to_owned(), + ProxyProto::Https => "https".to_owned(), + ProxyProto::Unknown(s) => s.to_string(), + }) + .unwrap_or("".to_owned()) +} + +mod tests { + use rocket::{Rocket, Build, Route}; + use rocket::http::Header; + use rocket::local::blocking::Client; + use rocket::figment::Figment; + + fn routes() -> Vec { + routes![super::inspect_proto] + } + + fn rocket_with_proto_header(header: Option<&'static str>) -> Rocket { + let mut config = rocket::Config::debug_default(); + config.proxy_proto_header = header.map(|h| h.into()); + rocket::custom(config).mount("/", routes()) + } + + #[test] + fn check_proxy_proto_header_works() { + let rocket = rocket_with_proto_header(Some("X-Url-Scheme")); + let client = Client::debug(rocket).unwrap(); + let response = client.get("/") + .header(Header::new("X-Forwarded-Proto", "https")) + .header(Header::new("X-Url-Scheme", "http")) + .dispatch(); + + assert_eq!(response.into_string().unwrap(), "http"); + + let response = client.get("/") + .header(Header::new("X-Url-Scheme", "https")) + .dispatch(); + + assert_eq!(response.into_string().unwrap(), "https"); + + let response = client.get("/").dispatch(); + assert_eq!(response.into_string().unwrap(), ""); + } + + #[test] + fn check_proxy_proto_header_works_again() { + let client = Client::debug(rocket_with_proto_header(Some("x-url-scheme"))).unwrap(); + let response = client + .get("/") + .header(Header::new("X-Url-Scheme", "https")) + .dispatch(); + + assert_eq!(response.into_string().unwrap(), "https"); + + let config = Figment::from(rocket::Config::debug_default()) + .merge(("proxy_proto_header", "x-url-scheme")); + + let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); + let response = client + .get("/") + .header(Header::new("X-url-Scheme", "https")) + .dispatch(); + + assert_eq!(response.into_string().unwrap(), "https"); + } + + #[test] + fn check_default_proxy_proto_header_works() { + let client = Client::debug_with(routes()).unwrap(); + let response = client + .get("/") + .header(Header::new("X-Forwarded-Proto", "https")) + .dispatch(); + + assert_eq!(response.into_string(), Some("".into())); + } + + #[test] + fn check_no_proxy_proto_header_works() { + let client = Client::debug(rocket_with_proto_header(None)).unwrap(); + let response = client.get("/") + .header(Header::new("X-Forwarded-Proto", "https")) + .dispatch(); + + assert_eq!(response.into_string(), Some("".into())); + + let config = + Figment::from(rocket::Config::debug_default()).merge(("proxy_proto_header", false)); + + let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); + let response = client + .get("/") + .header(Header::new("X-Forwarded-Proto", "https")) + .dispatch(); + + assert_eq!(response.into_string(), Some("".into())); + + let config = Figment::from(rocket::Config::debug_default()) + .merge(("proxy_proto_header", "x-forwarded-proto")); + + let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); + let response = client + .get("/") + .header(Header::new("x-Forwarded-Proto", "https")) + .dispatch(); + + assert_eq!(response.into_string(), Some("https".into())); + } +} diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 76b14a9a0f..5cd2c51ec0 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -17,28 +17,31 @@ is configured with. This means that no matter which configuration provider Rocket is asked to use, it must be able to read the following configuration values: -| key | kind | description | debug/release default | -|-----------------|-------------------|-------------------------------------------------|-------------------------| -| `address` | `IpAddr` | IP address to serve on | `127.0.0.1` | -| `port` | `u16` | Port to serve on. | `8000` | -| `workers`* | `usize` | Number of threads to use for executing futures. | cpu core count | -| `max_blocking`* | `usize` | Limit on threads to start for blocking tasks. | `512` | -| `ident` | `string`, `false` | If and how to identify via the `Server` header. | `"Rocket"` | -| `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` | -| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | -| `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | -| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` | -| `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` | -| `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` | -| `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] | -| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" | -| `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` | -| `shutdown`* | [`Shutdown`] | Graceful shutdown configuration. | [`Shutdown::default()`] | +| key | kind | description | debug/release default | +|----------------------|-------------------|-------------------------------------------------|-------------------------| +| `address` | `IpAddr` | IP address to serve on. | `127.0.0.1` | +| `port` | `u16` | Port to serve on. | `8000` | +| `workers`* | `usize` | Number of threads to use for executing futures. | cpu core count | +| `max_blocking`* | `usize` | Limit on threads to start for blocking tasks. | `512` | +| `ident` | `string`, `false` | If and how to identify via the `Server` header. | `"Rocket"` | +| `ip_header` | `string`, `false` | IP header to inspect to get [client's real IP]. | `"X-Real-IP"` | +| `proxy_proto_header` | `string`, `false` | Header identifying [client to proxy protocol]. | `None` | +| `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | +| `log_level` | [`LogLevel`] | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | +| `cli_colors` | [`CliColors`] | Whether to use colors and emoji when logging. | `"auto"` | +| `secret_key` | [`SecretKey`] | Secret key for signing and encrypting values. | `None` | +| `tls` | [`TlsConfig`] | TLS configuration, if any. | `None` | +| `limits` | [`Limits`] | Streaming read size limits. | [`Limits::default()`] | +| `limits.$name` | `&str`/`uint` | Read limit for `$name`. | form = "32KiB" | +| `ctrlc` | `bool` | Whether `ctrl-c` initiates a server shutdown. | `true` | +| `shutdown`* | [`Shutdown`] | Graceful shutdown configuration. | [`Shutdown::default()`] | + * Note: the `workers`, `max_blocking`, and `shutdown.force` configuration parameters are only read from the [default provider](#default-provider). [client's real IP]: @api/rocket/request/struct.Request.html#method.real_ip +[client to proxy protocol]: @api/rocket/request/struct.Request.html#method.proxy_proto ### Profiles @@ -130,6 +133,7 @@ port = 9001 [release] port = 9999 ip_header = false +proxy_proto_header = "X-Forwarded-Proto" # NOTE: Don't (!) use this key! Generate your own and keep it private! # e.g. via `head -c64 /dev/urandom | base64` secret_key = "hPrYyะญRiMyยต5sBB1ฯ€+CMรฆ1kรธFsรฅqKvBiQJxBVHQk=" @@ -148,7 +152,8 @@ workers = 16 max_blocking = 512 keep_alive = 5 ident = "Rocket" -ip_header = "X-Real-IP" # set to `false` to disable +ip_header = "X-Real-IP" # set to `false` or `None` to disable +proxy_proto_header = `false` # set to `false` or `None` to disable log_level = "normal" temp_dir = "/tmp" cli_colors = true @@ -358,6 +363,25 @@ mutual TLS. [`mtls::Certificate`]: @api/rocket/mtls/struct.Certificate.html +### Proxied TLS + +If Rocket is running behind a reverse proxy that terminates TLS, it is useful to +know whether the original connection was made securely. Therefore, Rocket offers +the option to configure a `proxy_proto_header` that is used to determine if the +request is handled in a secure context. The outcome is available via +[`Request::context_is_likely_secure()`] and used to set cookies' secure flag by +default. To enable this behaviour, configure the header as set by your reverse +proxy. For example: + +```toml,ignore +proxy_proto_header = 'X-Forwarded-Proto' +``` + +Note that this only sets the cookies' secure flag when not configured +explicitly. This setting also provides the [request guard `ProxyProto`]. + +[`ProxyProto`]: @api/rocket/request/trait.FromRequest.html#impl-FromRequest%3C'r%3E-for-%26ProxyProto + ### Workers The `workers` parameter sets the number of threads used for parallel task From e9b568d9b24ba1f1b4ed9d9f008dfef989103042 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 22 Jan 2024 18:18:06 -0800 Subject: [PATCH 068/178] Fixup docs for 'proxy_proto_header'. --- core/http/src/header/proxy_proto.rs | 25 ++++++++-- core/lib/src/config/config.rs | 20 +++++--- core/lib/src/cookies.rs | 21 ++++----- core/lib/src/local/asynchronous/response.rs | 7 +-- core/lib/src/request/from_request.rs | 11 ++--- core/lib/src/request/request.rs | 9 ++-- core/lib/tests/config-proxy-proto-header.rs | 52 +++++++-------------- site/guide/9-configuration.md | 36 ++++++++------ 8 files changed, 95 insertions(+), 86 deletions(-) diff --git a/core/http/src/header/proxy_proto.rs b/core/http/src/header/proxy_proto.rs index 6c475263cb..9b7a4ce355 100644 --- a/core/http/src/header/proxy_proto.rs +++ b/core/http/src/header/proxy_proto.rs @@ -2,15 +2,30 @@ use std::fmt; use uncased::{UncasedStr, AsUncased}; -/// A protocol used to identify a specific protocol forwarded by an HTTP proxy. -/// Value are case-insensitive. +/// Parsed [`Config::proxy_proto_header`] value: identifies a forwarded HTTP +/// protocol (aka [X-Forwarded-Proto]). +/// +/// The value of the header with name [`Config::proxy_proto_header`] is parsed +/// case-insensitively into this `enum`. For a given request, the parsed value, +/// if the header was present, can be retrieved via [`Request::proxy_proto()`] +/// or directly as a [request guard]. That value is used to determine whether a +/// request's context is likely secure ([`Request::context_is_likely_secure()`]) +/// which in-turn is used to determine whether the `Secure` cookie flag is set +/// by default when [cookies are added] to a `CookieJar`. +/// +/// [X-Forwarded-Proto]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto +/// [`Config::proxy_proto_header`]: ../../rocket/struct.Config.html#structfield.proxy_proto_header +/// [`Request::proxy_proto()`]: ../../rocket/request/struct.Request.html#method.proxy_proto +/// [`Request::context_is_likely_secure()`]: ../../rocket/request/struct.Request.html#method.context_is_likely_secure +/// [cookies are added]: ../..//rocket/http/struct.CookieJar.html#method.add +/// [request guard]: ../../rocket/request/trait.FromRequest.html#provided-implementations #[derive(Clone, Debug, Eq, PartialEq)] pub enum ProxyProto<'a> { - /// `http` value, Hypertext Transfer Protocol. + /// `"http"`: Hypertext Transfer Protocol. Http, - /// `https` value, Hypertext Transfer Protocol Secure. + /// `"https"`: Hypertext Transfer Protocol Secure. Https, - /// Any protocol name other than `http` or `https`. + /// Any protocol name other than `"http"` or `"https"`. Unknown(&'a UncasedStr), } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index aaa62c4173..197b6a2f6c 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -93,18 +93,24 @@ pub struct Config { #[serde(deserialize_with = "crate::config::http_header::deserialize")] pub ip_header: Option>, /// The name of a header, whose value is typically set by an intermediary - /// server or proxy, which contains the protocol (HTTP or HTTPS) used by the - /// connecting client. This should probably be [`X-Forwarded-Proto`], as - /// that is the de facto standard. Used by [`Request::forwarded_proto()`] - /// to determine the forwarded protocol and [`Request::forwarded_secure()`] - /// to determine whether a request is handled in a secure context. + /// server or proxy, which contains the protocol ("http" or "https") used by + /// the connecting client. This is usually [`"X-Forwarded-Proto"`], as that + /// is the de-facto standard. /// - /// [`X-Forwarded-Proto`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + /// The header value is parsed into a [`ProxyProto`], accessible via + /// [`Request::proxy_proto()`]. The value influences + /// [`Request::context_is_likely_secure()`] and the default value for the + /// `Secure` flag in cookies added to [`CookieJar`]s. /// /// To disable using any header for this purpose, set this value to `false` - /// or `None`. Deserialization semantics are identical to those of [`ip_header`]. + /// or `None`. Deserialization semantics are identical to those of + /// [`Config::ip_header`] (the value must be a valid HTTP header name). /// /// **(default: `None`)** + /// + /// [`CookieJar`]: crate::http::CookieJar + /// [`ProxyProto`]: crate::http::ProxyProto + /// [`"X-Forwarded-Proto"`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto #[serde(deserialize_with = "crate::config::http_header::deserialize")] pub proxy_proto_header: Option>, /// Streaming read size limits. **(default: [`Limits::default()`])** diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 926241fd7c..b64441f03d 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -278,12 +278,13 @@ impl<'a> CookieJar<'a> { /// /// * `path`: `"/"` /// * `SameSite`: `Strict` + /// * `Secure`: `true` if [`Request::context_is_likely_secure()`] /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. /// These defaults ensure maximum usability and security. For additional /// security, you may wish to set the `secure` flag explicitly. /// + /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure() + /// /// # Example /// /// ```rust @@ -321,11 +322,13 @@ impl<'a> CookieJar<'a> { /// * `SameSite`: `Strict` /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now + /// * `Secure`: `true` if [`Request::context_is_likely_secure()`] /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. /// These defaults ensure maximum usability and security. For additional - /// security, you may wish to set the `secure` flag explicitly. + /// security, you may wish to set the `secure` flag explicitly and + /// unconditionally. + /// + /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure() /// /// # Example /// @@ -510,9 +513,7 @@ impl<'a> CookieJar<'a> { /// /// * `path`: `"/"` /// * `SameSite`: `Strict` - /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// * `Secure`: `true` if `Request::context_is_likely_secure()` fn set_defaults(&self, cookie: &mut Cookie<'static>) { if cookie.path().is_none() { cookie.set_path("/"); @@ -550,9 +551,7 @@ impl<'a> CookieJar<'a> { /// * `SameSite`: `Strict` /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now - /// - /// Furthermore, if TLS is enabled or handled by a proxy (as determined by - /// [`Request::context_is_likely_secure()`]), the `Secure` cookie flag is set. + /// * `Secure`: `true` if `Request::context_is_likely_secure()` #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] fn set_private_defaults(&self, cookie: &mut Cookie<'static>) { diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index e0fbd83669..f91afeb25c 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -87,9 +87,10 @@ impl<'c> LocalResponse<'c> { let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; async move { - // NOTE: The new `secure` cookie jar state will not reflect the last - // known value in `request.cookies()`. This is okay as new cookies - // should never be added to the resulting jar. + // NOTE: The cookie jar `secure` state will not reflect the last + // known value in `request.cookies()`. This is okay: new cookies + // should never be added to the resulting jar which is the only time + // the value is used to set cookie defaults. let response: Response<'c> = f(request).await; let mut cookies = CookieJar::new(None, request.rocket()); for cookie in response.cookies() { diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index e183d4e480..c95a427f09 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use std::net::{IpAddr, SocketAddr}; use crate::{Request, Route}; -use crate::outcome::{self, Outcome::*}; +use crate::outcome::{self, IntoOutcome, Outcome::*}; use crate::http::uri::{Host, Origin}; use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar}; @@ -163,8 +163,8 @@ pub type Outcome = outcome::Outcome; /// * **ProxyProto** /// /// Extracts the protocol of the incoming request as a [`ProxyProto`] via -/// [`Request::proxy_proto()`] (HTTP or HTTPS). If value of the header is -/// not known, the request is forwarded with a 404 Not Found status. +/// [`Request::proxy_proto()`]. If no such header is present, the request is +/// forwarded with a 500 Internal Server Error status. /// /// * **SocketAddr** /// @@ -481,10 +481,7 @@ impl<'r> FromRequest<'r> for ProxyProto<'r> { type Error = std::convert::Infallible; async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.proxy_proto() { - Some(proto) => Success(proto), - None => Forward(Status::InternalServerError), - } + request.proxy_proto().or_forward(Status::InternalServerError) } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 44630b26ae..1d380a9767 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -389,8 +389,9 @@ impl<'r> Request<'r> { /// /// The value is determined by inspecting the header named /// [`proxy_proto_header`](crate::Config::proxy_proto_header), if - /// configured. If parameter isn't configured or the request doesn't contain - /// a header named as indicated, this method returns `None`. + /// configured, and parsing it case-insensitivity. If the parameter isn't + /// configured or the request doesn't contain a header named as indicated, + /// this method returns `None`. /// /// # Example /// @@ -413,7 +414,7 @@ impl<'r> Request<'r> { /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Https)); /// /// # let req = c.get("/"); - /// let req = req.header(Header::new("x-forwarded-proto", "http")); + /// let req = req.header(Header::new("x-forwarded-proto", "HTTP")); /// assert_eq!(req.proxy_proto(), Some(ProxyProto::Http)); /// /// # let req = c.get("/"); @@ -438,7 +439,7 @@ impl<'r> Request<'r> { /// be in a secure context. We say _likely_ because it is entirely possible /// for the header to indicate that the connection is being proxied via /// HTTPS while reality differs. As such, this value should not be trusted - /// when security is a concern. + /// when 100% confidence is a necessity. /// /// # Example /// diff --git a/core/lib/tests/config-proxy-proto-header.rs b/core/lib/tests/config-proxy-proto-header.rs index 48dc5af37a..b0ddc0141c 100644 --- a/core/lib/tests/config-proxy-proto-header.rs +++ b/core/lib/tests/config-proxy-proto-header.rs @@ -1,22 +1,13 @@ -use rocket::http::ProxyProto; - -#[macro_use] -extern crate rocket; +#[macro_use] extern crate rocket; #[get("/")] -fn inspect_proto(proto: Option) -> String { - proto - .map(|proto| match proto { - ProxyProto::Http => "http".to_owned(), - ProxyProto::Https => "https".to_owned(), - ProxyProto::Unknown(s) => s.to_string(), - }) - .unwrap_or("".to_owned()) +fn inspect_proto(proto: rocket::http::ProxyProto) -> String { + proto.to_string() } mod tests { use rocket::{Rocket, Build, Route}; - use rocket::http::Header; + use rocket::http::{Header, Status}; use rocket::local::blocking::Client; use rocket::figment::Figment; @@ -32,8 +23,7 @@ mod tests { #[test] fn check_proxy_proto_header_works() { - let rocket = rocket_with_proto_header(Some("X-Url-Scheme")); - let client = Client::debug(rocket).unwrap(); + let client = Client::debug(rocket_with_proto_header(Some("X-Url-Scheme"))).unwrap(); let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .header(Header::new("X-Url-Scheme", "http")) @@ -41,22 +31,18 @@ mod tests { assert_eq!(response.into_string().unwrap(), "http"); - let response = client.get("/") - .header(Header::new("X-Url-Scheme", "https")) - .dispatch(); - + let response = client.get("/").header(Header::new("X-Url-Scheme", "https")).dispatch(); assert_eq!(response.into_string().unwrap(), "https"); let response = client.get("/").dispatch(); - assert_eq!(response.into_string().unwrap(), ""); + assert_eq!(response.status(), Status::InternalServerError); } #[test] fn check_proxy_proto_header_works_again() { let client = Client::debug(rocket_with_proto_header(Some("x-url-scheme"))).unwrap(); - let response = client - .get("/") - .header(Header::new("X-Url-Scheme", "https")) + let response = client.get("/") + .header(Header::new("X-Url-Scheme", "hTTpS")) .dispatch(); assert_eq!(response.into_string().unwrap(), "https"); @@ -65,9 +51,8 @@ mod tests { .merge(("proxy_proto_header", "x-url-scheme")); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") - .header(Header::new("X-url-Scheme", "https")) + let response = client.get("/") + .header(Header::new("X-url-Scheme", "HTTPS")) .dispatch(); assert_eq!(response.into_string().unwrap(), "https"); @@ -76,12 +61,11 @@ mod tests { #[test] fn check_default_proxy_proto_header_works() { let client = Client::debug_with(routes()).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); } #[test] @@ -91,25 +75,23 @@ mod tests { .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); let config = Figment::from(rocket::Config::debug_default()).merge(("proxy_proto_header", false)); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("X-Forwarded-Proto", "https")) .dispatch(); - assert_eq!(response.into_string(), Some("".into())); + assert_eq!(response.status(), Status::InternalServerError); let config = Figment::from(rocket::Config::debug_default()) .merge(("proxy_proto_header", "x-forwarded-proto")); let client = Client::debug(rocket::custom(config).mount("/", routes())).unwrap(); - let response = client - .get("/") + let response = client.get("/") .header(Header::new("x-Forwarded-Proto", "https")) .dispatch(); diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 5cd2c51ec0..0c4b680af6 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -152,8 +152,8 @@ workers = 16 max_blocking = 512 keep_alive = 5 ident = "Rocket" -ip_header = "X-Real-IP" # set to `false` or `None` to disable -proxy_proto_header = `false` # set to `false` or `None` to disable +ip_header = "X-Real-IP" # set to `false` (the default) to disable +proxy_proto_header = `false` # set to `false` (the default) to disable log_level = "normal" temp_dir = "/tmp" cli_colors = true @@ -365,22 +365,30 @@ mutual TLS. ### Proxied TLS -If Rocket is running behind a reverse proxy that terminates TLS, it is useful to -know whether the original connection was made securely. Therefore, Rocket offers -the option to configure a `proxy_proto_header` that is used to determine if the -request is handled in a secure context. The outcome is available via -[`Request::context_is_likely_secure()`] and used to set cookies' secure flag by -default. To enable this behaviour, configure the header as set by your reverse -proxy. For example: +The `proxy_proto_header` configuration parameter allows Rocket applications to +determine when and if a client's initial connection was likely made in a secure +context by examining the header with the configured name. The header's value is +parsed into a [`ProxyProto`], retrievable via [`Request::proxy_proto()`]. + +That value is in-turn inspected to determine if the initial connection was +secure (i.e, made over TLS) and the outcome made available via +[`Request::context_is_likely_secure()`]. The value returned by this method +influences cookie defaults. In particular, if the method returns `true` (i.e, +the request context is likely secure), the `Secure` cookie flag is set by +default when a cookie is added to a [`CookieJar`]. + +To enable this behaviour, configure the header as set by your reverse proxy or +forwarding entity. For example, to set the header name to `X-Forwarded-Proto` +via a TOML file: ```toml,ignore -proxy_proto_header = 'X-Forwarded-Proto' +proxy_proto_header = "X-Forwarded-Proto" ``` -Note that this only sets the cookies' secure flag when not configured -explicitly. This setting also provides the [request guard `ProxyProto`]. - -[`ProxyProto`]: @api/rocket/request/trait.FromRequest.html#impl-FromRequest%3C'r%3E-for-%26ProxyProto +[`Request::proxy_proto()`]: @api/rocket/request/struct.Request.html#method.proxy_proto +[`ProxyProto`]: @api/rocket/http/enum.ProxyProto.html +[`CookieJar`]: @api/rocket/http/struct.CookieJar.html +[`Request::context_is_likely_secure()`]: @api/rocket/request/struct.Request.html#method.context_is_likely_secure ### Workers From fd294049c784cb52680a423616fadc29d57fa25b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 19 Dec 2023 14:32:11 -0800 Subject: [PATCH 069/178] Update to hyper 1. Enable custom + unix listeners. This commit completely rewrites Rocket's HTTP serving. In addition to significant internal cleanup, this commit introduces the following major features: * Support for custom, external listeners in the `listener` module. The new `listener` module contains new `Bindable`, `Listener`, and `Connection` traits which enable composable, external implementations of connection listeners. Rocket can launch on any `Listener`, or anything that can be used to create a listener (`Bindable`), via a new `launch_on()` method. * Support for Unix domain socket listeners out of the box. The default listener backwards compatibly supports listening on Unix domain sockets. To do so, configure an `address` of `unix:path/to/socket` and optional set `reuse` to `true` (the default) or `false` which controls whether Rocket will handle creating and deleting the unix domain socket. In addition to these new features, this commit makes the following major improvements: * Rocket now depends on hyper 1. * Rocket no longer depends on hyper to handle connections. This allows us to handle more connection failure conditions which results in an overall more robust server with fewer dependencies. * Logic to work around hyper's inability to reference incoming request data in the response results in a 15% performance improvement. * `Client`s can be marked secure with `Client::{un}tracked_secure()`, allowing Rocket to treat local connections as running under TLS. * The `macros` feature of `tokio` is no longer used by Rocket itself. Dependencies can take advantage of this reduction in compile-time cost by disabling the new default feature `tokio-macros`. * A new `TlsConfig::validate()` method allows checking a TLS config. * New `TlsConfig::{certs,key}_reader()`, `MtlsConfig::ca_certs_reader()` methods return `BufReader`s, which allow reading the configured certs and key directly. * A new `NamedFile::open_with()` constructor allows specifying `OpenOptions`. These improvements resulted in the following breaking changes: * The MSRV is now 1.74. * `hyper` is no longer exported from `rocket::http`. * `IoHandler::io` takes `Box` instead of `Pin>`. - Use `Box::into_pin(self)` to recover the previous type. * `Response::upgrade()` now returns an `&mut dyn IoHandler`, not `Pin<& mut _>`. * `Config::{address,port,tls,mtls}` methods have been removed. - Use methods on `Rocket::endpoint()` instead. * `TlsConfig` was moved to `tls::TlsConfig`. * `MutualTls` was renamed and moved to `mtls::MtlsConfig`. * `ErrorKind::TlsBind` was removed. * The second field of `ErrorKind::Shutdown` was removed. * `{Local}Request::{set_}remote()` methods take/return an `Endpoint`. * `Client::new()` was removed; it was previously deprecated. Internally, the following major changes were made: * A new `async_bound` attribute macro was introduced to allow setting bounds on futures returned by `async fn`s in traits while maintaining good docs. * All utility functionality was moved to a new `util` module. Resolves #2671. Resolves #1070. --- contrib/ws/src/duplex.rs | 1 - contrib/ws/src/websocket.rs | 19 +- core/codegen/src/attribute/async_bound/mod.rs | 61 ++ core/codegen/src/attribute/mod.rs | 1 + core/codegen/src/attribute/route/mod.rs | 2 +- core/codegen/src/http_codegen.rs | 22 +- core/codegen/src/lib.rs | 7 + core/http/Cargo.toml | 23 +- core/http/src/header/header.rs | 3 +- core/http/src/hyper.rs | 35 - core/http/src/lib.rs | 13 +- core/http/src/listener.rs | 257 ------- core/http/src/method.rs | 20 +- core/http/src/tls/listener.rs | 235 ------ core/http/src/tls/mod.rs | 11 - core/lib/Cargo.toml | 66 +- core/lib/src/config/config.rs | 95 +-- core/lib/src/config/mod.rs | 273 +------ core/lib/src/config/secret_key.rs | 2 +- core/lib/src/config/shutdown.rs | 10 +- core/lib/src/data/data_stream.rs | 42 +- core/lib/src/data/io_stream.rs | 19 +- core/lib/src/data/transform.rs | 24 +- core/lib/src/erased.rs | 193 +++++ core/lib/src/error.rs | 98 ++- core/lib/src/ext.rs | 404 ----------- core/lib/src/form/mod.rs | 5 +- core/lib/src/fs/named_file.rs | 9 +- core/lib/src/{ => http}/cookies.rs | 17 +- core/lib/src/http/mod.rs | 12 + core/lib/src/lib.rs | 114 +-- core/lib/src/lifecycle.rs | 272 +++++++ core/lib/src/listener/bindable.rs | 40 + core/lib/src/listener/bounced.rs | 58 ++ core/lib/src/listener/cancellable.rs | 273 +++++++ core/lib/src/listener/connection.rs | 93 +++ core/lib/src/listener/default.rs | 61 ++ core/lib/src/listener/endpoint.rs | 281 +++++++ core/lib/src/listener/listener.rs | 65 ++ core/lib/src/listener/mod.rs | 24 + core/lib/src/listener/tcp.rs | 43 ++ core/lib/src/listener/tls.rs | 116 +++ core/lib/src/listener/unix.rs | 107 +++ core/lib/src/local/asynchronous/client.rs | 13 +- core/lib/src/local/asynchronous/request.rs | 6 +- core/lib/src/local/asynchronous/response.rs | 12 +- core/lib/src/local/blocking/client.rs | 6 +- core/lib/src/local/blocking/request.rs | 2 +- core/lib/src/local/client.rs | 22 +- core/lib/src/local/request.rs | 32 +- core/lib/src/mtls.rs | 25 - .../mtls.rs => lib/src/mtls/certificate.rs} | 307 +------- core/lib/src/mtls/config.rs | 212 ++++++ core/lib/src/mtls/error.rs | 74 ++ core/lib/src/mtls/mod.rs | 56 ++ core/lib/src/mtls/name.rs | 146 ++++ core/lib/src/phase.rs | 2 + core/lib/src/request/atomic_method.rs | 43 ++ core/lib/src/request/from_request.rs | 21 +- core/lib/src/request/mod.rs | 2 + core/lib/src/request/request.rs | 181 +++-- core/lib/src/request/tests.rs | 11 +- core/lib/src/response/response.rs | 43 +- core/lib/src/response/stream/sse.rs | 33 +- core/lib/src/rocket.rs | 86 ++- core/lib/src/route/handler.rs | 1 - core/lib/src/server.rs | 684 ++++-------------- core/lib/src/shield/shield.rs | 2 +- core/lib/src/shutdown.rs | 2 +- core/lib/src/{config/tls.rs => tls/config.rs} | 592 +++++++-------- core/{http => lib}/src/tls/error.rs | 3 + core/lib/src/tls/mod.rs | 7 + core/{http => lib}/src/tls/util.rs | 0 core/lib/src/util/chain.rs | 52 ++ core/lib/src/util/join.rs | 77 ++ core/lib/src/util/mod.rs | 12 + core/lib/src/util/reader_stream.rs | 124 ++++ .../src/{trip_wire.rs => util/tripwire.rs} | 0 core/lib/src/util/unix.rs | 25 + core/lib/tests/can-launch-tls.rs | 8 +- .../on_launch_fairing_can_inspect_port.rs | 8 +- core/lib/tests/sentinel.rs | 2 +- core/lib/tests/tls-config-from-source-1503.rs | 13 +- examples/config/src/tests.rs | 4 - examples/hello/src/main.rs | 11 - examples/tls/src/redirector.rs | 67 +- examples/tls/src/tests.rs | 71 +- examples/upgrade/static/index.html | 2 +- scripts/mk-docs.sh | 6 +- scripts/test.sh | 3 +- 90 files changed, 3630 insertions(+), 3007 deletions(-) create mode 100644 core/codegen/src/attribute/async_bound/mod.rs delete mode 100644 core/http/src/hyper.rs delete mode 100644 core/http/src/listener.rs delete mode 100644 core/http/src/tls/listener.rs delete mode 100644 core/http/src/tls/mod.rs create mode 100644 core/lib/src/erased.rs delete mode 100644 core/lib/src/ext.rs rename core/lib/src/{ => http}/cookies.rs (97%) create mode 100644 core/lib/src/http/mod.rs create mode 100644 core/lib/src/lifecycle.rs create mode 100644 core/lib/src/listener/bindable.rs create mode 100644 core/lib/src/listener/bounced.rs create mode 100644 core/lib/src/listener/cancellable.rs create mode 100644 core/lib/src/listener/connection.rs create mode 100644 core/lib/src/listener/default.rs create mode 100644 core/lib/src/listener/endpoint.rs create mode 100644 core/lib/src/listener/listener.rs create mode 100644 core/lib/src/listener/mod.rs create mode 100644 core/lib/src/listener/tcp.rs create mode 100644 core/lib/src/listener/tls.rs create mode 100644 core/lib/src/listener/unix.rs delete mode 100644 core/lib/src/mtls.rs rename core/{http/src/tls/mtls.rs => lib/src/mtls/certificate.rs} (50%) create mode 100644 core/lib/src/mtls/config.rs create mode 100644 core/lib/src/mtls/error.rs create mode 100644 core/lib/src/mtls/mod.rs create mode 100644 core/lib/src/mtls/name.rs create mode 100644 core/lib/src/request/atomic_method.rs rename core/lib/src/{config/tls.rs => tls/config.rs} (56%) rename core/{http => lib}/src/tls/error.rs (94%) create mode 100644 core/lib/src/tls/mod.rs rename core/{http => lib}/src/tls/util.rs (100%) create mode 100644 core/lib/src/util/chain.rs create mode 100644 core/lib/src/util/join.rs create mode 100644 core/lib/src/util/mod.rs create mode 100644 core/lib/src/util/reader_stream.rs rename core/lib/src/{trip_wire.rs => util/tripwire.rs} (100%) create mode 100644 core/lib/src/util/unix.rs diff --git a/contrib/ws/src/duplex.rs b/contrib/ws/src/duplex.rs index 76b5eac289..04da6160ec 100644 --- a/contrib/ws/src/duplex.rs +++ b/contrib/ws/src/duplex.rs @@ -33,7 +33,6 @@ use crate::result::{Result, Error}; /// /// [`StreamExt`]: rocket::futures::StreamExt /// [`SinkExt`]: rocket::futures::SinkExt - pub struct DuplexStream(tokio_tungstenite::WebSocketStream); impl DuplexStream { diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index 63414a111c..662cbe6574 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -1,5 +1,4 @@ use std::io; -use std::pin::Pin; use rocket::data::{IoHandler, IoStream}; use rocket::futures::{self, StreamExt, SinkExt, future::BoxFuture, stream::SplitStream}; @@ -37,10 +36,6 @@ pub struct WebSocket { } impl WebSocket { - fn new(key: String) -> WebSocket { - WebSocket { config: Config::default(), key } - } - /// Change the default connection configuration to `config`. /// /// # Example @@ -202,7 +197,9 @@ impl<'r> FromRequest<'r> for WebSocket { let is_13 = headers.get_one("Sec-WebSocket-Version").map_or(false, |v| v == "13"); let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); match key { - Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)), + Some(key) if is_upgrade && is_ws && is_13 => { + Outcome::Success(WebSocket { key, config: Config::default() }) + }, Some(_) | None => Outcome::Forward(Status::BadRequest) } } @@ -232,9 +229,9 @@ impl<'r, 'o: 'r, S> Responder<'r, 'o> for MessageStream<'o, S> #[rocket::async_trait] impl IoHandler for Channel<'_> { - async fn io(self: Pin>, io: IoStream) -> io::Result<()> { - let channel = Pin::into_inner(self); - let result = (channel.handler)(DuplexStream::new(io, channel.ws.config).await).await; + async fn io(self: Box, io: IoStream) -> io::Result<()> { + let stream = DuplexStream::new(io, self.ws.config).await; + let result = (self.handler)(stream).await; handle_result(result).map(|_| ()) } } @@ -243,9 +240,9 @@ impl IoHandler for Channel<'_> { impl<'r, S> IoHandler for MessageStream<'r, S> where S: futures::Stream> + Send + 'r { - async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + async fn io(self: Box, io: IoStream) -> io::Result<()> { let (mut sink, source) = DuplexStream::new(io, self.ws.config).await.split(); - let stream = (Pin::into_inner(self).handler)(source); + let stream = (self.handler)(source); rocket::tokio::pin!(stream); while let Some(msg) = stream.next().await { let result = match msg { diff --git a/core/codegen/src/attribute/async_bound/mod.rs b/core/codegen/src/attribute/async_bound/mod.rs new file mode 100644 index 0000000000..d58f0afe9f --- /dev/null +++ b/core/codegen/src/attribute/async_bound/mod.rs @@ -0,0 +1,61 @@ +use proc_macro2::{TokenStream, Span}; +use devise::{Spanned, Result, ext::SpanDiagnosticExt}; +use syn::{Token, parse_quote, parse_quote_spanned}; +use syn::{TraitItemFn, TypeParamBound, ReturnType, Attribute}; +use syn::punctuated::Punctuated; +use syn::parse::Parser; + +fn _async_bound( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream +) -> Result { + let bounds = >::parse_terminated.parse(args)?; + if bounds.is_empty() { + return Ok(input.into()); + } + + let mut func: TraitItemFn = syn::parse(input)?; + let original: TraitItemFn = func.clone(); + if !func.sig.asyncness.is_some() { + let diag = Span::call_site() + .error("attribute can only be applied to async fns") + .span_help(func.sig.span(), "this fn declaration must be `async`"); + + return Err(diag); + } + + let doc: Attribute = parse_quote! { + #[doc = concat!( + "# Future Bounds", + "\n", + "**The `Future` generated by this `async fn` must be `", stringify!(#bounds), "`**." + )] + }; + + func.sig.asyncness = None; + func.sig.output = match func.sig.output { + ReturnType::Type(arrow, ty) => parse_quote_spanned!(ty.span() => + #arrow impl ::core::future::Future + #bounds + ), + default@ReturnType::Default => default + }; + + Ok(quote! { + #[cfg(all(not(doc), rust_analyzer))] + #original + + #[cfg(all(doc, not(rust_analyzer)))] + #doc + #original + + #[cfg(not(any(doc, rust_analyzer)))] + #func + }) +} + +pub fn async_bound( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream +) -> TokenStream { + _async_bound(args, input).unwrap_or_else(|d| d.emit_as_item_tokens()) +} diff --git a/core/codegen/src/attribute/mod.rs b/core/codegen/src/attribute/mod.rs index 4d06591df9..c851bebcd2 100644 --- a/core/codegen/src/attribute/mod.rs +++ b/core/codegen/src/attribute/mod.rs @@ -2,3 +2,4 @@ pub mod entry; pub mod catch; pub mod route; pub mod param; +pub mod async_bound; diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 6e29a401c0..dbf28a5180 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -331,7 +331,7 @@ fn codegen_route(route: Route) -> Result { let internal_uri_macro = internal_uri_macro_decl(&route); let responder_outcome = responder_outcome_expr(&route); - let method = route.attr.method; + let method = &route.attr.method; let uri = route.attr.uri.to_string(); let rank = Optional(route.attr.rank); let format = Optional(route.attr.format.as_ref()); diff --git a/core/codegen/src/http_codegen.rs b/core/codegen/src/http_codegen.rs index 8d021a38d6..35a5cb9a8d 100644 --- a/core/codegen/src/http_codegen.rs +++ b/core/codegen/src/http_codegen.rs @@ -13,7 +13,7 @@ pub struct Status(pub http::Status); #[derive(Debug)] pub struct MediaType(pub http::MediaType); -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct Method(pub http::Method); #[derive(Clone, Debug)] @@ -108,7 +108,7 @@ const VALID_METHODS: &[http::Method] = &[ impl FromMeta for Method { fn from_meta(meta: &MetaItem) -> Result { let span = meta.value_span(); - let help_text = format!("method must be one of: {}", VALID_METHODS_STR); + let help_text = format!("method must be one of: {VALID_METHODS_STR}"); if let MetaItem::Path(path) = meta { if let Some(ident) = path.last_ident() { @@ -131,19 +131,13 @@ impl FromMeta for Method { impl ToTokens for Method { fn to_tokens(&self, tokens: &mut TokenStream) { - let method_tokens = match self.0 { - http::Method::Get => quote!(::rocket::http::Method::Get), - http::Method::Put => quote!(::rocket::http::Method::Put), - http::Method::Post => quote!(::rocket::http::Method::Post), - http::Method::Delete => quote!(::rocket::http::Method::Delete), - http::Method::Options => quote!(::rocket::http::Method::Options), - http::Method::Head => quote!(::rocket::http::Method::Head), - http::Method::Trace => quote!(::rocket::http::Method::Trace), - http::Method::Connect => quote!(::rocket::http::Method::Connect), - http::Method::Patch => quote!(::rocket::http::Method::Patch), - }; + let mut chars = self.0.as_str().chars(); + let variant_str = chars.next() + .map(|c| c.to_ascii_uppercase().to_string() + &chars.as_str().to_lowercase()) + .unwrap_or_default(); - tokens.extend(method_tokens); + let variant = syn::Ident::new(&variant_str, Span::call_site()); + tokens.extend(quote!(::rocket::http::Method::#variant)); } } diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index c375351f1c..aab19f3049 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1497,3 +1497,10 @@ pub fn internal_guide_tests(input: TokenStream) -> TokenStream { pub fn export(input: TokenStream) -> TokenStream { emit!(bang::export_internal(input)) } + +/// Private Rocket attribute: `async_bound(Bounds + On + Returned + Future)`. +#[doc(hidden)] +#[proc_macro_attribute] +pub fn async_bound(args: TokenStream, input: TokenStream) -> TokenStream { + emit!(attribute::async_bound::async_bound(args, input)) +} diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 6c29fa1762..c5f6a309d5 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -17,43 +17,22 @@ rust-version = "1.64" [features] default = [] -tls = ["rustls", "tokio-rustls", "rustls-pemfile"] -mtls = ["tls", "x509-parser"] -http2 = ["hyper/http2"] -private-cookies = ["cookie/private", "cookie/key-expansion"] serde = ["uncased/with-serde-alloc", "serde_"] uuid = ["uuid_"] [dependencies] smallvec = { version = "1.11", features = ["const_generics", "const_new"] } percent-encoding = "2" -http = "0.2" time = { version = "0.3", features = ["formatting", "macros"] } indexmap = "2" -rustls = { version = "0.22", optional = true } -tokio-rustls = { version = "0.25", optional = true } -rustls-pemfile = { version = "2.0.0", optional = true } -tokio = { version = "1.6.1", features = ["net", "sync", "time"] } -log = "0.4" ref-cast = "1.0" -uncased = "0.9.6" +uncased = "0.9.10" either = "1" pear = "0.2.8" -pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" cookie = { version = "0.18", features = ["percent-encode"] } state = "0.6" -futures = { version = "0.3", default-features = false } - -[dependencies.x509-parser] -version = "0.13" -optional = true - -[dependencies.hyper] -version = "0.14.9" -default-features = false -features = ["http1", "runtime", "server", "stream"] [dependencies.serde_] package = "serde" diff --git a/core/http/src/header/header.rs b/core/http/src/header/header.rs index e51be2097d..8a76b6a3f3 100644 --- a/core/http/src/header/header.rs +++ b/core/http/src/header/header.rs @@ -745,8 +745,7 @@ impl<'h> HeaderMap<'h> { /// WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] #[inline] - pub fn into_iter_raw(self) - -> impl Iterator, Vec>)> { + pub fn into_iter_raw(self) -> impl Iterator, Vec>)> { self.headers.into_iter() } } diff --git a/core/http/src/hyper.rs b/core/http/src/hyper.rs deleted file mode 100644 index 2e98e1f01f..0000000000 --- a/core/http/src/hyper.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Re-exported hyper HTTP library types. -//! -//! All types that are re-exported from Hyper reside inside of this module. -//! These types will, with certainty, be removed with time, but they reside here -//! while necessary. - -pub use hyper::{Method, Error, Body, Uri, Version, Request, Response}; -pub use hyper::{body, server, service, upgrade}; -pub use http::{HeaderValue, request, uri}; - -/// Reexported Hyper HTTP header types. -pub mod header { - macro_rules! import_http_headers { - ($($name:ident),*) => ($( - pub use hyper::header::$name as $name; - )*) - } - - import_http_headers! { - ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, - ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, - ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, - ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE, - ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ALLOW, - AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, - CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, - CONTENT_RANGE, CONTENT_SECURITY_POLICY, - CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, DATE, ETAG, EXPECT, - EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, - IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, - LOCATION, ORIGIN, PRAGMA, RANGE, REFERER, REFERRER_POLICY, REFRESH, - STRICT_TRANSPORT_SECURITY, TE, TRANSFER_ENCODING, UPGRADE, USER_AGENT, - VARY - } -} diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 7ab89758d6..86935cce74 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -4,15 +4,11 @@ //! Types that map to concepts in HTTP. //! //! This module exports types that map to HTTP concepts or to the underlying -//! HTTP library when needed. Because the underlying HTTP library is likely to -//! change (see [#17]), types in [`hyper`] should be considered unstable. -//! -//! [#17]: https://github.com/rwf2/Rocket/issues/17 +//! HTTP library when needed. #[macro_use] extern crate pear; -pub mod hyper; pub mod uri; pub mod ext; @@ -22,7 +18,6 @@ mod method; mod status; mod raw_str; mod parse; -mod listener; /// Case-preserving, ASCII case-insensitive string types. /// @@ -39,14 +34,8 @@ pub mod uncased { pub mod private { pub use crate::parse::Indexed; pub use smallvec::{SmallVec, Array}; - pub use crate::listener::{TcpListener, Incoming, Listener, Connection, Certificates}; - pub use cookie; } -#[doc(hidden)] -#[cfg(feature = "tls")] -pub mod tls; - pub use crate::method::Method; pub use crate::status::{Status, StatusClass}; pub use crate::raw_str::{RawStr, RawStrBuf}; diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs deleted file mode 100644 index 956c8ec4a2..0000000000 --- a/core/http/src/listener.rs +++ /dev/null @@ -1,257 +0,0 @@ -use std::fmt; -use std::future::Future; -use std::io; -use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::time::Duration; -use std::sync::Arc; - -use log::warn; -use tokio::time::Sleep; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::net::TcpStream; -use hyper::server::accept::Accept; -use state::InitCell; - -pub use tokio::net::TcpListener; - -/// A thin wrapper over raw, DER-encoded X.509 client certificate data. -#[cfg(not(feature = "tls"))] -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CertificateDer(pub(crate) Vec); - -/// A thin wrapper over raw, DER-encoded X.509 client certificate data. -#[cfg(feature = "tls")] -#[derive(Debug, Clone, Eq, PartialEq)] -#[repr(transparent)] -pub struct CertificateDer(pub(crate) rustls::pki_types::CertificateDer<'static>); - -/// A collection of raw certificate data. -#[derive(Clone, Default)] -pub struct Certificates(Arc>>); - -impl From> for Certificates { - fn from(value: Vec) -> Self { - Certificates(Arc::new(value.into())) - } -} - -#[cfg(feature = "tls")] -impl From>> for Certificates { - fn from(value: Vec>) -> Self { - let value: Vec<_> = value.into_iter().map(CertificateDer).collect(); - Certificates(Arc::new(value.into())) - } -} - -#[doc(hidden)] -impl Certificates { - /// Set the the raw certificate chain data. Only the first call actually - /// sets the data; the remaining do nothing. - #[cfg(feature = "tls")] - pub(crate) fn set(&self, data: Vec) { - self.0.set(data); - } - - /// Returns the raw certificate chain data, if any is available. - pub fn chain_data(&self) -> Option<&[CertificateDer]> { - self.0.try_get().map(|v| v.as_slice()) - } -} - -// TODO.async: 'Listener' and 'Connection' provide common enough functionality -// that they could be introduced in upstream libraries. -/// A 'Listener' yields incoming connections -pub trait Listener { - /// The connection type returned by this listener. - type Connection: Connection; - - /// Return the actual address this listener bound to. - fn local_addr(&self) -> Option; - - /// Try to accept an incoming Connection if ready. This should only return - /// an `Err` when a fatal problem occurs as Hyper kills the server on `Err`. - fn poll_accept( - self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll>; -} - -/// A 'Connection' represents an open connection to a client -pub trait Connection: AsyncRead + AsyncWrite { - /// The remote address, i.e. the client's socket address, if it is known. - fn peer_address(&self) -> Option; - - /// Requests that the connection not delay reading or writing data as much - /// as possible. For connections backed by TCP, this corresponds to setting - /// `TCP_NODELAY`. - fn enable_nodelay(&self) -> io::Result<()>; - - /// DER-encoded X.509 certificate chain presented by the client, if any. - /// - /// The certificate order must be as it appears in the TLS protocol: the - /// first certificate relates to the peer, the second certifies the first, - /// the third certifies the second, and so on. - /// - /// Defaults to an empty vector to indicate that no certificates were - /// presented. - fn peer_certificates(&self) -> Option { None } -} - -pin_project_lite::pin_project! { - /// This is a generic version of hyper's AddrIncoming that is intended to be - /// usable with listeners other than a plain TCP stream, e.g. TLS and/or Unix - /// sockets. It does so by bridging the `Listener` trait to what hyper wants (an - /// Accept). This type is internal to Rocket. - #[must_use = "streams do nothing unless polled"] - pub struct Incoming { - sleep_on_errors: Option, - nodelay: bool, - #[pin] - pending_error_delay: Option, - #[pin] - listener: L, - } -} - -impl Incoming { - /// Construct an `Incoming` from an existing `Listener`. - pub fn new(listener: L) -> Self { - Self { - listener, - sleep_on_errors: Some(Duration::from_millis(250)), - pending_error_delay: None, - nodelay: false, - } - } - - /// Set whether and how long to sleep on accept errors. - /// - /// A possible scenario is that the process has hit the max open files - /// allowed, and so trying to accept a new connection will fail with - /// `EMFILE`. In some cases, it's preferable to just wait for some time, if - /// the application will likely close some files (or connections), and try - /// to accept the connection again. If this option is `true`, the error - /// will be logged at the `error` level, since it is still a big deal, - /// and then the listener will sleep for 1 second. - /// - /// In other cases, hitting the max open files should be treat similarly - /// to being out-of-memory, and simply error (and shutdown). Setting - /// this option to `None` will allow that. - /// - /// Default is 1 second. - pub fn sleep_on_errors(mut self, val: Option) -> Self { - self.sleep_on_errors = val; - self - } - - /// Set whether to request no delay on all incoming connections. The default - /// is `false`. See [`Connection::enable_nodelay()`] for details. - pub fn nodelay(mut self, nodelay: bool) -> Self { - self.nodelay = nodelay; - self - } - - fn poll_accept_next( - self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - /// This function defines per-connection errors: errors that affect only - /// a single connection's accept() and don't imply anything about the - /// success probability of the next accept(). Thus, we can attempt to - /// `accept()` another connection immediately. All other errors will - /// incur a delay before the next `accept()` is performed. The delay is - /// useful to handle resource exhaustion errors like ENFILE and EMFILE. - /// Otherwise, could enter into tight loop. - fn is_connection_error(e: &io::Error) -> bool { - matches!(e.kind(), - | io::ErrorKind::ConnectionRefused - | io::ErrorKind::ConnectionAborted - | io::ErrorKind::ConnectionReset) - } - - let mut this = self.project(); - loop { - // Check if a previous sleep timer is active, set on I/O errors. - if let Some(delay) = this.pending_error_delay.as_mut().as_pin_mut() { - futures::ready!(delay.poll(cx)); - } - - this.pending_error_delay.set(None); - - match futures::ready!(this.listener.as_mut().poll_accept(cx)) { - Ok(stream) => { - if *this.nodelay { - if let Err(e) = stream.enable_nodelay() { - warn!("failed to enable NODELAY: {}", e); - } - } - - return Poll::Ready(Ok(stream)); - }, - Err(e) => { - if is_connection_error(&e) { - warn!("single connection accept error {}; accepting next now", e); - } else if let Some(duration) = this.sleep_on_errors { - // We might be able to recover. Try again in a bit. - warn!("accept error {}; recovery attempt in {}ms", e, duration.as_millis()); - this.pending_error_delay.set(Some(tokio::time::sleep(*duration))); - } else { - return Poll::Ready(Err(e)); - } - }, - } - } - } -} - -impl Accept for Incoming { - type Conn = L::Connection; - type Error = io::Error; - - #[inline] - fn poll_accept( - self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll>> { - self.poll_accept_next(cx).map(Some) - } -} - -impl fmt::Debug for Incoming { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Incoming") - .field("listener", &self.listener) - .finish() - } -} - -impl Listener for TcpListener { - type Connection = TcpStream; - - #[inline] - fn local_addr(&self) -> Option { - self.local_addr().ok() - } - - #[inline] - fn poll_accept( - self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - (*self).poll_accept(cx).map_ok(|(stream, _addr)| stream) - } -} - -impl Connection for TcpStream { - #[inline] - fn peer_address(&self) -> Option { - self.peer_addr().ok() - } - - #[inline] - fn enable_nodelay(&self) -> io::Result<()> { - self.set_nodelay(true) - } -} diff --git a/core/http/src/method.rs b/core/http/src/method.rs index 734d7681f7..a959fbc203 100644 --- a/core/http/src/method.rs +++ b/core/http/src/method.rs @@ -3,8 +3,6 @@ use std::str::FromStr; use self::Method::*; -use crate::hyper; - // TODO: Support non-standard methods, here and in codegen? /// Representation of HTTP methods. @@ -29,6 +27,7 @@ use crate::hyper; /// } /// # } /// ``` +#[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Method { /// The `GET` variant. @@ -52,23 +51,6 @@ pub enum Method { } impl Method { - /// WARNING: This is unstable! Do not use this method outside of Rocket! - #[doc(hidden)] - pub fn from_hyp(method: &hyper::Method) -> Option { - match *method { - hyper::Method::GET => Some(Get), - hyper::Method::PUT => Some(Put), - hyper::Method::POST => Some(Post), - hyper::Method::DELETE => Some(Delete), - hyper::Method::OPTIONS => Some(Options), - hyper::Method::HEAD => Some(Head), - hyper::Method::TRACE => Some(Trace), - hyper::Method::CONNECT => Some(Connect), - hyper::Method::PATCH => Some(Patch), - _ => None, - } - } - /// Returns `true` if an HTTP request with the method represented by `self` /// always supports a payload. /// diff --git a/core/http/src/tls/listener.rs b/core/http/src/tls/listener.rs deleted file mode 100644 index 7ef76ebd8d..0000000000 --- a/core/http/src/tls/listener.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::io; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::future::Future; -use std::net::SocketAddr; - -use tokio::net::{TcpListener, TcpStream}; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio_rustls::{Accept, TlsAcceptor, server::TlsStream as BareTlsStream}; -use rustls::server::{ServerSessionMemoryCache, ServerConfig, WebPkiClientVerifier}; - -use crate::tls::util::{load_cert_chain, load_key, load_ca_certs}; -use crate::listener::{Connection, Listener, Certificates, CertificateDer}; - -/// A TLS listener over TCP. -pub struct TlsListener { - listener: TcpListener, - acceptor: TlsAcceptor, -} - -/// This implementation exists so that ROCKET_WORKERS=1 can make progress while -/// a TLS handshake is being completed. It does this by returning `Ready` from -/// `poll_accept()` as soon as we have a TCP connection and performing the -/// handshake in the `AsyncRead` and `AsyncWrite` implementations. -/// -/// A straight-forward implementation of this strategy results in none of the -/// TLS information being available at the time the connection is "established", -/// that is, when `poll_accept()` returns, since the handshake has yet to occur. -/// Importantly, certificate information isn't available at the time that we -/// request it. -/// -/// The underlying problem is hyper's "Accept" trait. Were we to manage -/// connections ourselves, we'd likely want to: -/// -/// 1. Stop blocking the worker as soon as we have a TCP connection. -/// 2. Perform the handshake in the background. -/// 3. Give the connection to Rocket when/if the handshake is done. -/// -/// See hyperium/hyper/issues/2321 for more details. -/// -/// To work around this, we "lie" when `peer_certificates()` are requested and -/// always return `Some(Certificates)`. Internally, `Certificates` is an -/// `Arc>>`, effectively a shared, thread-safe, -/// `OnceCell`. The cell is initially empty and is filled as soon as the -/// handshake is complete. If the certificate data were to be requested prior to -/// this point, it would be empty. However, in Rocket, we only request -/// certificate data when we have a `Request` object, which implies we're -/// receiving payload data, which implies the TLS handshake has finished, so the -/// certificate data as seen by a Rocket application will always be "fresh". -pub struct TlsStream { - remote: SocketAddr, - state: TlsState, - certs: Certificates, -} - -/// State of `TlsStream`. -pub enum TlsState { - /// The TLS handshake is taking place. We don't have a full connection yet. - Handshaking(Accept), - /// TLS handshake completed successfully; we're getting payload data. - Streaming(BareTlsStream), -} - -/// TLS as ~configured by `TlsConfig` in `rocket` core. -pub struct Config { - pub cert_chain: R, - pub private_key: R, - pub ciphersuites: Vec, - pub prefer_server_order: bool, - pub ca_certs: Option, - pub mandatory_mtls: bool, -} - -impl TlsListener { - pub async fn bind(addr: SocketAddr, mut c: Config) -> crate::tls::Result - where R: io::BufRead - { - let provider = rustls::crypto::CryptoProvider { - cipher_suites: c.ciphersuites, - ..rustls::crypto::ring::default_provider() - }; - - let verifier = match c.ca_certs { - Some(ref mut ca_certs) => { - let ca_roots = Arc::new(load_ca_certs(ca_certs)?); - let verifier = WebPkiClientVerifier::builder(ca_roots); - match c.mandatory_mtls { - true => verifier.build()?, - false => verifier.allow_unauthenticated().build()?, - } - }, - None => WebPkiClientVerifier::no_client_auth(), - }; - - let key = load_key(&mut c.private_key)?; - let cert_chain = load_cert_chain(&mut c.cert_chain)?; - let mut config = ServerConfig::builder_with_provider(Arc::new(provider)) - .with_safe_default_protocol_versions()? - .with_client_cert_verifier(verifier) - .with_single_cert(cert_chain, key)?; - - config.ignore_client_order = c.prefer_server_order; - config.session_storage = ServerSessionMemoryCache::new(1024); - config.ticketer = rustls::crypto::ring::Ticketer::new()?; - config.alpn_protocols = vec![b"http/1.1".to_vec()]; - if cfg!(feature = "http2") { - config.alpn_protocols.insert(0, b"h2".to_vec()); - } - - let listener = TcpListener::bind(addr).await?; - let acceptor = TlsAcceptor::from(Arc::new(config)); - Ok(TlsListener { listener, acceptor }) - } -} - -impl Listener for TlsListener { - type Connection = TlsStream; - - fn local_addr(&self) -> Option { - self.listener.local_addr().ok() - } - - fn poll_accept( - self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - match futures::ready!(self.listener.poll_accept(cx)) { - Ok((io, addr)) => Poll::Ready(Ok(TlsStream { - remote: addr, - state: TlsState::Handshaking(self.acceptor.accept(io)), - // These are empty and filled in after handshake is complete. - certs: Certificates::default(), - })), - Err(e) => Poll::Ready(Err(e)), - } - } -} - -impl Connection for TlsStream { - fn peer_address(&self) -> Option { - Some(self.remote) - } - - fn enable_nodelay(&self) -> io::Result<()> { - // If `Handshaking` is `None`, it either failed, so we returned an `Err` - // from `poll_accept()` and there's no connection to enable `NODELAY` - // on, or it succeeded, so we're in the `Streaming` stage and we have - // infallible access to the connection. - match &self.state { - TlsState::Handshaking(accept) => match accept.get_ref() { - None => Ok(()), - Some(s) => s.enable_nodelay(), - }, - TlsState::Streaming(stream) => stream.get_ref().0.enable_nodelay() - } - } - - fn peer_certificates(&self) -> Option { - Some(self.certs.clone()) - } -} - -impl TlsStream { - fn poll_accept_then( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - mut f: F - ) -> Poll> - where F: FnMut(&mut BareTlsStream, &mut Context<'_>) -> Poll> - { - loop { - match self.state { - TlsState::Handshaking(ref mut accept) => { - match futures::ready!(Pin::new(accept).poll(cx)) { - Ok(stream) => { - if let Some(peer_certs) = stream.get_ref().1.peer_certificates() { - self.certs.set(peer_certs.into_iter() - .map(|v| CertificateDer(v.clone().into_owned())) - .collect()); - } - - self.state = TlsState::Streaming(stream); - } - Err(e) => { - log::warn!("tls handshake with {} failed: {}", self.remote, e); - return Poll::Ready(Err(e)); - } - } - }, - TlsState::Streaming(ref mut stream) => return f(stream, cx), - } - } - } -} - -impl AsyncRead for TlsStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - self.poll_accept_then(cx, |stream, cx| Pin::new(stream).poll_read(cx, buf)) - } -} - -impl AsyncWrite for TlsStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - self.poll_accept_then(cx, |stream, cx| Pin::new(stream).poll_write(cx, buf)) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match &mut self.state { - TlsState::Handshaking(accept) => match accept.get_mut() { - Some(io) => Pin::new(io).poll_flush(cx), - None => Poll::Ready(Ok(())), - } - TlsState::Streaming(stream) => Pin::new(stream).poll_flush(cx), - } - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match &mut self.state { - TlsState::Handshaking(accept) => match accept.get_mut() { - Some(io) => Pin::new(io).poll_shutdown(cx), - None => Poll::Ready(Ok(())), - } - TlsState::Streaming(stream) => Pin::new(stream).poll_shutdown(cx), - } - } -} diff --git a/core/http/src/tls/mod.rs b/core/http/src/tls/mod.rs deleted file mode 100644 index 8d3bcb3d67..0000000000 --- a/core/http/src/tls/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod listener; - -#[cfg(feature = "mtls")] -pub mod mtls; - -pub use rustls; -pub use listener::{TlsListener, Config}; -pub mod util; -pub mod error; - -pub use error::Result; diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 01b2b370b1..6724b21d32 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -20,23 +20,36 @@ rust-version = "1.64" all-features = true [features] -default = ["http2"] -tls = ["rocket_http/tls"] -mtls = ["rocket_http/mtls", "tls"] -http2 = ["rocket_http/http2"] -secrets = ["rocket_http/private-cookies"] -json = ["serde_json", "tokio/io-util"] -msgpack = ["rmp-serde", "tokio/io-util"] +default = ["http2", "tokio-macros"] +http2 = ["hyper/http2", "hyper-util/http2"] +secrets = ["cookie/private", "cookie/key-expansion"] +json = ["serde_json"] +msgpack = ["rmp-serde"] uuid = ["uuid_", "rocket_http/uuid"] +tls = ["rustls", "tokio-rustls", "rustls-pemfile"] +mtls = ["tls", "x509-parser"] +tokio-macros = ["tokio/macros"] [dependencies] -# Serialization dependencies. +# Optional serialization dependencies. serde_json = { version = "1.0.26", optional = true } rmp-serde = { version = "1", optional = true } uuid_ = { package = "uuid", version = "1", optional = true, features = ["serde"] } +# Optional TLS dependencies +rustls = { version = "0.22", optional = true } +tokio-rustls = { version = "0.25", optional = true } +rustls-pemfile = { version = "2.0.0", optional = true } + +# Optional MTLS dependencies +x509-parser = { version = "0.13", optional = true } + +# Hyper dependencies +http = "1" +bytes = "1.4" +hyper = { version = "1.1", default-features = false, features = ["http1", "server"] } + # Non-optional, core dependencies from here on out. -futures = { version = "0.3.0", default-features = false, features = ["std"] } yansi = { version = "1.0.0-rc", features = ["detect-tty"] } log = { version = "0.4", features = ["std"] } num_cpus = "1.0" @@ -44,11 +57,11 @@ time = { version = "0.3", features = ["macros", "parsing"] } memchr = "2" # TODO: Use pear instead. binascii = "0.1" ref-cast = "1.0" -atomic = "0.5" +ref-swap = "0.1.2" parking_lot = "0.12" ubyte = {version = "0.10.2", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } -figment = { version = "0.10.6", features = ["toml", "env"] } +figment = { version = "0.10.13", features = ["toml", "env"] } rand = "0.8" either = "1" pin-project-lite = "0.2" @@ -58,8 +71,25 @@ async-trait = "0.1.43" async-stream = "0.3.2" multer = { version = "3.0.0", features = ["tokio-io"] } tokio-stream = { version = "0.1.6", features = ["signal", "time"] } +cookie = { version = "0.18", features = ["percent-encode"] } +futures = { version = "0.3.30", default-features = false, features = ["std"] } state = "0.6" +[dependencies.hyper-util] +git = "https://github.com/SergioBenitez/hyper-util.git" +branch = "fix-readversion" +default-features = false +features = ["http1", "server", "tokio"] + +[dependencies.tokio] +version = "1.35.1" +features = ["rt-multi-thread", "net", "io-util", "fs", "time", "sync", "signal", "parking_lot"] + +[dependencies.tokio-util] +version = "0.7" +default-features = false +features = ["io"] + [dependencies.rocket_codegen] version = "0.6.0-dev" path = "../codegen" @@ -69,21 +99,13 @@ version = "0.6.0-dev" path = "../http" features = ["serde"] -[dependencies.tokio] -version = "1.6.1" -features = ["fs", "io-std", "io-util", "rt-multi-thread", "sync", "signal", "macros"] - -[dependencies.tokio-util] -version = "0.7" -default-features = false -features = ["io"] - -[dependencies.bytes] -version = "1.0" +[target.'cfg(unix)'.dependencies] +libc = "0.2.149" [build-dependencies] version_check = "0.9.1" [dev-dependencies] +tokio = { version = "1", features = ["macros", "io-std"] } figment = { version = "0.10", features = ["test"] } pretty_assertions = "1" diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 197b6a2f6c..e208944c76 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -1,5 +1,3 @@ -use std::net::{IpAddr, Ipv4Addr}; - use figment::{Figment, Profile, Provider, Metadata, error::Result}; use figment::providers::{Serialized, Env, Toml, Format}; use figment::value::{Map, Dict, magic::RelativePathBuf}; @@ -12,9 +10,6 @@ use crate::request::{self, Request, FromRequest}; use crate::http::uncased::Uncased; use crate::data::Limits; -#[cfg(feature = "tls")] -use crate::config::TlsConfig; - #[cfg(feature = "secrets")] use crate::config::SecretKey; @@ -66,10 +61,6 @@ pub struct Config { /// set to the extracting Figment's selected `Profile`._ #[serde(skip)] pub profile: Profile, - /// IP address to serve on. **(default: `127.0.0.1`)** - pub address: IpAddr, - /// Port to serve on. **(default: `8000`)** - pub port: u16, /// Number of threads to use for executing futures. **(default: `num_cores`)** /// /// _**Note:** Rocket only reads this value from sources in the [default @@ -121,10 +112,6 @@ pub struct Config { pub temp_dir: RelativePathBuf, /// Keep-alive timeout in seconds; disabled when `0`. **(default: `5`)** pub keep_alive: u32, - /// The TLS configuration, if any. **(default: `None`)** - #[cfg(feature = "tls")] - #[cfg_attr(nightly, doc(cfg(feature = "tls")))] - pub tls: Option, /// The secret key for signing and encrypting. **(default: `0`)** /// /// _**Note:** This field _always_ serializes as a 256-bit array of `0`s to @@ -148,7 +135,6 @@ pub struct Config { /// use rocket::Config; /// /// let config = Config { - /// port: 1024, /// keep_alive: 10, /// ..Default::default() /// }; @@ -204,8 +190,6 @@ impl Config { pub fn debug_default() -> Config { Config { profile: Self::DEBUG_PROFILE, - address: Ipv4Addr::new(127, 0, 0, 1).into(), - port: 8000, workers: num_cpus::get(), max_blocking: 512, ident: Ident::default(), @@ -214,8 +198,6 @@ impl Config { limits: Limits::default(), temp_dir: std::env::temp_dir().into(), keep_alive: 5, - #[cfg(feature = "tls")] - tls: None, #[cfg(feature = "secrets")] secret_key: SecretKey::zero(), shutdown: Shutdown::default(), @@ -331,59 +313,6 @@ impl Config { Self::try_from(provider).unwrap_or_else(bail_with_config_error) } - /// Returns `true` if TLS is enabled. - /// - /// TLS is enabled when the `tls` feature is enabled and TLS has been - /// configured with at least one ciphersuite. Note that without changing - /// defaults, all supported ciphersuites are enabled in the recommended - /// configuration. - /// - /// # Example - /// - /// ```rust - /// let config = rocket::Config::default(); - /// if config.tls_enabled() { - /// println!("TLS is enabled!"); - /// } else { - /// println!("TLS is disabled."); - /// } - /// ``` - pub fn tls_enabled(&self) -> bool { - #[cfg(feature = "tls")] { - self.tls.as_ref().map_or(false, |tls| !tls.ciphers.is_empty()) - } - - #[cfg(not(feature = "tls"))] { false } - } - - /// Returns `true` if mTLS is enabled. - /// - /// mTLS is enabled when TLS is enabled ([`Config::tls_enabled()`]) _and_ - /// the `mtls` feature is enabled _and_ mTLS has been configured with a CA - /// certificate chain. - /// - /// # Example - /// - /// ```rust - /// let config = rocket::Config::default(); - /// if config.mtls_enabled() { - /// println!("mTLS is enabled!"); - /// } else { - /// println!("mTLS is disabled."); - /// } - /// ``` - pub fn mtls_enabled(&self) -> bool { - if !self.tls_enabled() { - return false; - } - - #[cfg(feature = "mtls")] { - self.tls.as_ref().map_or(false, |tls| tls.mutual.is_some()) - } - - #[cfg(not(feature = "mtls"))] { false } - } - #[cfg(feature = "secrets")] pub(crate) fn known_secret_key_used(&self) -> bool { const KNOWN_SECRET_KEYS: &'static [&'static str] = &[ @@ -420,8 +349,6 @@ impl Config { self.trace_print(figment); launch_meta!("{}Configured for {}.", "๐Ÿ”ง ".emoji(), self.profile.underline()); - launch_meta_!("address: {}", self.address.paint(VAL)); - launch_meta_!("port: {}", self.port.paint(VAL)); launch_meta_!("workers: {}", self.workers.paint(VAL)); launch_meta_!("max blocking threads: {}", self.max_blocking.paint(VAL)); launch_meta_!("ident: {}", self.ident.paint(VAL)); @@ -445,12 +372,6 @@ impl Config { ka => launch_meta_!("keep-alive: {}{}", ka.paint(VAL), "s".paint(VAL)), } - match (self.tls_enabled(), self.mtls_enabled()) { - (true, true) => launch_meta_!("tls: {}", "enabled w/mtls".paint(VAL)), - (true, false) => launch_meta_!("tls: {} w/o mtls", "enabled".paint(VAL)), - (false, _) => launch_meta_!("tls: {}", "disabled".paint(VAL)), - } - launch_meta_!("shutdown: {}", self.shutdown.paint(VAL)); launch_meta_!("log level: {}", self.log_level.paint(VAL)); launch_meta_!("cli colors: {}", self.cli_colors.paint(VAL)); @@ -519,12 +440,6 @@ impl Config { /// This isn't `pub` because setting it directly does nothing. const PROFILE: &'static str = "profile"; - /// The stringy parameter name for setting/extracting [`Config::address`]. - pub const ADDRESS: &'static str = "address"; - - /// The stringy parameter name for setting/extracting [`Config::port`]. - pub const PORT: &'static str = "port"; - /// The stringy parameter name for setting/extracting [`Config::workers`]. pub const WORKERS: &'static str = "workers"; @@ -546,9 +461,6 @@ impl Config { /// The stringy parameter name for setting/extracting [`Config::limits`]. pub const LIMITS: &'static str = "limits"; - /// The stringy parameter name for setting/extracting [`Config::tls`]. - pub const TLS: &'static str = "tls"; - /// The stringy parameter name for setting/extracting [`Config::secret_key`]. pub const SECRET_KEY: &'static str = "secret_key"; @@ -566,9 +478,10 @@ impl Config { /// An array of all of the stringy parameter names. pub const PARAMETERS: &'static [&'static str] = &[ - Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, Self::KEEP_ALIVE, - Self::IDENT, Self::IP_HEADER, Self::PROXY_PROTO_HEADER, Self::LIMITS, Self::TLS, - Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, Self::CLI_COLORS, + Self::WORKERS, Self::MAX_BLOCKING, Self::KEEP_ALIVE, Self::IDENT, + Self::IP_HEADER, Self::PROXY_PROTO_HEADER, Self::LIMITS, + Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, + Self::CLI_COLORS, ]; } diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 2bf83cd264..86481af1fe 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -117,9 +117,6 @@ mod shutdown; mod cli_colors; mod http_header; -#[cfg(feature = "tls")] -mod tls; - #[cfg(feature = "secrets")] mod secret_key; @@ -132,12 +129,6 @@ pub use shutdown::Shutdown; pub use ident::Ident; pub use cli_colors::CliColors; -#[cfg(feature = "tls")] -pub use tls::{TlsConfig, CipherSuite}; - -#[cfg(feature = "mtls")] -pub use tls::MutualTls; - #[cfg(feature = "secrets")] pub use secret_key::SecretKey; @@ -146,7 +137,6 @@ pub use shutdown::Sig; #[cfg(test)] mod tests { - use std::net::Ipv4Addr; use figment::{Figment, Profile}; use pretty_assertions::assert_eq; @@ -202,9 +192,7 @@ mod tests { figment::Jail::expect_with(|jail| { jail.create_file("Rocket.toml", r#" [default] - address = "1.2.3.4" ident = "Something Cool" - port = 1234 workers = 20 keep_alive = 10 log_level = "off" @@ -213,8 +201,6 @@ mod tests { let config = Config::from(Config::figment()); assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, workers: 20, ident: ident!("Something Cool"), keep_alive: 10, @@ -225,9 +211,7 @@ mod tests { jail.create_file("Rocket.toml", r#" [global] - address = "1.2.3.4" ident = "Something Else Cool" - port = 1234 workers = 20 keep_alive = 10 log_level = "off" @@ -236,8 +220,6 @@ mod tests { let config = Config::from(Config::figment()); assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, workers: 20, ident: ident!("Something Else Cool"), keep_alive: 10, @@ -249,8 +231,6 @@ mod tests { jail.set_env("ROCKET_CONFIG", "Other.toml"); jail.create_file("Other.toml", r#" [default] - address = "1.2.3.4" - port = 1234 workers = 20 keep_alive = 10 log_level = "off" @@ -259,8 +239,6 @@ mod tests { let config = Config::from(Config::figment()); assert_eq!(config, Config { - address: Ipv4Addr::new(1, 2, 3, 4).into(), - port: 1234, workers: 20, keep_alive: 10, log_level: LogLevel::Off, @@ -367,228 +345,6 @@ mod tests { }) } - #[test] - #[cfg(feature = "tls")] - fn test_tls_config_from_file() { - use crate::config::{TlsConfig, CipherSuite, Ident, Shutdown}; - - figment::Jail::expect_with(|jail| { - jail.create_file("Rocket.toml", r#" - [global] - shutdown.ctrlc = 0 - ident = false - - [global.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - - [global.limits] - forms = "1mib" - json = "10mib" - stream = "50kib" - "#)?; - - let config = Config::from(Config::figment()); - assert_eq!(config, Config { - shutdown: Shutdown { ctrlc: false, ..Default::default() }, - ident: Ident::none(), - tls: Some(TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")), - limits: Limits::default() - .limit("forms", 1.mebibytes()) - .limit("json", 10.mebibytes()) - .limit("stream", 50.kibibytes()), - ..Config::default() - }); - - jail.create_file("Rocket.toml", r#" - [global.tls] - certs = "cert.pem" - key = "key.pem" - "#)?; - - let config = Config::from(Config::figment()); - assert_eq!(config, Config { - tls: Some(TlsConfig::from_paths( - jail.directory().join("cert.pem"), - jail.directory().join("key.pem") - )), - ..Config::default() - }); - - jail.create_file("Rocket.toml", r#" - [global.tls] - certs = "cert.pem" - key = "key.pem" - prefer_server_cipher_order = true - ciphers = [ - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - ] - "#)?; - - let config = Config::from(Config::figment()); - let cert_path = jail.directory().join("cert.pem"); - let key_path = jail.directory().join("key.pem"); - assert_eq!(config, Config { - tls: Some(TlsConfig::from_paths(cert_path, key_path) - .with_preferred_server_cipher_order(true) - .with_ciphers([ - CipherSuite::TLS_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_AES_256_GCM_SHA384, - CipherSuite::TLS_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - ])), - ..Config::default() - }); - - jail.create_file("Rocket.toml", r#" - [global] - shutdown.ctrlc = 0 - ident = false - - [global.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - - [global.limits] - forms = "1mib" - json = "10mib" - stream = "50kib" - "#)?; - - let config = Config::from(Config::figment()); - assert_eq!(config, Config { - shutdown: Shutdown { ctrlc: false, ..Default::default() }, - ident: Ident::none(), - tls: Some(TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")), - limits: Limits::default() - .limit("forms", 1.mebibytes()) - .limit("json", 10.mebibytes()) - .limit("stream", 50.kibibytes()), - ..Config::default() - }); - - jail.create_file("Rocket.toml", r#" - [global.tls] - certs = "cert.pem" - key = "key.pem" - "#)?; - - let config = Config::from(Config::figment()); - assert_eq!(config, Config { - tls: Some(TlsConfig::from_paths( - jail.directory().join("cert.pem"), - jail.directory().join("key.pem") - )), - ..Config::default() - }); - - jail.create_file("Rocket.toml", r#" - [global.tls] - certs = "cert.pem" - key = "key.pem" - prefer_server_cipher_order = true - ciphers = [ - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - ] - "#)?; - - let config = Config::from(Config::figment()); - let cert_path = jail.directory().join("cert.pem"); - let key_path = jail.directory().join("key.pem"); - assert_eq!(config, Config { - tls: Some(TlsConfig::from_paths(cert_path, key_path) - .with_preferred_server_cipher_order(true) - .with_ciphers([ - CipherSuite::TLS_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_AES_256_GCM_SHA384, - CipherSuite::TLS_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - ])), - ..Config::default() - }); - - Ok(()) - }); - } - - #[test] - #[cfg(feature = "mtls")] - fn test_mtls_config() { - use std::path::Path; - - figment::Jail::expect_with(|jail| { - jail.create_file("Rocket.toml", r#" - [default.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - "#)?; - - let config = Config::from(Config::figment()); - assert!(config.tls.is_some()); - assert!(config.tls.as_ref().unwrap().mutual.is_none()); - assert!(config.tls_enabled()); - assert!(!config.mtls_enabled()); - - jail.create_file("Rocket.toml", r#" - [default.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - mutual = { ca_certs = "/ssl/ca.pem" } - "#)?; - - let config = Config::from(Config::figment()); - assert!(config.tls_enabled()); - assert!(config.mtls_enabled()); - - let mtls = config.tls.as_ref().unwrap().mutual.as_ref().unwrap(); - assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem")); - assert!(!mtls.mandatory); - - jail.create_file("Rocket.toml", r#" - [default.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - - [default.tls.mutual] - ca_certs = "/ssl/ca.pem" - mandatory = true - "#)?; - - let config = Config::from(Config::figment()); - let mtls = config.tls.as_ref().unwrap().mutual.as_ref().unwrap(); - assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem")); - assert!(mtls.mandatory); - - jail.create_file("Rocket.toml", r#" - [default.tls] - certs = "/ssl/cert.pem" - key = "/ssl/key.pem" - mutual = { ca_certs = "relative/ca.pem" } - "#)?; - - let config = Config::from(Config::figment()); - let mtls = config.tls.as_ref().unwrap().mutual().unwrap(); - assert_eq!(mtls.ca_certs().unwrap_left(), - jail.directory().join("relative/ca.pem")); - - Ok(()) - }); - } - #[test] fn test_profiles_merge() { figment::Jail::expect_with(|jail| { @@ -629,42 +385,41 @@ mod tests { } #[test] - #[cfg(feature = "tls")] fn test_env_vars_merge() { - use crate::config::{TlsConfig, Ident}; + use crate::config::{Ident, Shutdown}; figment::Jail::expect_with(|jail| { - jail.set_env("ROCKET_PORT", 9999); + jail.set_env("ROCKET_KEEP_ALIVE", 9999); let config = Config::from(Config::figment()); assert_eq!(config, Config { - port: 9999, + keep_alive: 9999, ..Config::default() }); - jail.set_env("ROCKET_TLS", r#"{certs="certs.pem"}"#); + jail.set_env("ROCKET_SHUTDOWN", r#"{grace=7}"#); let first_figment = Config::figment(); - jail.set_env("ROCKET_TLS", r#"{key="key.pem"}"#); + jail.set_env("ROCKET_SHUTDOWN", r#"{mercy=10}"#); let prev_figment = Config::figment().join(&first_figment); let config = Config::from(&prev_figment); assert_eq!(config, Config { - port: 9999, - tls: Some(TlsConfig::from_paths("certs.pem", "key.pem")), + keep_alive: 9999, + shutdown: Shutdown { grace: 7, mercy: 10, ..Default::default() }, ..Config::default() }); - jail.set_env("ROCKET_TLS", r#"{certs="new.pem"}"#); + jail.set_env("ROCKET_SHUTDOWN", r#"{mercy=20}"#); let config = Config::from(Config::figment().join(&prev_figment)); assert_eq!(config, Config { - port: 9999, - tls: Some(TlsConfig::from_paths("new.pem", "key.pem")), + keep_alive: 9999, + shutdown: Shutdown { grace: 7, mercy: 20, ..Default::default() }, ..Config::default() }); jail.set_env("ROCKET_LIMITS", r#"{stream=100kiB}"#); let config = Config::from(Config::figment().join(&prev_figment)); assert_eq!(config, Config { - port: 9999, - tls: Some(TlsConfig::from_paths("new.pem", "key.pem")), + keep_alive: 9999, + shutdown: Shutdown { grace: 7, mercy: 20, ..Default::default() }, limits: Limits::default().limit("stream", 100.kibibytes()), ..Config::default() }); @@ -672,8 +427,8 @@ mod tests { jail.set_env("ROCKET_IDENT", false); let config = Config::from(Config::figment().join(&prev_figment)); assert_eq!(config, Config { - port: 9999, - tls: Some(TlsConfig::from_paths("new.pem", "key.pem")), + keep_alive: 9999, + shutdown: Shutdown { grace: 7, mercy: 20, ..Default::default() }, limits: Limits::default().limit("stream", 100.kibibytes()), ident: Ident::none(), ..Config::default() diff --git a/core/lib/src/config/secret_key.rs b/core/lib/src/config/secret_key.rs index 07d804a323..46818c4f51 100644 --- a/core/lib/src/config/secret_key.rs +++ b/core/lib/src/config/secret_key.rs @@ -1,8 +1,8 @@ use std::fmt; +use cookie::Key; use serde::{de, ser, Deserialize, Serialize}; -use crate::http::private::cookie::Key; use crate::request::{Outcome, Request, FromRequest}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/core/lib/src/config/shutdown.rs b/core/lib/src/config/shutdown.rs index e445ca2404..2353a4fbae 100644 --- a/core/lib/src/config/shutdown.rs +++ b/core/lib/src/config/shutdown.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, time::Duration}; #[cfg(unix)] use std::collections::HashSet; @@ -291,6 +291,14 @@ impl Default for Shutdown { } impl Shutdown { + pub(crate) fn grace(&self) -> Duration { + Duration::from_secs(self.grace as u64) + } + + pub(crate) fn mercy(&self) -> Duration { + Duration::from_secs(self.mercy as u64) + } + #[cfg(unix)] pub(crate) fn signal_stream(&self) -> Option> { use tokio_stream::{StreamExt, StreamMap, wrappers::SignalStream}; diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs index f30f046e3b..77d033284a 100644 --- a/core/lib/src/data/data_stream.rs +++ b/core/lib/src/data/data_stream.rs @@ -3,16 +3,16 @@ use std::task::{Context, Poll}; use std::path::Path; use std::io::{self, Cursor}; +use futures::ready; +use futures::stream::Stream; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, ReadBuf, Take}; use tokio_util::io::StreamReader; -use futures::{ready, stream::Stream}; +use hyper::body::{Body, Bytes, Incoming as HyperBody}; -use crate::http::hyper; -use crate::ext::{PollExt, Chain}; use crate::data::{Capped, N}; -use crate::http::hyper::body::Bytes; use crate::data::transform::Transform; +use crate::util::Chain; use super::peekable::Peekable; use super::transform::TransformBuf; @@ -68,7 +68,7 @@ pub type RawReader<'r> = StreamReader, Bytes>; /// Raw underlying data stream. pub enum RawStream<'r> { Empty, - Body(&'r mut hyper::Body), + Body(&'r mut HyperBody), Multipart(multer::Field<'r>), } @@ -154,8 +154,14 @@ impl<'r> DataStream<'r> { /// ``` pub fn hint(&self) -> usize { let base = self.base(); - let buf_len = base.get_ref().get_ref().0.get_ref().len(); - std::cmp::min(buf_len, base.limit() as usize) + if let (Some(cursor), _) = base.get_ref().get_ref() { + let len = cursor.get_ref().len() as u64; + let position = cursor.position().min(len); + let remaining = len - position; + remaining.min(base.limit()) as usize + } else { + 0 + } } /// A helper method to write the body of the request to any `AsyncWrite` @@ -331,17 +337,25 @@ impl Stream for RawStream<'_> { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - RawStream::Body(body) => Pin::new(body).poll_next(cx) - .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), - RawStream::Multipart(mp) => Pin::new(mp).poll_next(cx) - .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), + // TODO: Expose trailer headers, somehow. + RawStream::Body(body) => { + Pin::new(body) + .poll_frame(cx) + .map_ok(|frame| frame.into_data().unwrap_or_else(|_| Bytes::new())) + .map_err(io::Error::other) + } + RawStream::Multipart(s) => Pin::new(s).poll_next(cx).map_err(io::Error::other), RawStream::Empty => Poll::Ready(None), } } fn size_hint(&self) -> (usize, Option) { match self { - RawStream::Body(body) => body.size_hint(), + RawStream::Body(body) => { + let hint = body.size_hint(); + let (lower, upper) = (hint.lower(), hint.upper()); + (lower as usize, upper.map(|x| x as usize)) + }, RawStream::Multipart(mp) => mp.size_hint(), RawStream::Empty => (0, Some(0)), } @@ -358,8 +372,8 @@ impl std::fmt::Display for RawStream<'_> { } } -impl<'r> From<&'r mut hyper::Body> for RawStream<'r> { - fn from(value: &'r mut hyper::Body) -> Self { +impl<'r> From<&'r mut HyperBody> for RawStream<'r> { + fn from(value: &'r mut HyperBody) -> Self { Self::Body(value) } } diff --git a/core/lib/src/data/io_stream.rs b/core/lib/src/data/io_stream.rs index 0945c5c0f1..595138431a 100644 --- a/core/lib/src/data/io_stream.rs +++ b/core/lib/src/data/io_stream.rs @@ -3,8 +3,8 @@ use std::task::{Context, Poll}; use std::pin::Pin; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - -use crate::http::hyper::upgrade::Upgraded; +use hyper::upgrade::Upgraded; +use hyper_util::rt::TokioIo; /// A bidirectional, raw stream to the client. /// @@ -28,7 +28,7 @@ pub struct IoStream { /// Just in case we want to add stream kinds in the future. enum IoStreamKind { - Upgraded(Upgraded) + Upgraded(TokioIo) } /// An upgraded connection I/O handler. @@ -51,7 +51,7 @@ enum IoStreamKind { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { -/// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { +/// async fn io(self: Box, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -68,13 +68,20 @@ enum IoStreamKind { #[crate::async_trait] pub trait IoHandler: Send { /// Performs the raw I/O. - async fn io(self: Pin>, io: IoStream) -> io::Result<()>; + async fn io(self: Box, io: IoStream) -> io::Result<()>; +} + +#[crate::async_trait] +impl IoHandler for () { + async fn io(self: Box, _: IoStream) -> io::Result<()> { + Ok(()) + } } #[doc(hidden)] impl From for IoStream { fn from(io: Upgraded) -> Self { - IoStream { kind: IoStreamKind::Upgraded(io) } + IoStream { kind: IoStreamKind::Upgraded(TokioIo::new(io)) } } } diff --git a/core/lib/src/data/transform.rs b/core/lib/src/data/transform.rs index e3be992c76..f52478e6c1 100644 --- a/core/lib/src/data/transform.rs +++ b/core/lib/src/data/transform.rs @@ -178,7 +178,7 @@ impl<'a, 'b> DerefMut for TransformBuf<'a, 'b> { #[allow(deprecated)] mod tests { use std::hash::SipHasher; - use std::sync::{Arc, atomic::{AtomicU64, AtomicU8}}; + use std::sync::{Arc, atomic::{AtomicU8, AtomicU64, Ordering}}; use parking_lot::Mutex; use ubyte::ToByteUnit; @@ -264,41 +264,41 @@ mod tests { assert_eq!(bytes.len(), 8); let bytes: [u8; 8] = bytes.try_into().expect("[u8; 8]"); let value = u64::from_be_bytes(bytes); - hash1.store(value, atomic::Ordering::Release); + hash1.store(value, Ordering::Release); }) .chain_inspect(move |bytes| { assert_eq!(bytes.len(), 8); let bytes: [u8; 8] = bytes.try_into().expect("[u8; 8]"); let value = u64::from_be_bytes(bytes); - let prev = hash2.load(atomic::Ordering::Acquire); + let prev = hash2.load(Ordering::Acquire); assert_eq!(prev, value); - inspect2.fetch_add(1, atomic::Ordering::Release); + inspect2.fetch_add(1, Ordering::Release); }); }))); // Make sure nothing has happened yet. assert!(raw_data.lock().is_empty()); - assert_eq!(hash.load(atomic::Ordering::Acquire), 0); - assert_eq!(inspect2.load(atomic::Ordering::Acquire), 0); + assert_eq!(hash.load(Ordering::Acquire), 0); + assert_eq!(inspect2.load(Ordering::Acquire), 0); // Check that nothing happens if the data isn't read. let client = Client::debug(rocket).unwrap(); client.get("/").body("Hello, world!").dispatch(); assert!(raw_data.lock().is_empty()); - assert_eq!(hash.load(atomic::Ordering::Acquire), 0); - assert_eq!(inspect2.load(atomic::Ordering::Acquire), 0); + assert_eq!(hash.load(Ordering::Acquire), 0); + assert_eq!(inspect2.load(Ordering::Acquire), 0); // Check inspect + hash + inspect + inspect. client.post("/").body("Hello, world!").dispatch(); assert_eq!(raw_data.lock().as_slice(), "Hello, world!".as_bytes()); - assert_eq!(hash.load(atomic::Ordering::Acquire), 0xae5020d7cf49d14f); - assert_eq!(inspect2.load(atomic::Ordering::Acquire), 1); + assert_eq!(hash.load(Ordering::Acquire), 0xae5020d7cf49d14f); + assert_eq!(inspect2.load(Ordering::Acquire), 1); // Check inspect + hash + inspect + inspect, round 2. let string = "Rocket, Rocket, where art thee? Oh, tis in the sky, I see!"; client.post("/").body(string).dispatch(); assert_eq!(raw_data.lock().as_slice(), string.as_bytes()); - assert_eq!(hash.load(atomic::Ordering::Acquire), 0x323f9aa98f907faf); - assert_eq!(inspect2.load(atomic::Ordering::Acquire), 2); + assert_eq!(hash.load(Ordering::Acquire), 0x323f9aa98f907faf); + assert_eq!(inspect2.load(Ordering::Acquire), 2); } } diff --git a/core/lib/src/erased.rs b/core/lib/src/erased.rs new file mode 100644 index 0000000000..7b62522c55 --- /dev/null +++ b/core/lib/src/erased.rs @@ -0,0 +1,193 @@ +use std::io; +use std::mem::transmute; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Poll, Context}; + +use futures::future::BoxFuture; +use http::request::Parts; +use hyper::body::Incoming; +use tokio::io::{AsyncRead, ReadBuf}; + +use crate::data::{Data, IoHandler}; +use crate::{Request, Response, Rocket, Orbit}; + +// TODO: Magic with trait async fn to get rid of the box pin. +// TODO: Write safety proofs. + +macro_rules! static_assert_covariance { + ($T:tt) => ( + const _: () = { + fn _assert_covariance<'x: 'y, 'y>(x: &'y $T<'x>) -> &'y $T<'y> { x } + }; + ) +} + +#[derive(Debug)] +pub struct ErasedRequest { + // XXX: SAFETY: This (dependent) field must come first due to drop order! + request: Request<'static>, + _rocket: Arc>, + _parts: Box, +} + +impl Drop for ErasedRequest { + fn drop(&mut self) { } +} + +#[derive(Debug)] +pub struct ErasedResponse { + // XXX: SAFETY: This (dependent) field must come first due to drop order! + response: Response<'static>, + _request: Arc, + _incoming: Box, +} + +impl Drop for ErasedResponse { + fn drop(&mut self) { } +} + +pub struct ErasedIoHandler { + // XXX: SAFETY: This (dependent) field must come first due to drop order! + io: Box, + _request: Arc, +} + +impl Drop for ErasedIoHandler { + fn drop(&mut self) { } +} + +impl ErasedRequest { + pub fn new( + rocket: Arc>, + parts: Parts, + constructor: impl for<'r> FnOnce( + &'r Rocket, + &'r Parts + ) -> Request<'r>, + ) -> ErasedRequest { + let rocket: Arc> = rocket; + let parts: Box = Box::new(parts); + let request: Request<'_> = { + let rocket: &Rocket = &*rocket; + let rocket: &'static Rocket = unsafe { transmute(rocket) }; + let parts: &Parts = &*parts; + let parts: &'static Parts = unsafe { transmute(parts) }; + constructor(&rocket, &parts) + }; + + ErasedRequest { _rocket: rocket, _parts: parts, request, } + } + + pub async fn into_response( + self, + incoming: Incoming, + data_builder: impl for<'r> FnOnce(&'r mut Incoming) -> Data<'r>, + preprocess: impl for<'r, 'x> FnOnce( + &'r Rocket, + &'r mut Request<'x>, + &'r mut Data<'x> + ) -> BoxFuture<'r, T>, + dispatch: impl for<'r> FnOnce( + T, + &'r Rocket, + &'r Request<'r>, + Data<'r> + ) -> BoxFuture<'r, Response<'r>>, + ) -> ErasedResponse { + let mut incoming = Box::new(incoming); + let mut data: Data<'_> = { + let incoming: &mut Incoming = &mut *incoming; + let incoming: &'static mut Incoming = unsafe { transmute(incoming) }; + data_builder(incoming) + }; + + let mut parent = Arc::new(self); + let token: T = { + let parent: &mut ErasedRequest = Arc::get_mut(&mut parent).unwrap(); + let rocket: &Rocket = &*parent._rocket; + let request: &mut Request<'_> = &mut parent.request; + let data: &mut Data<'_> = &mut data; + preprocess(rocket, request, data).await + }; + + let parent = parent; + let response: Response<'_> = { + let parent: &ErasedRequest = &*parent; + let parent: &'static ErasedRequest = unsafe { transmute(parent) }; + let rocket: &Rocket = &*parent._rocket; + let request: &Request<'_> = &parent.request; + dispatch(token, rocket, request, data).await + }; + + ErasedResponse { + _request: parent, + _incoming: incoming, + response: response, + } + } +} + +impl ErasedResponse { + pub fn inner<'a>(&'a self) -> &'a Response<'a> { + static_assert_covariance!(Response); + &self.response + } + + pub fn with_inner_mut<'a, T>( + &'a mut self, + f: impl for<'r> FnOnce(&'a mut Response<'r>) -> T + ) -> T { + static_assert_covariance!(Response); + f(&mut self.response) + } + + pub fn to_io_handler<'a>( + &'a mut self, + constructor: impl for<'r> FnOnce( + &'r Request<'r>, + &'a mut Response<'r>, + ) -> Option> + ) -> Option { + let parent: Arc = self._request.clone(); + let io: Option> = { + let parent: &ErasedRequest = &*parent; + let parent: &'static ErasedRequest = unsafe { transmute(parent) }; + let request: &Request<'_> = &parent.request; + constructor(request, &mut self.response) + }; + + io.map(|io| ErasedIoHandler { _request: parent, io }) + } +} + +impl ErasedIoHandler { + pub fn with_inner_mut<'a, T: 'a>( + &'a mut self, + f: impl for<'r> FnOnce(&'a mut Box) -> T + ) -> T { + fn _assert_covariance<'x: 'y, 'y>( + x: &'y Box + ) -> &'y Box { x } + + f(&mut self.io) + } + + pub fn take<'a>(&'a mut self) -> Box { + fn _assert_covariance<'x: 'y, 'y>( + x: &'y Box + ) -> &'y Box { x } + + self.with_inner_mut(|handler| std::mem::replace(handler, Box::new(()))) + } +} + +impl AsyncRead for ErasedResponse { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.get_mut().with_inner_mut(|r| Pin::new(r.body_mut()).poll_read(cx, buf)) + } +} diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index ff3ef79f61..21753b1f1b 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -74,11 +74,8 @@ pub struct Error { #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind { - /// Binding to the provided address/port failed. - Bind(io::Error), - /// Binding via TLS to the provided address/port failed. - #[cfg(feature = "tls")] - TlsBind(crate::http::tls::error::Error), + /// Binding to the network interface failed. + Bind(Box), /// An I/O error occurred during launch. Io(io::Error), /// A valid [`Config`](crate::Config) could not be extracted from the @@ -90,15 +87,10 @@ pub enum ErrorKind { FailedFairings(Vec), /// Sentinels requested abort. SentinelAborts(Vec), - /// The configuration profile is not debug but not secret key is configured. + /// The configuration profile is not debug but no secret key is configured. InsecureSecretKey(Profile), - /// Shutdown failed. - Shutdown( - /// The instance of Rocket that failed to shutdown. - Arc>, - /// The error that occurred during shutdown, if any. - Option> - ), + /// Shutdown failed. Contains the Rocket instance that failed to shutdown. + Shutdown(Arc>), } /// An error that occurs when a value was unexpectedly empty. @@ -111,20 +103,24 @@ impl From for Error { } } +impl From for Error { + fn from(e: figment::Error) -> Self { + Error::new(ErrorKind::Config(e)) + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::new(ErrorKind::Io(e)) + } +} + impl Error { #[inline(always)] pub(crate) fn new(kind: ErrorKind) -> Error { Error { handled: AtomicBool::new(false), kind } } - #[inline(always)] - pub(crate) fn shutdown(rocket: Arc>, error: E) -> Error - where E: Into> - { - let error = error.into().map(|e| Box::new(e) as Box); - Error::new(ErrorKind::Shutdown(rocket, error)) - } - #[inline(always)] fn was_handled(&self) -> bool { self.handled.load(Ordering::Acquire) @@ -176,9 +172,9 @@ impl Error { self.mark_handled(); match self.kind() { ErrorKind::Bind(ref e) => { - error!("Rocket failed to bind network socket to given address/port."); + error!("Binding to the network interface failed."); info_!("{}", e); - "aborting due to socket bind error" + "aborting due to bind error" } ErrorKind::Io(ref e) => { error!("Rocket failed to launch due to an I/O error."); @@ -229,20 +225,10 @@ impl Error { "aborting due to sentinel-triggered abort(s)" } - ErrorKind::Shutdown(_, error) => { + ErrorKind::Shutdown(_) => { error!("Rocket failed to shutdown gracefully."); - if let Some(e) = error { - info_!("{}", e); - } - "aborting due to failed shutdown" } - #[cfg(feature = "tls")] - ErrorKind::TlsBind(e) => { - error!("Rocket failed to bind via TLS to network socket."); - info_!("{}", e); - "aborting due to TLS bind error" - } } } } @@ -260,10 +246,7 @@ impl fmt::Display for ErrorKind { ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f), ErrorKind::Config(_) => "failed to extract configuration".fmt(f), ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f), - ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {e}"), - ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f), - #[cfg(feature = "tls")] - ErrorKind::TlsBind(e) => write!(f, "TLS bind failed: {e}"), + ErrorKind::Shutdown(_) => "shutdown failed".fmt(f), } } } @@ -308,3 +291,42 @@ impl fmt::Display for Empty { } impl StdError for Empty { } + +/// Log an error that occurs during request processing +pub(crate) fn log_server_error(error: &Box) { + struct ServerError<'a>(&'a (dyn StdError + 'static)); + + impl fmt::Display for ServerError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let error = &self.0; + if let Some(e) = error.downcast_ref::() { + write!(f, "request processing failed: {e}")?; + } else if let Some(e) = error.downcast_ref::() { + write!(f, "connection I/O error: ")?; + + match e.kind() { + io::ErrorKind::NotConnected => write!(f, "remote disconnected")?, + io::ErrorKind::UnexpectedEof => write!(f, "remote sent early eof")?, + io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::BrokenPipe => write!(f, "terminated by remote")?, + _ => write!(f, "{e}")?, + } + } else { + write!(f, "http server error: {error}")?; + } + + if let Some(e) = error.source() { + write!(f, " ({})", ServerError(e))?; + } + + Ok(()) + } + } + + if error.downcast_ref::().is_some() { + warn!("{}", ServerError(&**error)) + } else { + error!("{}", ServerError(&**error)) + } +} diff --git a/core/lib/src/ext.rs b/core/lib/src/ext.rs deleted file mode 100644 index 03922184df..0000000000 --- a/core/lib/src/ext.rs +++ /dev/null @@ -1,404 +0,0 @@ -use std::{io, time::Duration}; -use std::task::{Poll, Context}; -use std::pin::Pin; - -use bytes::{Bytes, BytesMut}; -use pin_project_lite::pin_project; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tokio::time::{sleep, Sleep}; - -use futures::stream::Stream; -use futures::future::{self, Future, FutureExt}; - -pin_project! { - pub struct ReaderStream { - #[pin] - reader: Option, - buf: BytesMut, - cap: usize, - } -} - -impl Stream for ReaderStream { - type Item = std::io::Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - use tokio_util::io::poll_read_buf; - - let mut this = self.as_mut().project(); - - let reader = match this.reader.as_pin_mut() { - Some(r) => r, - None => return Poll::Ready(None), - }; - - if this.buf.capacity() == 0 { - this.buf.reserve(*this.cap); - } - - match poll_read_buf(reader, cx, &mut this.buf) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => { - self.project().reader.set(None); - Poll::Ready(Some(Err(err))) - } - Poll::Ready(Ok(0)) => { - self.project().reader.set(None); - Poll::Ready(None) - } - Poll::Ready(Ok(_)) => { - let chunk = this.buf.split(); - Poll::Ready(Some(Ok(chunk.freeze()))) - } - } - } -} - -pub trait AsyncReadExt: AsyncRead + Sized { - fn into_bytes_stream(self, cap: usize) -> ReaderStream { - ReaderStream { reader: Some(self), cap, buf: BytesMut::with_capacity(cap) } - } -} - -impl AsyncReadExt for T { } - -pub trait PollExt { - fn map_err_ext(self, f: F) -> Poll>> - where F: FnOnce(E) -> U; -} - -impl PollExt for Poll>> { - /// Changes the error value of this `Poll` with the closure provided. - fn map_err_ext(self, f: F) -> Poll>> - where F: FnOnce(E) -> U - { - match self { - Poll::Ready(Some(Ok(t))) => Poll::Ready(Some(Ok(t))), - Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(f(e)))), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -pin_project! { - /// Stream for the [`chain`](super::AsyncReadExt::chain) method. - #[must_use = "streams do nothing unless polled"] - pub struct Chain { - #[pin] - first: T, - #[pin] - second: U, - done_first: bool, - } -} - -impl Chain { - pub(crate) fn new(first: T, second: U) -> Self { - Self { first, second, done_first: false } - } -} - -impl Chain { - /// Gets references to the underlying readers in this `Chain`. - pub fn get_ref(&self) -> (&T, &U) { - (&self.first, &self.second) - } -} - -impl AsyncRead for Chain { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let me = self.project(); - - if !*me.done_first { - let init_rem = buf.remaining(); - futures::ready!(me.first.poll_read(cx, buf))?; - if buf.remaining() == init_rem { - *me.done_first = true; - } else { - return Poll::Ready(Ok(())); - } - } - me.second.poll_read(cx, buf) - } -} - -enum State { - /// I/O has not been cancelled. Proceed as normal. - Active, - /// I/O has been cancelled. See if we can finish before the timer expires. - Grace(Pin>), - /// Grace period elapsed. Shutdown the connection, waiting for the timer - /// until we force close. - Mercy(Pin>), -} - -pin_project! { - /// I/O that can be cancelled when a future `F` resolves. - #[must_use = "futures do nothing unless polled"] - pub struct CancellableIo { - #[pin] - io: Option, - #[pin] - trigger: future::Fuse, - state: State, - grace: Duration, - mercy: Duration, - } -} - -impl CancellableIo { - pub fn new(trigger: F, io: I, grace: Duration, mercy: Duration) -> Self { - CancellableIo { - grace, mercy, - io: Some(io), - trigger: trigger.fuse(), - state: State::Active, - } - } - - pub fn io(&self) -> Option<&I> { - self.io.as_ref() - } - - /// Run `do_io` while connection processing should continue. - fn poll_trigger_then( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - do_io: impl FnOnce(Pin<&mut I>, &mut Context<'_>) -> Poll>, - ) -> Poll> { - let mut me = self.as_mut().project(); - let io = match me.io.as_pin_mut() { - Some(io) => io, - None => return Poll::Ready(Err(gone())), - }; - - loop { - match me.state { - State::Active => { - if me.trigger.as_mut().poll(cx).is_ready() { - *me.state = State::Grace(Box::pin(sleep(*me.grace))); - } else { - return do_io(io, cx); - } - } - State::Grace(timer) => { - if timer.as_mut().poll(cx).is_ready() { - *me.state = State::Mercy(Box::pin(sleep(*me.mercy))); - } else { - return do_io(io, cx); - } - } - State::Mercy(timer) => { - if timer.as_mut().poll(cx).is_ready() { - self.project().io.set(None); - return Poll::Ready(Err(time_out())); - } else { - let result = futures::ready!(io.poll_shutdown(cx)); - self.project().io.set(None); - return match result { - Err(e) => Poll::Ready(Err(e)), - Ok(()) => Poll::Ready(Err(gone())) - }; - } - }, - } - } - } -} - -fn time_out() -> io::Error { - io::Error::new(io::ErrorKind::TimedOut, "Shutdown grace timed out") -} - -fn gone() -> io::Error { - io::Error::new(io::ErrorKind::BrokenPipe, "IO driver has terminated") -} - -impl AsyncRead for CancellableIo { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_read(cx, buf)) - } -} - -impl AsyncWrite for CancellableIo { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_write(cx, buf)) - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_flush(cx)) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_shutdown(cx)) - } - - fn poll_write_vectored( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[io::IoSlice<'_>], - ) -> Poll> { - self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_write_vectored(cx, bufs)) - } - - fn is_write_vectored(&self) -> bool { - self.io().map(|io| io.is_write_vectored()).unwrap_or(false) - } -} - -use crate::http::private::{Listener, Connection, Certificates}; - -impl Connection for CancellableIo { - fn peer_address(&self) -> Option { - self.io().and_then(|io| io.peer_address()) - } - - fn peer_certificates(&self) -> Option { - self.io().and_then(|io| io.peer_certificates()) - } - - fn enable_nodelay(&self) -> io::Result<()> { - match self.io() { - Some(io) => io.enable_nodelay(), - None => Ok(()) - } - } -} - -pin_project! { - pub struct CancellableListener { - pub trigger: F, - #[pin] - pub listener: L, - pub grace: Duration, - pub mercy: Duration, - } -} - -impl CancellableListener { - pub fn new(trigger: F, listener: L, grace: u64, mercy: u64) -> Self { - let (grace, mercy) = (Duration::from_secs(grace), Duration::from_secs(mercy)); - CancellableListener { trigger, listener, grace, mercy } - } -} - -impl Listener for CancellableListener { - type Connection = CancellableIo; - - fn local_addr(&self) -> Option { - self.listener.local_addr() - } - - fn poll_accept( - mut self: Pin<&mut Self>, - cx: &mut Context<'_> - ) -> Poll> { - self.as_mut().project().listener - .poll_accept(cx) - .map(|res| res.map(|conn| { - CancellableIo::new(self.trigger.clone(), conn, self.grace, self.mercy) - })) - } -} - -pub trait StreamExt: Sized + Stream { - fn join(self, other: U) -> Join - where U: Stream; -} - -impl StreamExt for S { - fn join(self, other: U) -> Join - where U: Stream - { - Join::new(self, other) - } -} - -pin_project! { - /// Stream returned by the [`join`](super::StreamExt::join) method. - pub struct Join { - #[pin] - a: T, - #[pin] - b: U, - // When `true`, poll `a` first, otherwise, `poll` b`. - toggle: bool, - // Set when either `a` or `b` return `None`. - done: bool, - } -} - -impl Join { - pub(super) fn new(a: T, b: U) -> Join - where T: Stream, U: Stream, - { - Join { a, b, toggle: false, done: false, } - } - - fn poll_next>( - first: Pin<&mut A>, - second: Pin<&mut B>, - done: &mut bool, - cx: &mut Context<'_>, - ) -> Poll> { - match first.poll_next(cx) { - Poll::Ready(opt) => { *done = opt.is_none(); Poll::Ready(opt) } - Poll::Pending => match second.poll_next(cx) { - Poll::Ready(opt) => { *done = opt.is_none(); Poll::Ready(opt) } - Poll::Pending => Poll::Pending - } - } - } -} - -impl Stream for Join - where T: Stream, - U: Stream, -{ - type Item = T::Item; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - if self.done { - return Poll::Ready(None); - } - - let me = self.project(); - *me.toggle = !*me.toggle; - match *me.toggle { - true => Self::poll_next(me.a, me.b, me.done, cx), - false => Self::poll_next(me.b, me.a, me.done, cx), - } - } - - fn size_hint(&self) -> (usize, Option) { - let (left_low, left_high) = self.a.size_hint(); - let (right_low, right_high) = self.b.size_hint(); - - let low = left_low.saturating_add(right_low); - let high = match (left_high, right_high) { - (Some(h1), Some(h2)) => h1.checked_add(h2), - _ => None, - }; - - (low, high) - } -} diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index aa772a39a4..915a64f1b1 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -341,6 +341,7 @@ // `key_contexts: Vec`, a vector of `value_contexts: // Vec`, a `mapping` from a string index to an integer index // into the `contexts`, and a vector of `errors`. +// // 2. **Push.** An index is required; an error is emitted and `push` returns // if they field's first key does not contain an index. If the first key // contains _one_ index, a new `K::Context` and `V::Context` are created. @@ -356,9 +357,9 @@ // to `second` in `mapping`. If the first index is `k`, the field, // stripped of the first key, is pushed to the key's context; the same is // done for the value's context is the first index is `v`. +// // 3. **Finalization.** Every context is finalized; errors and `Ok` values -// are collected. TODO: FINISH. Split this into two: one for single-index, -// another for two-indices. +// are collected. mod field; mod options; diff --git a/core/lib/src/fs/named_file.rs b/core/lib/src/fs/named_file.rs index 8fd165f209..d4eed82a92 100644 --- a/core/lib/src/fs/named_file.rs +++ b/core/lib/src/fs/named_file.rs @@ -2,7 +2,7 @@ use std::io; use std::path::{Path, PathBuf}; use std::ops::{Deref, DerefMut}; -use tokio::fs::File; +use tokio::fs::{File, OpenOptions}; use crate::request::Request; use crate::response::{self, Responder}; @@ -60,7 +60,7 @@ impl NamedFile { /// } /// ``` pub async fn open>(path: P) -> io::Result { - // FIXME: Grab the file size here and prohibit `seek`ing later (or else + // TODO: Grab the file size here and prohibit `seek`ing later (or else // the file's effective size may change), to save on the cost of doing // all of those `seek`s to determine the file size. But, what happens if // the file gets changed between now and then? @@ -68,6 +68,11 @@ impl NamedFile { Ok(NamedFile(path.as_ref().to_path_buf(), file)) } + pub async fn open_with>(path: P, opts: &OpenOptions) -> io::Result { + let file = opts.open(path.as_ref()).await?; + Ok(NamedFile(path.as_ref().to_path_buf(), file)) + } + /// Retrieve the underlying `File`. /// /// # Example diff --git a/core/lib/src/cookies.rs b/core/lib/src/http/cookies.rs similarity index 97% rename from core/lib/src/cookies.rs rename to core/lib/src/http/cookies.rs index b64441f03d..1a17949ab9 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/http/cookies.rs @@ -2,11 +2,10 @@ use std::fmt; use parking_lot::Mutex; -use crate::http::private::cookie; use crate::{Rocket, Orbit}; #[doc(inline)] -pub use self::cookie::{Cookie, SameSite, Iter}; +pub use cookie::{Cookie, SameSite, Iter}; /// Collection of one or more HTTP cookies. /// @@ -167,7 +166,7 @@ pub(crate) struct CookieState<'a> { #[derive(Clone)] enum Op { Add(Cookie<'static>, bool), - Remove(Cookie<'static>, bool), + Remove(Cookie<'static>), } impl<'a> CookieJar<'a> { @@ -177,7 +176,7 @@ impl<'a> CookieJar<'a> { ops: Mutex::new(Vec::new()), state: CookieState { // This is updated dynamically when headers are received. - secure: rocket.config().tls_enabled(), + secure: rocket.endpoint().is_tls(), config: rocket.config(), } } @@ -256,7 +255,7 @@ impl<'a> CookieJar<'a> { for op in ops.iter().rev().filter(|op| op.cookie().name() == name) { match op { Op::Add(c, _) => return Some(c.clone()), - Op::Remove(_, _) => return None, + Op::Remove(_) => return None, } } @@ -389,7 +388,7 @@ impl<'a> CookieJar<'a> { pub fn remove>>(&self, cookie: C) { let mut cookie = cookie.into(); Self::set_removal_defaults(&mut cookie); - self.ops.lock().push(Op::Remove(cookie, false)); + self.ops.lock().push(Op::Remove(cookie)); } /// Removes the private `cookie` from the collection. @@ -432,7 +431,7 @@ impl<'a> CookieJar<'a> { pub fn remove_private>>(&self, cookie: C) { let mut cookie = cookie.into(); Self::set_removal_defaults(&mut cookie); - self.ops.lock().push(Op::Remove(cookie, true)); + self.ops.lock().push(Op::Remove(cookie)); } /// Returns an iterator over all of the _original_ cookies present in this @@ -477,7 +476,7 @@ impl<'a> CookieJar<'a> { Op::Add(c, true) => { jar.private_mut(&self.state.config.secret_key.key).add(c); } - Op::Remove(mut c, _) => { + Op::Remove(mut c) => { if self.jar.get(c.name()).is_some() { c.make_removal(); jar.add(c); @@ -595,7 +594,7 @@ impl<'a> Clone for CookieJar<'a> { impl Op { fn cookie(&self) -> &Cookie<'static> { match self { - Op::Add(c, _) | Op::Remove(c, _) => c + Op::Add(c, _) | Op::Remove(c) => c } } } diff --git a/core/lib/src/http/mod.rs b/core/lib/src/http/mod.rs new file mode 100644 index 0000000000..ac38395c1b --- /dev/null +++ b/core/lib/src/http/mod.rs @@ -0,0 +1,12 @@ +//! Types that map to concepts in HTTP. +//! +//! This module exports types that map to HTTP concepts or to the underlying +//! HTTP library when needed. + +mod cookies; + +#[doc(inline)] +pub use rocket_http::*; + +#[doc(inline)] +pub use cookies::*; diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 232a981482..5ffee01a8f 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -7,7 +7,9 @@ #![cfg_attr(nightly, feature(decl_macro))] #![warn(rust_2018_idioms)] -#![warn(missing_docs)] +// #![warn(missing_docs)] +#![allow(async_fn_in_trait)] +#![allow(refining_impl_trait)] //! # Rocket - Core API Documentation //! @@ -109,18 +111,24 @@ /// These are public dependencies! Update docs if these are changed, especially /// figment's version number in docs. -#[doc(hidden)] pub use yansi; -#[doc(hidden)] pub use async_stream; +#[doc(hidden)] +pub use yansi; +#[doc(hidden)] +pub use async_stream; pub use futures; pub use tokio; pub use figment; pub use time; #[doc(hidden)] -#[macro_use] pub mod log; -#[macro_use] pub mod outcome; -#[macro_use] pub mod data; -#[doc(hidden)] pub mod sentinel; +#[macro_use] +pub mod log; +#[macro_use] +pub mod outcome; +#[macro_use] +pub mod data; +#[doc(hidden)] +pub mod sentinel; pub mod local; pub mod request; pub mod response; @@ -133,74 +141,41 @@ pub mod route; pub mod serde; pub mod shield; pub mod fs; - -// Reexport of HTTP everything. -pub mod http { - //! Types that map to concepts in HTTP. - //! - //! This module exports types that map to HTTP concepts or to the underlying - //! HTTP library when needed. - - #[doc(inline)] - pub use rocket_http::*; - - /// Re-exported hyper HTTP library types. - /// - /// All types that are re-exported from Hyper reside inside of this module. - /// These types will, with certainty, be removed with time, but they reside here - /// while necessary. - pub mod hyper { - #[doc(hidden)] - pub use rocket_http::hyper::*; - - pub use rocket_http::hyper::header; - } - - #[doc(inline)] - pub use crate::cookies::*; -} - +pub mod http; +pub mod listener; +#[cfg(feature = "tls")] +#[cfg_attr(nightly, doc(cfg(feature = "tls")))] +pub mod tls; #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] pub mod mtls; -/// TODO: We need a futures mod or something. -mod trip_wire; +mod util; mod shutdown; mod server; -mod ext; +mod lifecycle; mod state; -mod cookies; mod rocket; mod router; mod phase; +mod erased; + +#[doc(hidden)] pub use either::Either; + +#[doc(inline)] pub use rocket_codegen::*; #[doc(inline)] pub use crate::response::Response; #[doc(inline)] pub use crate::data::Data; #[doc(inline)] pub use crate::config::Config; #[doc(inline)] pub use crate::catcher::Catcher; #[doc(inline)] pub use crate::route::Route; -#[doc(hidden)] pub use either::Either; -#[doc(inline)] pub use phase::{Phase, Build, Ignite, Orbit}; -#[doc(inline)] pub use error::Error; -#[doc(inline)] pub use sentinel::Sentinel; +#[doc(inline)] pub use crate::phase::{Phase, Build, Ignite, Orbit}; +#[doc(inline)] pub use crate::error::Error; +#[doc(inline)] pub use crate::sentinel::Sentinel; #[doc(inline)] pub use crate::request::Request; #[doc(inline)] pub use crate::rocket::Rocket; #[doc(inline)] pub use crate::shutdown::Shutdown; #[doc(inline)] pub use crate::state::State; -#[doc(inline)] pub use rocket_codegen::*; - -/// Creates a [`Rocket`] instance with the default config provider: aliases -/// [`Rocket::build()`]. -pub fn build() -> Rocket { - Rocket::build() -} - -/// Creates a [`Rocket`] instance with a custom config provider: aliases -/// [`Rocket::custom()`]. -pub fn custom(provider: T) -> Rocket { - Rocket::custom(provider) -} /// Retrofits support for `async fn` in trait impls and declarations. /// @@ -231,6 +206,20 @@ pub fn custom(provider: T) -> Rocket { #[doc(inline)] pub use async_trait::async_trait; +const WORKER_PREFIX: &'static str = "rocket-worker"; + +/// Creates a [`Rocket`] instance with the default config provider: aliases +/// [`Rocket::build()`]. +pub fn build() -> Rocket { + Rocket::build() +} + +/// Creates a [`Rocket`] instance with a custom config provider: aliases +/// [`Rocket::custom()`]. +pub fn custom(provider: T) -> Rocket { + Rocket::custom(provider) +} + /// WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] pub fn async_run(fut: F, workers: usize, sync: usize, force_end: bool, name: &str) -> R @@ -255,7 +244,7 @@ pub fn async_run(fut: F, workers: usize, sync: usize, force_end: bool, nam /// WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] pub fn async_test(fut: impl std::future::Future) -> R { - async_run(fut, 1, 32, true, "rocket-worker-test-thread") + async_run(fut, 1, 32, true, &format!("{WORKER_PREFIX}-test-thread")) } /// WARNING: This is unstable! Do not use this method outside of Rocket! @@ -276,7 +265,7 @@ pub fn async_main(fut: impl std::future::Future + Send) -> R { let workers = fig.extract_inner(Config::WORKERS).unwrap_or_else(bail); let max_blocking = fig.extract_inner(Config::MAX_BLOCKING).unwrap_or_else(bail); let force = fig.focus(Config::SHUTDOWN).extract_inner("force").unwrap_or_else(bail); - async_run(fut, workers, max_blocking, force, "rocket-worker-thread") + async_run(fut, workers, max_blocking, force, &format!("{WORKER_PREFIX}-thread")) } /// Executes a `future` to completion on a new tokio-based Rocket async runtime. @@ -359,3 +348,14 @@ pub fn execute(future: F) -> R { async_main(future) } + +/// Returns a future that evalutes to `true` exactly when there is a presently +/// running tokio async runtime that was likely started by Rocket. +fn running_within_rocket_async_rt() -> impl std::future::Future { + use futures::FutureExt; + + tokio::task::spawn_blocking(|| { + let this = std::thread::current(); + this.name().map_or(false, |s| s.starts_with(WORKER_PREFIX)) + }).map(|r| r.unwrap_or(false)) +} diff --git a/core/lib/src/lifecycle.rs b/core/lib/src/lifecycle.rs new file mode 100644 index 0000000000..1759af5c32 --- /dev/null +++ b/core/lib/src/lifecycle.rs @@ -0,0 +1,272 @@ +use yansi::Paint; +use futures::future::{FutureExt, Future}; + +use crate::{route, Rocket, Orbit, Request, Response, Data}; +use crate::data::IoHandler; +use crate::http::{Method, Status, Header}; +use crate::outcome::Outcome; +use crate::form::Form; + +// A token returned to force the execution of one method before another. +pub(crate) struct RequestToken; + +async fn catch_handle(name: Option<&str>, run: F) -> Option + where F: FnOnce() -> Fut, Fut: Future, +{ + macro_rules! panic_info { + ($name:expr, $e:expr) => {{ + match $name { + Some(name) => error_!("Handler {} panicked.", name.primary()), + None => error_!("A handler panicked.") + }; + + info_!("This is an application bug."); + info_!("A panic in Rust must be treated as an exceptional event."); + info_!("Panicking is not a suitable error handling mechanism."); + info_!("Unwinding, the result of a panic, is an expensive operation."); + info_!("Panics will degrade application performance."); + info_!("Instead of panicking, return `Option` and/or `Result`."); + info_!("Values of either type can be returned directly from handlers."); + warn_!("A panic is treated as an internal server error."); + $e + }} + } + + let run = std::panic::AssertUnwindSafe(run); + let fut = std::panic::catch_unwind(move || run()) + .map_err(|e| panic_info!(name, e)) + .ok()?; + + std::panic::AssertUnwindSafe(fut) + .catch_unwind() + .await + .map_err(|e| panic_info!(name, e)) + .ok() +} + +impl Rocket { + /// Preprocess the request for Rocket things. Currently, this means: + /// + /// * Rewriting the method in the request if _method form field exists. + /// * Run the request fairings. + /// + /// This is the only place during lifecycle processing that `Request` is + /// mutable. Keep this in-sync with the `FromForm` derive. + pub(crate) async fn preprocess( + &self, + req: &mut Request<'_>, + data: &mut Data<'_> + ) -> RequestToken { + // Check if this is a form and if the form contains the special _method + // field which we use to reinterpret the request's method. + let (min_len, max_len) = ("_method=get".len(), "_method=delete".len()); + let peek_buffer = data.peek(max_len).await; + let is_form = req.content_type().map_or(false, |ct| ct.is_form()); + + if is_form && req.method() == Method::Post && peek_buffer.len() >= min_len { + let method = std::str::from_utf8(peek_buffer).ok() + .and_then(|raw_form| Form::values(raw_form).next()) + .filter(|field| field.name == "_method") + .and_then(|field| field.value.parse().ok()); + + if let Some(method) = method { + req.set_method(method); + } + } + + // Run request fairings. + self.fairings.handle_request(req, data).await; + + RequestToken + } + + /// Dispatches the request to the router and processes the outcome to + /// produce a response. If the initial outcome is a *forward* and the + /// request was a HEAD request, the request is rewritten and rerouted as a + /// GET. This is automatic HEAD handling. + /// + /// After performing the above, if the outcome is a forward or error, the + /// appropriate error catcher is invoked to produce the response. Otherwise, + /// the successful response is used directly. + /// + /// Finally, new cookies in the cookie jar are added to the response, + /// Rocket-specific headers are written, and response fairings are run. Note + /// that error responses have special cookie handling. See `handle_error`. + pub(crate) async fn dispatch<'r, 's: 'r>( + &'s self, + _token: RequestToken, + request: &'r Request<'s>, + data: Data<'r>, + // io_stream: impl Future> + Send, + ) -> Response<'r> { + info!("{}:", request); + + // Remember if the request is `HEAD` for later body stripping. + let was_head_request = request.method() == Method::Head; + + // Route the request and run the user's handlers. + let mut response = match self.route(request, data).await { + Outcome::Success(response) => response, + Outcome::Forward((data, _)) if request.method() == Method::Head => { + info_!("Autohandling {} request.", "HEAD".primary().bold()); + + // Dispatch the request again with Method `GET`. + request._set_method(Method::Get); + match self.route(request, data).await { + Outcome::Success(response) => response, + Outcome::Error(status) => self.dispatch_error(status, request).await, + Outcome::Forward((_, status)) => self.dispatch_error(status, request).await, + } + } + Outcome::Forward((_, status)) => self.dispatch_error(status, request).await, + Outcome::Error(status) => self.dispatch_error(status, request).await, + }; + + // Set the cookies. Note that error responses will only include cookies + // set by the error handler. See `handle_error` for more. + let delta_jar = request.cookies().take_delta_jar(); + for cookie in delta_jar.delta() { + response.adjoin_header(cookie); + } + + // Add a default 'Server' header if it isn't already there. + // TODO: If removing Hyper, write out `Date` header too. + if let Some(ident) = request.rocket().config.ident.as_str() { + if !response.headers().contains("Server") { + response.set_header(Header::new("Server", ident)); + } + } + + // Run the response fairings. + self.fairings.handle_response(request, &mut response).await; + + // Strip the body if this is a `HEAD` request. + if was_head_request { + response.strip_body(); + } + + // TODO: Should upgrades be handled here? We miss them on local clients. + response + } + + pub(crate) fn extract_io_handler<'r>( + request: &Request<'_>, + response: &mut Response<'r>, + // io_stream: impl Future> + Send, + ) -> Option> { + let upgrades = request.headers().get("upgrade"); + let Ok(upgrade) = response.search_upgrades(upgrades) else { + warn_!("Request wants upgrade but no I/O handler matched."); + info_!("Request is not being upgraded."); + return None; + }; + + if let Some((proto, io_handler)) = upgrade { + info_!("Attemping upgrade with {proto} I/O handler."); + response.set_status(Status::SwitchingProtocols); + response.set_raw_header("Connection", "Upgrade"); + response.set_raw_header("Upgrade", proto.to_string()); + return Some(io_handler); + } + + None + } + + /// Calls the handler for each matching route until one of the handlers + /// returns success or error, or there are no additional routes to try, in + /// which case a `Forward` with the last forwarding state is returned. + #[inline] + async fn route<'s, 'r: 's>( + &'s self, + request: &'r Request<'s>, + mut data: Data<'r>, + ) -> route::Outcome<'r> { + // Go through all matching routes until we fail or succeed or run out of + // routes to try, in which case we forward with the last status. + let mut status = Status::NotFound; + for route in self.router.route(request) { + // Retrieve and set the requests parameters. + info_!("Matched: {}", route); + request.set_route(route); + + let name = route.name.as_deref(); + let outcome = catch_handle(name, || route.handler.handle(request, data)).await + .unwrap_or(Outcome::Error(Status::InternalServerError)); + + // Check if the request processing completed (Some) or if the + // request needs to be forwarded. If it does, continue the loop + // (None) to try again. + info_!("{}", outcome.log_display()); + match outcome { + o@Outcome::Success(_) | o@Outcome::Error(_) => return o, + Outcome::Forward(forwarded) => (data, status) = forwarded, + } + } + + error_!("No matching routes for {}.", request); + Outcome::Forward((data, status)) + } + + // Invokes the catcher for `status`. Returns the response on success. + // + // Resets the cookie jar delta state to prevent any modifications from + // earlier unsuccessful paths from being reflected in the error response. + // + // On catcher error, the 500 error catcher is attempted. If _that_ errors, + // the (infallible) default 500 error cather is used. + pub(crate) async fn dispatch_error<'r, 's: 'r>( + &'s self, + mut status: Status, + req: &'r Request<'s> + ) -> Response<'r> { + // We may wish to relax this in the future. + req.cookies().reset_delta(); + + // Dispatch to the `status` catcher. + if let Ok(r) = self.invoke_catcher(status, req).await { + return r; + } + + // If it fails and it's not a 500, try the 500 catcher. + if status != Status::InternalServerError { + error_!("Catcher failed. Attempting 500 error catcher."); + status = Status::InternalServerError; + if let Ok(r) = self.invoke_catcher(status, req).await { + return r; + } + } + + // If it failed again or if it was already a 500, use Rocket's default. + error_!("{} catcher failed. Using Rocket default 500.", status.code); + crate::catcher::default_handler(Status::InternalServerError, req) + } + + /// Invokes the handler with `req` for catcher with status `status`. + /// + /// In order of preference, invoked handler is: + /// * the user's registered handler for `status` + /// * the user's registered `default` handler + /// * Rocket's default handler for `status` + /// + /// Return `Ok(result)` if the handler succeeded. Returns `Ok(Some(Status))` + /// if the handler ran to completion but failed. Returns `Ok(None)` if the + /// handler panicked while executing. + async fn invoke_catcher<'s, 'r: 's>( + &'s self, + status: Status, + req: &'r Request<'s> + ) -> Result, Option> { + if let Some(catcher) = self.router.catch(status, req) { + warn_!("Responding with registered {} catcher.", catcher); + let name = catcher.name.as_deref(); + catch_handle(name, || catcher.handler.handle(status, req)).await + .map(|result| result.map_err(Some)) + .unwrap_or_else(|| Err(None)) + } else { + let code = status.code.blue().bold(); + warn_!("No {} catcher registered. Using Rocket default.", code); + Ok(crate::catcher::default_handler(status, req)) + } + } + +} diff --git a/core/lib/src/listener/bindable.rs b/core/lib/src/listener/bindable.rs new file mode 100644 index 0000000000..6702138239 --- /dev/null +++ b/core/lib/src/listener/bindable.rs @@ -0,0 +1,40 @@ +use futures::TryFutureExt; + +use crate::listener::Listener; + +pub trait Bindable: Sized { + type Listener: Listener + 'static; + + type Error: std::error::Error + Send + 'static; + + async fn bind(self) -> Result; +} + +impl Bindable for L { + type Listener = L; + + type Error = std::convert::Infallible; + + async fn bind(self) -> Result { + Ok(self) + } +} + +impl Bindable for either::Either { + type Listener = tokio_util::either::Either; + + type Error = either::Either; + + async fn bind(self) -> Result { + match self { + either::Either::Left(a) => a.bind() + .map_ok(tokio_util::either::Either::Left) + .map_err(either::Either::Left) + .await, + either::Either::Right(b) => b.bind() + .map_ok(tokio_util::either::Either::Right) + .map_err(either::Either::Right) + .await, + } + } +} diff --git a/core/lib/src/listener/bounced.rs b/core/lib/src/listener/bounced.rs new file mode 100644 index 0000000000..c8e4203bec --- /dev/null +++ b/core/lib/src/listener/bounced.rs @@ -0,0 +1,58 @@ +use std::{io, time::Duration}; + +use crate::listener::{Listener, Endpoint}; + +static DURATION: Duration = Duration::from_millis(250); + +pub struct Bounced { + listener: L, +} + +pub trait BouncedExt: Sized { + fn bounced(self) -> Bounced { + Bounced { listener: self } + } +} + +impl BouncedExt for L { } + +fn is_recoverable(e: &io::Error) -> bool { + matches!(e.kind(), + | io::ErrorKind::ConnectionRefused + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::ConnectionReset) +} + +impl Bounced { + #[inline] + pub async fn accept_next(&self) -> ::Accept { + loop { + match self.listener.accept().await { + Ok(accept) => return accept, + Err(e) if is_recoverable(&e) => warn!("recoverable connection error: {e}"), + Err(e) => { + warn!("accept error: {e} [retrying in {}ms]", DURATION.as_millis()); + tokio::time::sleep(DURATION).await; + } + }; + } + } +} + +impl Listener for Bounced { + type Accept = L::Accept; + + type Connection = L::Connection; + + async fn accept(&self) -> io::Result { + Ok(self.accept_next().await) + } + + async fn connect(&self, accept: Self::Accept) -> io::Result { + self.listener.connect(accept).await + } + + fn socket_addr(&self) -> io::Result { + self.listener.socket_addr() + } +} diff --git a/core/lib/src/listener/cancellable.rs b/core/lib/src/listener/cancellable.rs new file mode 100644 index 0000000000..fbabfb2c6d --- /dev/null +++ b/core/lib/src/listener/cancellable.rs @@ -0,0 +1,273 @@ +use std::io; +use std::time::Duration; +use std::task::{Poll, Context}; +use std::pin::Pin; + +use tokio::time::{sleep, Sleep}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use futures::{StreamExt, future::{select, Either, Fuse, Future, FutureExt}}; +use pin_project_lite::pin_project; + +use crate::{config, Shutdown}; +use crate::listener::{Listener, Connection, Certificates, Bounced, Endpoint}; + +// Rocket wraps all connections in a `CancellableIo` struct, an internal +// structure that gracefully closes I/O when it receives a signal. That signal +// is the `shutdown` future. When the future resolves, `CancellableIo` begins to +// terminate in grace, mercy, and finally force close phases. Since all +// connections are wrapped in `CancellableIo`, this eventually ends all I/O. +// +// At that point, unless a user spawned an infinite, stand-alone task that isn't +// monitoring `Shutdown`, all tasks should resolve. This means that all +// instances of the shared `Arc` are dropped and we can return the owned +// instance of `Rocket`. +// +// Unfortunately, the Hyper `server` future resolves as soon as it has finished +// processing requests without respect for ongoing responses. That is, `server` +// resolves even when there are running tasks that are generating a response. +// So, `server` resolving implies little to nothing about the state of +// connections. As a result, we depend on the timing of grace + mercy + some +// buffer to determine when all connections should be closed, thus all tasks +// should be complete, thus all references to `Arc` should be dropped +// and we can get a unique reference. +pin_project! { + pub struct CancellableListener { + pub trigger: F, + #[pin] + pub listener: L, + pub grace: Duration, + pub mercy: Duration, + } +} + +pin_project! { + /// I/O that can be cancelled when a future `F` resolves. + #[must_use = "futures do nothing unless polled"] + pub struct CancellableIo { + #[pin] + io: Option, + #[pin] + trigger: Fuse, + state: State, + grace: Duration, + mercy: Duration, + } +} + +enum State { + /// I/O has not been cancelled. Proceed as normal. + Active, + /// I/O has been cancelled. See if we can finish before the timer expires. + Grace(Pin>), + /// Grace period elapsed. Shutdown the connection, waiting for the timer + /// until we force close. + Mercy(Pin>), +} + +pub trait CancellableExt: Sized { + fn cancellable( + self, + trigger: Shutdown, + config: &config::Shutdown + ) -> CancellableListener { + if let Some(mut stream) = config.signal_stream() { + let trigger = trigger.clone(); + tokio::spawn(async move { + while let Some(sig) = stream.next().await { + if trigger.0.tripped() { + warn!("Received {}. Shutdown already in progress.", sig); + } else { + warn!("Received {}. Requesting shutdown.", sig); + } + + trigger.0.trip(); + } + }); + }; + + CancellableListener { + trigger, + listener: self, + grace: config.grace(), + mercy: config.mercy(), + } + } +} + +impl CancellableExt for L { } + +fn time_out() -> io::Error { + io::Error::new(io::ErrorKind::TimedOut, "Shutdown grace timed out") +} + +fn gone() -> io::Error { + io::Error::new(io::ErrorKind::BrokenPipe, "IO driver has terminated") +} + +impl CancellableListener> + where L: Listener + Sync, + F: Future + Unpin + Clone + Send + Sync + 'static +{ + pub async fn accept_next(&self) -> Option<::Accept> { + let next = std::pin::pin!(self.listener.accept_next()); + match select(next, self.trigger.clone()).await { + Either::Left((next, _)) => Some(next), + Either::Right(_) => None, + } + } +} + +impl CancellableListener + where L: Listener + Sync, + F: Future + Clone + Send + Sync + 'static +{ + fn io(&self, conn: C) -> CancellableIo { + CancellableIo { + io: Some(conn), + trigger: self.trigger.clone().fuse(), + state: State::Active, + grace: self.grace, + mercy: self.mercy, + } + } +} + +impl Listener for CancellableListener + where L: Listener + Sync, + F: Future + Clone + Send + Sync + Unpin + 'static +{ + type Accept = L::Accept; + + type Connection = CancellableIo; + + async fn accept(&self) -> io::Result { + let accept = std::pin::pin!(self.listener.accept()); + match select(accept, self.trigger.clone()).await { + Either::Left((result, _)) => result, + Either::Right(_) => Err(gone()), + } + } + + async fn connect(&self, accept: Self::Accept) -> io::Result { + let conn = std::pin::pin!(self.listener.connect(accept)); + match select(conn, self.trigger.clone()).await { + Either::Left((conn, _)) => Ok(self.io(conn?)), + Either::Right(_) => Err(gone()), + } + } + + fn socket_addr(&self) -> io::Result { + self.listener.socket_addr() + } +} + +impl CancellableIo { + fn inner(&self) -> Option<&I> { + self.io.as_ref() + } + + /// Run `do_io` while connection processing should continue. + fn poll_trigger_then( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + do_io: impl FnOnce(Pin<&mut I>, &mut Context<'_>) -> Poll>, + ) -> Poll> { + let mut me = self.as_mut().project(); + let io = match me.io.as_pin_mut() { + Some(io) => io, + None => return Poll::Ready(Err(gone())), + }; + + loop { + match me.state { + State::Active => { + if me.trigger.as_mut().poll(cx).is_ready() { + *me.state = State::Grace(Box::pin(sleep(*me.grace))); + } else { + return do_io(io, cx); + } + } + State::Grace(timer) => { + if timer.as_mut().poll(cx).is_ready() { + *me.state = State::Mercy(Box::pin(sleep(*me.mercy))); + } else { + return do_io(io, cx); + } + } + State::Mercy(timer) => { + if timer.as_mut().poll(cx).is_ready() { + self.project().io.set(None); + return Poll::Ready(Err(time_out())); + } else { + let result = futures::ready!(io.poll_shutdown(cx)); + self.project().io.set(None); + return match result { + Err(e) => Poll::Ready(Err(e)), + Ok(()) => Poll::Ready(Err(gone())) + }; + } + }, + } + } + } +} + +impl AsyncRead for CancellableIo { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_read(cx, buf)) + } +} + +impl AsyncWrite for CancellableIo { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_write(cx, buf)) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_> + ) -> Poll> { + self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_flush(cx)) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_> + ) -> Poll> { + self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_shutdown(cx)) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + self.as_mut().poll_trigger_then(cx, |io, cx| io.poll_write_vectored(cx, bufs)) + } + + fn is_write_vectored(&self) -> bool { + self.inner().map(|io| io.is_write_vectored()).unwrap_or(false) + } +} + +impl Connection for CancellableIo + where F: Unpin + Send + 'static +{ + fn peer_address(&self) -> io::Result { + self.inner() + .ok_or_else(|| gone()) + .and_then(|io| io.peer_address()) + } + + fn peer_certificates(&self) -> Option> { + self.inner().and_then(|io| io.peer_certificates()) + } +} diff --git a/core/lib/src/listener/connection.rs b/core/lib/src/listener/connection.rs new file mode 100644 index 0000000000..68541109e0 --- /dev/null +++ b/core/lib/src/listener/connection.rs @@ -0,0 +1,93 @@ +use std::io; +use std::borrow::Cow; + +use tokio_util::either::Either; +use tokio::io::{AsyncRead, AsyncWrite}; + +use super::Endpoint; + +/// A collection of raw certificate data. +#[derive(Clone)] +pub struct Certificates<'r>(Cow<'r, [der::CertificateDer<'r>]>); + +pub trait Connection: AsyncRead + AsyncWrite + Send + Unpin { + fn peer_address(&self) -> io::Result; + + /// DER-encoded X.509 certificate chain presented by the client, if any. + /// + /// The certificate order must be as it appears in the TLS protocol: the + /// first certificate relates to the peer, the second certifies the first, + /// the third certifies the second, and so on. + /// + /// Defaults to an empty vector to indicate that no certificates were + /// presented. + fn peer_certificates(&self) -> Option> { None } +} + +impl Connection for Either { + fn peer_address(&self) -> io::Result { + match self { + Either::Left(c) => c.peer_address(), + Either::Right(c) => c.peer_address(), + } + } + + fn peer_certificates(&self) -> Option> { + match self { + Either::Left(c) => c.peer_certificates(), + Either::Right(c) => c.peer_certificates(), + } + } +} + +impl Certificates<'_> { + pub fn into_owned(self) -> Certificates<'static> { + let cow = self.0.into_iter() + .map(|der| der.clone().into_owned()) + .collect::>() + .into(); + + Certificates(cow) + } +} + +#[cfg(feature = "mtls")] +#[cfg_attr(nightly, doc(cfg(feature = "mtls")))] +mod der { + use super::*; + + pub use crate::mtls::CertificateDer; + + impl<'r> Certificates<'r> { + pub(crate) fn inner(&self) -> &[CertificateDer<'r>] { + &self.0 + } + } + + impl<'r> From<&'r [CertificateDer<'r>]> for Certificates<'r> { + fn from(value: &'r [CertificateDer<'r>]) -> Self { + Certificates(value.into()) + } + } + + impl From>> for Certificates<'static> { + fn from(value: Vec>) -> Self { + Certificates(value.into()) + } + } +} + +#[cfg(not(feature = "mtls"))] +mod der { + use std::marker::PhantomData; + + /// A thin wrapper over raw, DER-encoded X.509 client certificate data. + #[derive(Clone)] + pub struct CertificateDer<'r>(PhantomData<&'r [u8]>); + + impl CertificateDer<'_> { + pub fn into_owned(self) -> CertificateDer<'static> { + CertificateDer(PhantomData) + } + } +} diff --git a/core/lib/src/listener/default.rs b/core/lib/src/listener/default.rs new file mode 100644 index 0000000000..32f4a650f0 --- /dev/null +++ b/core/lib/src/listener/default.rs @@ -0,0 +1,61 @@ +use either::Either; + +use crate::listener::{Bindable, Endpoint}; +use crate::error::{Error, ErrorKind}; + +#[derive(serde::Deserialize)] +pub struct DefaultListener { + #[serde(default)] + pub address: Endpoint, + pub port: Option, + pub reuse: Option, + #[cfg(feature = "tls")] + pub tls: Option, +} + +#[cfg(not(unix))] type BaseBindable = Either; +#[cfg(unix)] type BaseBindable = Either; + +#[cfg(not(feature = "tls"))] type TlsBindable = Either; +#[cfg(feature = "tls")] type TlsBindable = Either, T>; + +impl DefaultListener { + pub(crate) fn base_bindable(&self) -> Result { + match &self.address { + Endpoint::Tcp(mut address) => { + self.port.map(|port| address.set_port(port)); + Ok(BaseBindable::Left(address)) + }, + #[cfg(unix)] + Endpoint::Unix(path) => { + let uds = super::unix::UdsConfig { path: path.clone(), reuse: self.reuse, }; + Ok(BaseBindable::Right(uds)) + }, + #[cfg(not(unix))] + Endpoint::Unix(_) => { + let msg = "Unix domain sockets unavailable on non-unix platforms."; + let boxed = Box::::from(msg); + Err(Error::new(ErrorKind::Bind(boxed))) + }, + other => { + let msg = format!("unsupported default listener address: {other}"); + let boxed = Box::::from(msg); + Err(Error::new(ErrorKind::Bind(boxed))) + } + } + } + + pub(crate) fn tls_bindable(&self, inner: T) -> TlsBindable { + #[cfg(feature = "tls")] + if let Some(tls) = self.tls.clone() { + return TlsBindable::Left(super::tls::TlsBindable { inner, tls }); + } + + TlsBindable::Right(inner) + } + + pub fn bindable(&self) -> Result { + self.base_bindable() + .map(|b| b.map_either(|b| self.tls_bindable(b), |b| self.tls_bindable(b))) + } +} diff --git a/core/lib/src/listener/endpoint.rs b/core/lib/src/listener/endpoint.rs new file mode 100644 index 0000000000..26640d1d1c --- /dev/null +++ b/core/lib/src/listener/endpoint.rs @@ -0,0 +1,281 @@ +use std::fmt; +use std::path::{Path, PathBuf}; +use std::any::Any; +use std::net::{SocketAddr as TcpAddr, Ipv4Addr, AddrParseError}; +use std::str::FromStr; +use std::sync::Arc; + +use serde::de; + +use crate::http::uncased::AsUncased; + +pub trait EndpointAddr: fmt::Display + fmt::Debug + Sync + Send + Any { } + +impl EndpointAddr for T {} + +#[cfg(not(feature = "tls"))] type TlsInfo = Option<()>; +#[cfg(feature = "tls")] type TlsInfo = Option; + +/// # Conversions +/// +/// * [`&str`] - parse with [`FromStr`] +/// * [`tokio::net::unix::SocketAddr`] - must be path: [`ListenerAddr::Unix`] +/// * [`std::net::SocketAddr`] - infallibly as [ListenerAddr::Tcp] +/// * [`PathBuf`] - infallibly as [`ListenerAddr::Unix`] +// TODO: Rename to something better. `Endpoint`? +#[derive(Debug)] +pub enum Endpoint { + Tcp(TcpAddr), + Unix(PathBuf), + Tls(Arc, TlsInfo), + Custom(Arc), +} + +impl Endpoint { + pub fn new(value: T) -> Endpoint { + Endpoint::Custom(Arc::new(value)) + } + + pub fn tcp(&self) -> Option { + match self { + Endpoint::Tcp(addr) => Some(*addr), + _ => None, + } + } + + pub fn unix(&self) -> Option<&Path> { + match self { + Endpoint::Unix(addr) => Some(addr), + _ => None, + } + } + + pub fn tls(&self) -> Option<&Endpoint> { + match self { + Endpoint::Tls(addr, _) => Some(addr), + _ => None, + } + } + + #[cfg(feature = "tls")] + pub fn tls_config(&self) -> Option<&crate::tls::TlsConfig> { + match self { + Endpoint::Tls(_, Some(ref config)) => Some(config), + _ => None, + } + } + + #[cfg(feature = "mtls")] + pub fn mtls_config(&self) -> Option<&crate::mtls::MtlsConfig> { + match self { + Endpoint::Tls(_, Some(config)) => config.mutual(), + _ => None, + } + } + + pub fn downcast(&self) -> Option<&T> { + match self { + Endpoint::Tcp(addr) => (&*addr as &dyn Any).downcast_ref(), + Endpoint::Unix(addr) => (&*addr as &dyn Any).downcast_ref(), + Endpoint::Custom(addr) => (&*addr as &dyn Any).downcast_ref(), + Endpoint::Tls(inner, ..) => inner.downcast(), + } + } + + pub fn is_tcp(&self) -> bool { + self.tcp().is_some() + } + + pub fn is_unix(&self) -> bool { + self.unix().is_some() + } + + pub fn is_tls(&self) -> bool { + self.tls().is_some() + } + + #[cfg(feature = "tls")] + pub fn with_tls(self, config: crate::tls::TlsConfig) -> Endpoint { + if self.is_tls() { + return self; + } + + Self::Tls(Arc::new(self), Some(config)) + } + + pub fn assume_tls(self) -> Endpoint { + if self.is_tls() { + return self; + } + + Self::Tls(Arc::new(self), None) + } +} + +impl fmt::Display for Endpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Endpoint::*; + + match self { + Tcp(addr) => write!(f, "http://{addr}"), + Unix(addr) => write!(f, "unix:{}", addr.display()), + Custom(inner) => inner.fmt(f), + Tls(inner, c) => match (&**inner, c.as_ref()) { + #[cfg(feature = "mtls")] + (Tcp(i), Some(c)) if c.mutual().is_some() => write!(f, "https://{i} (TLS + MTLS)"), + (Tcp(i), _) => write!(f, "https://{i} (TLS)"), + #[cfg(feature = "mtls")] + (i, Some(c)) if c.mutual().is_some() => write!(f, "{i} (TLS + MTLS)"), + (inner, _) => write!(f, "{inner} (TLS)"), + }, + } + } +} + +impl From for Endpoint { + fn from(value: std::net::SocketAddr) -> Self { + Self::Tcp(value) + } +} + +impl From for Endpoint { + fn from(value: std::net::SocketAddrV4) -> Self { + Self::Tcp(value.into()) + } +} + +impl From for Endpoint { + fn from(value: std::net::SocketAddrV6) -> Self { + Self::Tcp(value.into()) + } +} + +impl From for Endpoint { + fn from(value: PathBuf) -> Self { + Self::Unix(value) + } +} + +#[cfg(unix)] +impl TryFrom for Endpoint { + type Error = std::io::Error; + + fn try_from(v: tokio::net::unix::SocketAddr) -> Result { + v.as_pathname() + .ok_or_else(|| std::io::Error::other("unix socket is not path")) + .map(|path| Endpoint::Unix(path.to_path_buf())) + } +} + +impl TryFrom<&str> for Endpoint { + type Error = AddrParseError; + + fn try_from(value: &str) -> Result { + value.parse() + } +} + +impl Default for Endpoint { + fn default() -> Self { + Endpoint::Tcp(TcpAddr::new(Ipv4Addr::LOCALHOST.into(), 8000)) + } +} + +/// Parses an address into a `ListenerAddr`. +/// +/// The syntax is: +/// +/// ```text +/// listener_addr = 'tcp' ':' tcp_addr | 'unix' ':' unix_addr | tcp_addr +/// tcp_addr := IP_ADDR | SOCKET_ADDR +/// unix_addr := PATH +/// +/// IP_ADDR := `std::net::IpAddr` string as defined by Rust +/// SOCKET_ADDR := `std::net::SocketAddr` string as defined by Rust +/// PATH := `PathBuf` (any UTF-8) string as defined by Rust +/// ``` +/// +/// If `IP_ADDR` is specified, the port defaults to `8000`. +impl FromStr for Endpoint { + type Err = AddrParseError; + + fn from_str(string: &str) -> Result { + fn parse_tcp(string: &str, def_port: u16) -> Result { + string.parse().or_else(|_| string.parse().map(|ip| TcpAddr::new(ip, def_port))) + } + + if let Some((proto, string)) = string.split_once(':') { + if proto.trim().as_uncased() == "tcp" { + return parse_tcp(string.trim(), 8000).map(Self::Tcp); + } else if proto.trim().as_uncased() == "unix" { + return Ok(Self::Unix(PathBuf::from(string.trim()))); + } + } + + parse_tcp(string.trim(), 8000).map(Self::Tcp) + } +} + +impl<'de> de::Deserialize<'de> for Endpoint { + fn deserialize>(de: D) -> Result { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = Endpoint; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("TCP or Unix address") + } + + fn visit_str(self, v: &str) -> Result { + v.parse::().map_err(|e| E::custom(e.to_string())) + } + } + + de.deserialize_any(Visitor) + } +} + +impl Eq for Endpoint { } + +impl PartialEq for Endpoint { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Tcp(l0), Self::Tcp(r0)) => l0 == r0, + (Self::Unix(l0), Self::Unix(r0)) => l0 == r0, + (Self::Tls(l0, _), Self::Tls(r0, _)) => l0 == r0, + (Self::Custom(l0), Self::Custom(r0)) => l0.to_string() == r0.to_string(), + _ => false, + } + } +} + +impl PartialEq for Endpoint { + fn eq(&self, other: &std::net::SocketAddr) -> bool { + self.tcp() == Some(*other) + } +} + +impl PartialEq for Endpoint { + fn eq(&self, other: &std::net::SocketAddrV4) -> bool { + self.tcp() == Some((*other).into()) + } +} + +impl PartialEq for Endpoint { + fn eq(&self, other: &std::net::SocketAddrV6) -> bool { + self.tcp() == Some((*other).into()) + } +} + +impl PartialEq for Endpoint { + fn eq(&self, other: &PathBuf) -> bool { + self.unix() == Some(other.as_path()) + } +} + +impl PartialEq for Endpoint { + fn eq(&self, other: &Path) -> bool { + self.unix() == Some(other) + } +} diff --git a/core/lib/src/listener/listener.rs b/core/lib/src/listener/listener.rs new file mode 100644 index 0000000000..8bdbc08c2b --- /dev/null +++ b/core/lib/src/listener/listener.rs @@ -0,0 +1,65 @@ +use std::io; + +use futures::TryFutureExt; +use tokio_util::either::Either; + +use crate::listener::{Connection, Endpoint}; + +pub trait Listener: Send + Sync { + type Accept: Send; + + type Connection: Connection; + + async fn accept(&self) -> io::Result; + + #[crate::async_bound(Send)] + async fn connect(&self, accept: Self::Accept) -> io::Result; + + fn socket_addr(&self) -> io::Result; +} + +impl Listener for &L { + type Accept = L::Accept; + + type Connection = L::Connection; + + async fn accept(&self) -> io::Result { + ::accept(self).await + } + + async fn connect(&self, accept: Self::Accept) -> io::Result { + ::connect(self, accept).await + } + + fn socket_addr(&self) -> io::Result { + ::socket_addr(self) + } +} + +impl Listener for Either { + type Accept = Either; + + type Connection = Either; + + async fn accept(&self) -> io::Result { + match self { + Either::Left(l) => l.accept().map_ok(Either::Left).await, + Either::Right(l) => l.accept().map_ok(Either::Right).await, + } + } + + async fn connect(&self, accept: Self::Accept) -> io::Result { + match (self, accept) { + (Either::Left(l), Either::Left(a)) => l.connect(a).map_ok(Either::Left).await, + (Either::Right(l), Either::Right(a)) => l.connect(a).map_ok(Either::Right).await, + _ => unreachable!() + } + } + + fn socket_addr(&self) -> io::Result { + match self { + Either::Left(l) => l.socket_addr(), + Either::Right(l) => l.socket_addr(), + } + } +} diff --git a/core/lib/src/listener/mod.rs b/core/lib/src/listener/mod.rs new file mode 100644 index 0000000000..244c36c604 --- /dev/null +++ b/core/lib/src/listener/mod.rs @@ -0,0 +1,24 @@ +mod cancellable; +mod bounced; +mod listener; +mod endpoint; +mod connection; +mod bindable; +mod default; + +#[cfg(unix)] +#[cfg_attr(nightly, doc(cfg(unix)))] +pub mod unix; +#[cfg(feature = "tls")] +#[cfg_attr(nightly, doc(cfg(feature = "tls")))] +pub mod tls; +pub mod tcp; + +pub use endpoint::*; +pub use listener::*; +pub use connection::*; +pub use bindable::*; +pub use default::*; + +pub(crate) use cancellable::*; +pub(crate) use bounced::*; diff --git a/core/lib/src/listener/tcp.rs b/core/lib/src/listener/tcp.rs new file mode 100644 index 0000000000..c2e3fd9f3f --- /dev/null +++ b/core/lib/src/listener/tcp.rs @@ -0,0 +1,43 @@ +use std::io; + +#[doc(inline)] +pub use tokio::net::{TcpListener, TcpStream}; + +use crate::listener::{Listener, Bindable, Connection, Endpoint}; + +impl Bindable for std::net::SocketAddr { + type Listener = TcpListener; + + type Error = io::Error; + + async fn bind(self) -> Result { + TcpListener::bind(self).await + } +} + +impl Listener for TcpListener { + type Accept = Self::Connection; + + type Connection = TcpStream; + + async fn accept(&self) -> io::Result { + let conn = self.accept().await?.0; + let _ = conn.set_nodelay(true); + let _ = conn.set_linger(None); + Ok(conn) + } + + async fn connect(&self, conn: Self::Connection) -> io::Result { + Ok(conn) + } + + fn socket_addr(&self) -> io::Result { + self.local_addr().map(Endpoint::Tcp) + } +} + +impl Connection for TcpStream { + fn peer_address(&self) -> io::Result { + self.peer_addr().map(Endpoint::Tcp) + } +} diff --git a/core/lib/src/listener/tls.rs b/core/lib/src/listener/tls.rs new file mode 100644 index 0000000000..ce2b53ffaf --- /dev/null +++ b/core/lib/src/listener/tls.rs @@ -0,0 +1,116 @@ +use std::io; +use std::sync::Arc; + +use serde::Deserialize; +use rustls::server::{ServerSessionMemoryCache, ServerConfig, WebPkiClientVerifier}; +use tokio_rustls::TlsAcceptor; + +use crate::tls::{TlsConfig, Error}; +use crate::tls::util::{load_cert_chain, load_key, load_ca_certs}; +use crate::listener::{Listener, Bindable, Connection, Certificates, Endpoint}; + +#[doc(inline)] +pub use tokio_rustls::server::TlsStream; + +/// A TLS listener over some listener interface L. +pub struct TlsListener { + listener: L, + acceptor: TlsAcceptor, + config: TlsConfig, +} + +#[derive(Clone, Deserialize)] +pub struct TlsBindable { + #[serde(flatten)] + pub inner: I, + pub tls: TlsConfig, +} + +impl TlsConfig { + pub(crate) fn acceptor(&self) -> Result { + let provider = rustls::crypto::CryptoProvider { + cipher_suites: self.ciphers().map(|c| c.into()).collect(), + ..rustls::crypto::ring::default_provider() + }; + + #[cfg(feature = "mtls")] + let verifier = match self.mutual { + Some(ref mtls) => { + let ca_certs = load_ca_certs(&mut mtls.ca_certs_reader()?)?; + let verifier = WebPkiClientVerifier::builder(Arc::new(ca_certs)); + match mtls.mandatory { + true => verifier.build()?, + false => verifier.allow_unauthenticated().build()?, + } + }, + None => WebPkiClientVerifier::no_client_auth(), + }; + + #[cfg(not(feature = "mtls"))] + let verifier = WebPkiClientVerifier::no_client_auth(); + + let key = load_key(&mut self.key_reader()?)?; + let cert_chain = load_cert_chain(&mut self.certs_reader()?)?; + let mut tls_config = ServerConfig::builder_with_provider(Arc::new(provider)) + .with_safe_default_protocol_versions()? + .with_client_cert_verifier(verifier) + .with_single_cert(cert_chain, key)?; + + tls_config.ignore_client_order = self.prefer_server_cipher_order; + tls_config.session_storage = ServerSessionMemoryCache::new(1024); + tls_config.ticketer = rustls::crypto::ring::Ticketer::new()?; + tls_config.alpn_protocols = vec![b"http/1.1".to_vec()]; + if cfg!(feature = "http2") { + tls_config.alpn_protocols.insert(0, b"h2".to_vec()); + } + + Ok(TlsAcceptor::from(Arc::new(tls_config))) + } +} + +impl Bindable for TlsBindable { + type Listener = TlsListener; + + type Error = Error; + + async fn bind(self) -> Result { + Ok(TlsListener { + acceptor: self.tls.acceptor()?, + listener: self.inner.bind().await.map_err(|e| Error::Bind(Box::new(e)))?, + config: self.tls, + }) + } +} + +impl Listener for TlsListener + where L::Connection: Unpin +{ + type Accept = L::Accept; + + type Connection = TlsStream; + + async fn accept(&self) -> io::Result { + self.listener.accept().await + } + + async fn connect(&self, accept: L::Accept) -> io::Result { + let conn = self.listener.connect(accept).await?; + self.acceptor.accept(conn).await + } + + fn socket_addr(&self) -> io::Result { + Ok(self.listener.socket_addr()?.with_tls(self.config.clone())) + } +} + +impl Connection for TlsStream { + fn peer_address(&self) -> io::Result { + Ok(self.get_ref().0.peer_address()?.assume_tls()) + } + + #[cfg(feature = "mtls")] + fn peer_certificates(&self) -> Option> { + let cert_chain = self.get_ref().1.peer_certificates()?; + Some(Certificates::from(cert_chain)) + } +} diff --git a/core/lib/src/listener/unix.rs b/core/lib/src/listener/unix.rs new file mode 100644 index 0000000000..b6dea5870f --- /dev/null +++ b/core/lib/src/listener/unix.rs @@ -0,0 +1,107 @@ +use std::io; +use std::path::PathBuf; + +use tokio::time::{sleep, Duration}; + +use crate::fs::NamedFile; +use crate::listener::{Listener, Bindable, Connection, Endpoint}; +use crate::util::unix; + +pub use tokio::net::UnixStream; + +#[derive(Debug, Clone)] +pub struct UdsConfig { + /// Socket address. + pub path: PathBuf, + /// Recreate a socket that already exists. + pub reuse: Option, +} + +pub struct UdsListener { + path: PathBuf, + lock: Option, + listener: tokio::net::UnixListener, +} + +impl Bindable for UdsConfig { + type Listener = UdsListener; + + type Error = io::Error; + + async fn bind(self) -> Result { + let lock = if self.reuse.unwrap_or(true) { + let lock_ext = match self.path.extension().and_then(|s| s.to_str()) { + Some(ext) if !ext.is_empty() => format!("{}.lock", ext), + _ => "lock".to_string() + }; + + let mut opts = tokio::fs::File::options(); + opts.create(true).write(true); + let lock_path = self.path.with_extension(lock_ext); + let lock_file = NamedFile::open_with(lock_path, &opts).await?; + + unix::lock_exlusive_nonblocking(lock_file.file())?; + if self.path.exists() { + tokio::fs::remove_file(&self.path).await?; + } + + Some(lock_file) + } else { + None + }; + + // Sometimes, we get `AddrInUse`, even though we've tried deleting the + // socket. If all is well, eventually the socket will _really_ be gone, + // and this will succeed. So let's try a few times. + let mut retries = 5; + let listener = loop { + match tokio::net::UnixListener::bind(&self.path) { + Ok(listener) => break listener, + Err(e) if self.path.exists() && lock.is_none() => return Err(e), + Err(_) if retries > 0 => { + retries -= 1; + sleep(Duration::from_millis(100)).await; + }, + Err(e) => return Err(e), + } + }; + + Ok(UdsListener { lock, listener, path: self.path, }) + } +} + +impl Listener for UdsListener { + type Accept = UnixStream; + + type Connection = Self::Accept; + + async fn accept(&self) -> io::Result { + Ok(self.listener.accept().await?.0) + } + + async fn connect(&self, accept:Self::Accept) -> io::Result { + Ok(accept) + } + + fn socket_addr(&self) -> io::Result { + self.listener.local_addr()?.try_into() + } +} + +impl Connection for UnixStream { + fn peer_address(&self) -> io::Result { + self.local_addr()?.try_into() + } +} + +impl Drop for UdsListener { + fn drop(&mut self) { + if let Some(lock) = &self.lock { + let _ = std::fs::remove_file(&self.path); + let _ = std::fs::remove_file(lock.path()); + let _ = unix::unlock_nonblocking(lock.file()); + } else { + let _ = std::fs::remove_file(&self.path); + } + } +} diff --git a/core/lib/src/local/asynchronous/client.rs b/core/lib/src/local/asynchronous/client.rs index ecec4527cc..2a45a33194 100644 --- a/core/lib/src/local/asynchronous/client.rs +++ b/core/lib/src/local/asynchronous/client.rs @@ -4,7 +4,8 @@ use parking_lot::RwLock; use crate::{Rocket, Phase, Orbit, Ignite, Error}; use crate::local::asynchronous::{LocalRequest, LocalResponse}; -use crate::http::{Method, uri::Origin, private::cookie}; +use crate::http::{Method, uri::Origin}; +use crate::listener::Endpoint; /// An `async` client to construct and dispatch local requests. /// @@ -55,9 +56,15 @@ pub struct Client { impl Client { pub(crate) async fn _new( rocket: Rocket

    , - tracked: bool + tracked: bool, + secure: bool, ) -> Result { - let rocket = rocket.local_launch().await?; + let mut listener = Endpoint::new("local client"); + if secure { + listener = listener.assume_tls(); + } + + let rocket = rocket.local_launch(listener).await?; let cookies = RwLock::new(cookie::CookieJar::new()); Ok(Client { rocket, cookies, tracked }) } diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index 76ed3f3707..4c85c02024 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -23,7 +23,7 @@ use super::{Client, LocalResponse}; /// let client = Client::tracked(rocket::build()).await.expect("valid rocket"); /// let req = client.post("/") /// .header(ContentType::JSON) -/// .remote("127.0.0.1:8000".parse().unwrap()) +/// .remote("127.0.0.1:8000") /// .cookie(("name", "value")) /// .body(r#"{ "value": 42 }"#); /// @@ -86,14 +86,14 @@ impl<'c> LocalRequest<'c> { if self.inner().uri() == invalid { error!("invalid request URI: {:?}", invalid.path()); return LocalResponse::new(self.request, move |req| { - rocket.handle_error(Status::BadRequest, req) + rocket.dispatch_error(Status::BadRequest, req) }).await } } // Actually dispatch the request. let mut data = Data::local(self.data); - let token = rocket.preprocess_request(&mut self.request, &mut data).await; + let token = rocket.preprocess(&mut self.request, &mut data).await; let response = LocalResponse::new(self.request, move |req| { rocket.dispatch(token, req, data) }).await; diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index f91afeb25c..0c3350be8e 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -53,9 +53,14 @@ use crate::{Request, Response}; /// /// For more, see [the top-level documentation](../index.html#localresponse). pub struct LocalResponse<'c> { - _request: Box>, + // XXX: SAFETY: This (dependent) field must come first due to drop order! response: Response<'c>, cookies: CookieJar<'c>, + _request: Box>, +} + +impl Drop for LocalResponse<'_> { + fn drop(&mut self) { } } impl<'c> LocalResponse<'c> { @@ -64,7 +69,8 @@ impl<'c> LocalResponse<'c> { O: Future> + Send { // `LocalResponse` is a self-referential structure. In particular, - // `inner` can refer to `_request` and its contents. As such, we must + // `response` and `cookies` can refer to `_request` and its contents. As + // such, we must // 1) Ensure `Request` has a stable address. // // This is done by `Box`ing the `Request`, using only the stable @@ -97,7 +103,7 @@ impl<'c> LocalResponse<'c> { cookies.add_original(cookie.into_owned()); } - LocalResponse { cookies, _request: boxed_req, response, } + LocalResponse { _request: boxed_req, cookies, response, } } } } diff --git a/core/lib/src/local/blocking/client.rs b/core/lib/src/local/blocking/client.rs index d3a8b0ef94..f87df009f2 100644 --- a/core/lib/src/local/blocking/client.rs +++ b/core/lib/src/local/blocking/client.rs @@ -30,7 +30,7 @@ pub struct Client { } impl Client { - fn _new(rocket: Rocket

    , tracked: bool) -> Result { + fn _new(rocket: Rocket

    , tracked: bool, secure: bool) -> Result { let runtime = tokio::runtime::Builder::new_multi_thread() .thread_name("rocket-local-client-worker-thread") .worker_threads(1) @@ -39,7 +39,7 @@ impl Client { .expect("create tokio runtime"); // Initialize the Rocket instance - let inner = Some(runtime.block_on(asynchronous::Client::_new(rocket, tracked))?); + let inner = Some(runtime.block_on(asynchronous::Client::_new(rocket, tracked, secure))?); Ok(Self { inner, runtime: RefCell::new(runtime) }) } @@ -73,7 +73,7 @@ impl Client { #[inline(always)] pub(crate) fn _with_raw_cookies(&self, f: F) -> T - where F: FnOnce(&crate::http::private::cookie::CookieJar) -> T + where F: FnOnce(&cookie::CookieJar) -> T { self.inner()._with_raw_cookies(f) } diff --git a/core/lib/src/local/blocking/request.rs b/core/lib/src/local/blocking/request.rs index f094c60e44..4d8e35373e 100644 --- a/core/lib/src/local/blocking/request.rs +++ b/core/lib/src/local/blocking/request.rs @@ -21,7 +21,7 @@ use super::{Client, LocalResponse}; /// let client = Client::tracked(rocket::build()).expect("valid rocket"); /// let req = client.post("/") /// .header(ContentType::JSON) -/// .remote("127.0.0.1:8000".parse().unwrap()) +/// .remote("127.0.0.1:8000") /// .cookie(("name", "value")) /// .body(r#"{ "value": 42 }"#); /// diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index f2b3b922d0..9b09ae74ff 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -68,7 +68,12 @@ macro_rules! pub_client_impl { /// ``` #[inline(always)] pub $($prefix)? fn tracked(rocket: Rocket

    ) -> Result { - Self::_new(rocket, true) $(.$suffix)? + Self::_new(rocket, true, false) $(.$suffix)? + } + + #[inline(always)] + pub $($prefix)? fn tracked_secure(rocket: Rocket

    ) -> Result { + Self::_new(rocket, true, true) $(.$suffix)? } /// Construct a new `Client` from an instance of `Rocket` _without_ @@ -92,7 +97,11 @@ macro_rules! pub_client_impl { /// let client = Client::untracked(rocket); /// ``` pub $($prefix)? fn untracked(rocket: Rocket

    ) -> Result { - Self::_new(rocket, false) $(.$suffix)? + Self::_new(rocket, false, false) $(.$suffix)? + } + + pub $($prefix)? fn untracked_secure(rocket: Rocket

    ) -> Result { + Self::_new(rocket, false, true) $(.$suffix)? } /// Terminates `Client` by initiating a graceful shutdown via @@ -135,15 +144,6 @@ macro_rules! pub_client_impl { Self::tracked(rocket.configure(figment)) $(.$suffix)? } - /// Deprecated alias to [`Client::tracked()`]. - #[deprecated( - since = "0.6.0-dev", - note = "choose between `Client::untracked()` and `Client::tracked()`" - )] - pub $($prefix)? fn new(rocket: Rocket

    ) -> Result { - Self::tracked(rocket) $(.$suffix)? - } - /// Returns a reference to the `Rocket` this client is creating requests /// for. /// diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index 78e975957c..1ec0740050 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -97,24 +97,40 @@ macro_rules! pub_request_impl { self._request_mut().add_header(header.into()); } - /// Set the remote address of this request. + /// Set the remote address of this request to `address`. + /// + /// `address` may be any type that [can be converted into a `ListenerAddr`]. + /// If `address` fails to convert, the remote is left unchanged. + /// + /// [can be converted into a `ListenerAddr`]: crate::listener::ListenerAddr#conversions /// /// # Examples /// /// Set the remote address to "8.8.8.8:80": /// /// ```rust + /// use std::net::{SocketAddrV4, Ipv4Addr}; + /// #[doc = $import] /// /// # Client::_test(|_, request, _| { /// let request: LocalRequest = request; - /// let address = "8.8.8.8:80".parse().unwrap(); - /// let req = request.remote(address); + /// let req = request.remote("8.8.8.8:80"); + /// + /// let addr = SocketAddrV4::new(Ipv4Addr::new(8, 8, 8, 8).into(), 80); + /// assert_eq!(req.inner().remote().unwrap(), &addr); /// # }); /// ``` #[inline] - pub fn remote(mut self, address: std::net::SocketAddr) -> Self { - self.set_remote(address); + pub fn remote(mut self, endpoint: T) -> Self + where T: TryInto + { + if let Ok(endpoint) = endpoint.try_into() { + self.set_remote(endpoint); + } else { + warn!("remote failed to convert"); + } + self } @@ -228,11 +244,13 @@ macro_rules! pub_request_impl { #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] pub fn identity(mut self, reader: C) -> Self { - use crate::http::{tls::util::load_cert_chain, private::Certificates}; + use std::sync::Arc; + use crate::tls::util::load_cert_chain; + use crate::listener::Certificates; let mut reader = std::io::BufReader::new(reader); let certs = load_cert_chain(&mut reader).map(Certificates::from); - self._request_mut().connection.client_certificates = certs.ok(); + self._request_mut().connection.peer_certs = certs.ok().map(Arc::new); self } diff --git a/core/lib/src/mtls.rs b/core/lib/src/mtls.rs deleted file mode 100644 index 441fffb6f7..0000000000 --- a/core/lib/src/mtls.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Support for mutual TLS client certificates. -//! -//! For details on how to configure mutual TLS, see -//! [`MutualTls`](crate::config::MutualTls) and the [TLS -//! guide](https://rocket.rs/master/guide/configuration/#tls). See -//! [`Certificate`] for a request guard that validated, verifies, and retrieves -//! client certificates. - -#[doc(inline)] -pub use crate::http::tls::mtls::*; - -use crate::request::{Request, FromRequest, Outcome}; -use crate::outcome::{try_outcome, IntoOutcome}; -use crate::http::Status; - -#[crate::async_trait] -impl<'r> FromRequest<'r> for Certificate<'r> { - type Error = Error; - - async fn from_request(req: &'r Request<'_>) -> Outcome { - let certs = req.connection.client_certificates.as_ref().or_forward(Status::Unauthorized); - let data = try_outcome!(try_outcome!(certs).chain_data().or_forward(Status::Unauthorized)); - Certificate::parse(data).or_error(Status::Unauthorized) - } -} diff --git a/core/http/src/tls/mtls.rs b/core/lib/src/mtls/certificate.rs similarity index 50% rename from core/http/src/tls/mtls.rs rename to core/lib/src/mtls/certificate.rs index 417db2f87d..a430b79fb8 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/lib/src/mtls/certificate.rs @@ -1,51 +1,8 @@ -pub mod oid { - //! Lower-level OID types re-exported from - //! [`oid_registry`](https://docs.rs/oid-registry/0.4) and - //! [`der-parser`](https://docs.rs/der-parser/7). - - pub use x509_parser::oid_registry::*; - pub use x509_parser::objects::*; -} - -pub mod bigint { - //! Signed and unsigned big integer types re-exported from - //! [`num_bigint`](https://docs.rs/num-bigint/0.4). - pub use x509_parser::der_parser::num_bigint::*; -} - -pub mod x509 { - //! Lower-level X.509 types re-exported from - //! [`x509_parser`](https://docs.rs/x509-parser/0.13). - //! - //! Lack of documentation is directly inherited from the source crate. - //! Prefer to use Rocket's wrappers when possible. - - pub use x509_parser::certificate::*; - pub use x509_parser::cri_attributes::*; - pub use x509_parser::error::*; - pub use x509_parser::extensions::*; - pub use x509_parser::revocation_list::*; - pub use x509_parser::time::*; - pub use x509_parser::x509::*; - pub use x509_parser::der_parser::der; - pub use x509_parser::der_parser::ber; - pub use x509_parser::traits::*; -} - -use std::fmt; -use std::ops::Deref; -use std::num::NonZeroUsize; - use ref_cast::RefCast; -use x509_parser::nom; -use x509::{ParsedExtension, X509Name, X509Certificate, TbsCertificate, X509Error, FromDer}; -use oid::OID_X509_EXT_SUBJECT_ALT_NAME as SUBJECT_ALT_NAME; - -use crate::listener::CertificateDer; -/// A type alias for [`Result`](std::result::Result) with the error type set to -/// [`Error`]. -pub type Result = std::result::Result; +use crate::mtls::{x509, oid, bigint, Name, Result, Error}; +use crate::request::{Request, FromRequest, Outcome}; +use crate::http::Status; /// A request guard for validated, verified client certificates. /// @@ -143,60 +100,42 @@ pub type Result = std::result::Result; /// ``` #[derive(Debug, PartialEq)] pub struct Certificate<'a> { - x509: X509Certificate<'a>, - data: &'a CertificateDer, + x509: x509::X509Certificate<'a>, + data: &'a CertificateDer<'a>, } -/// An X.509 Distinguished Name (DN) found in a [`Certificate`]. -/// -/// This type is a wrapper over [`x509::X509Name`] with convenient methods and -/// complete documentation. Should the data exposed by the inherent methods not -/// suffice, this type derefs to [`x509::X509Name`]. -#[repr(transparent)] -#[derive(Debug, PartialEq, RefCast)] -pub struct Name<'a>(X509Name<'a>); +pub use rustls::pki_types::CertificateDer; -/// An error returned by the [`Certificate`] request guard. -/// -/// To retrieve this error in a handler, use an `mtls::Result` -/// guard type: -/// -/// ```rust -/// # extern crate rocket; -/// # use rocket::get; -/// use rocket::mtls::{self, Certificate}; -/// -/// #[get("/auth")] -/// fn auth(cert: mtls::Result>) { -/// match cert { -/// Ok(cert) => { /* do something with the client cert */ }, -/// Err(e) => { /* do something with the error */ }, -/// } -/// } -/// ``` -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum Error { - /// The certificate chain presented by the client had no certificates. - Empty, - /// The certificate contained neither a subject nor a subjectAlt extension. - NoSubject, - /// There is no subject and the subjectAlt is not marked as critical. - NonCriticalSubjectAlt, - /// An error occurred while parsing the certificate. - Parse(X509Error), - /// The certificate parsed partially but is incomplete. - /// - /// If `Some(n)`, then `n` more bytes were expected. Otherwise, the number - /// of expected bytes is unknown. - Incomplete(Option), - /// The certificate contained `.0` bytes of trailing data. - Trailing(usize), +#[crate::async_trait] +impl<'r> FromRequest<'r> for Certificate<'r> { + type Error = Error; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + use crate::outcome::{try_outcome, IntoOutcome}; + + let certs = req.connection + .peer_certs + .as_ref() + .or_forward(Status::Unauthorized); + + let chain = try_outcome!(certs); + Certificate::parse(chain.inner()).or_error(Status::Unauthorized) + } } impl<'a> Certificate<'a> { - fn parse_one(raw: &[u8]) -> Result> { - let (left, x509) = X509Certificate::from_der(raw)?; + /// PRIVATE: For internal Rocket use only! + fn parse<'r>(chain: &'r [CertificateDer<'r>]) -> Result> { + let data = chain.first().ok_or_else(|| Error::Empty)?; + let x509 = Certificate::parse_one(&*data)?; + Ok(Certificate { x509, data }) + } + + fn parse_one(raw: &[u8]) -> Result> { + use oid::OID_X509_EXT_SUBJECT_ALT_NAME as SUBJECT_ALT_NAME; + use x509_parser::traits::FromDer; + + let (left, x509) = x509::X509Certificate::from_der(raw)?; if !left.is_empty() { return Err(Error::Trailing(left.len())); } @@ -204,7 +143,7 @@ impl<'a> Certificate<'a> { // Ensure we have a subject or a subjectAlt. if x509.subject().as_raw().is_empty() { if let Some(ext) = x509.extensions().iter().find(|e| e.oid == SUBJECT_ALT_NAME) { - if !matches!(ext.parsed_extension(), ParsedExtension::SubjectAlternativeName(..)) { + if let x509::ParsedExtension::SubjectAlternativeName(..) = ext.parsed_extension() { return Err(Error::NoSubject); } else if !ext.critical { return Err(Error::NonCriticalSubjectAlt); @@ -218,18 +157,10 @@ impl<'a> Certificate<'a> { } #[inline(always)] - fn inner(&self) -> &TbsCertificate<'a> { + fn inner(&self) -> &x509::TbsCertificate<'a> { &self.x509.tbs_certificate } - /// PRIVATE: For internal Rocket use only! - #[doc(hidden)] - pub fn parse(chain: &[CertificateDer]) -> Result> { - let data = chain.first().ok_or_else(|| Error::Empty)?; - let x509 = Certificate::parse_one(&data.0)?; - Ok(Certificate { x509, data }) - } - /// Returns the serial number of the X.509 certificate. /// /// # Example @@ -387,176 +318,14 @@ impl<'a> Certificate<'a> { /// } /// ``` pub fn as_bytes(&self) -> &'a [u8] { - &self.data.0 + &*self.data } } -impl<'a> Deref for Certificate<'a> { - type Target = TbsCertificate<'a>; +impl<'a> std::ops::Deref for Certificate<'a> { + type Target = x509::TbsCertificate<'a>; fn deref(&self) -> &Self::Target { self.inner() } } - -impl<'a> Name<'a> { - /// Returns the _first_ UTF-8 _string_ common name, if any. - /// - /// Note that common names need not be UTF-8 strings, or strings at all. - /// This method returns the first common name attribute that is. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::mtls::Certificate; - /// - /// #[get("/auth")] - /// fn auth(cert: Certificate<'_>) { - /// if let Some(name) = cert.subject().common_name() { - /// println!("Hello, {}!", name); - /// } - /// } - /// ``` - pub fn common_name(&self) -> Option<&'a str> { - self.common_names().next() - } - - /// Returns an iterator over all of the UTF-8 _string_ common names in - /// `self`. - /// - /// Note that common names need not be UTF-8 strings, or strings at all. - /// This method filters the common names in `self` to those that are. Use - /// the raw [`iter_common_name()`](#method.iter_common_name) to iterate over - /// all value types. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::mtls::Certificate; - /// - /// #[get("/auth")] - /// fn auth(cert: Certificate<'_>) { - /// for name in cert.issuer().common_names() { - /// println!("Issued by {}.", name); - /// } - /// } - /// ``` - pub fn common_names(&self) -> impl Iterator + '_ { - self.iter_by_oid(&oid::OID_X509_COMMON_NAME).filter_map(|n| n.as_str().ok()) - } - - /// Returns the _first_ UTF-8 _string_ email address, if any. - /// - /// Note that email addresses need not be UTF-8 strings, or strings at all. - /// This method returns the first email address attribute that is. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::mtls::Certificate; - /// - /// #[get("/auth")] - /// fn auth(cert: Certificate<'_>) { - /// if let Some(email) = cert.subject().email() { - /// println!("Hello, {}!", email); - /// } - /// } - /// ``` - pub fn email(&self) -> Option<&'a str> { - self.emails().next() - } - - /// Returns an iterator over all of the UTF-8 _string_ email addresses in - /// `self`. - /// - /// Note that email addresses need not be UTF-8 strings, or strings at all. - /// This method filters the email address in `self` to those that are. Use - /// the raw [`iter_email()`](#method.iter_email) to iterate over all value - /// types. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::mtls::Certificate; - /// - /// #[get("/auth")] - /// fn auth(cert: Certificate<'_>) { - /// for email in cert.subject().emails() { - /// println!("Reach me at: {}", email); - /// } - /// } - /// ``` - pub fn emails(&self) -> impl Iterator + '_ { - self.iter_by_oid(&oid::OID_PKCS9_EMAIL_ADDRESS).filter_map(|n| n.as_str().ok()) - } - - /// Returns `true` if `self` has no data. - /// - /// When this is the case for a `subject()`, the subject data can be found - /// in the `subjectAlt` [`extension()`](Certificate::extensions()). - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::mtls::Certificate; - /// - /// #[get("/auth")] - /// fn auth(cert: Certificate<'_>) { - /// let no_data = cert.subject().is_empty(); - /// } - /// ``` - pub fn is_empty(&self) -> bool { - self.0.as_raw().is_empty() - } -} - -impl<'a> Deref for Name<'a> { - type Target = X509Name<'a>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for Name<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Parse(e) => write!(f, "parse error: {}", e), - Error::Incomplete(_) => write!(f, "incomplete certificate data"), - Error::Trailing(n) => write!(f, "found {} trailing bytes", n), - Error::Empty => write!(f, "empty certificate chain"), - Error::NoSubject => write!(f, "empty subject without subjectAlt"), - Error::NonCriticalSubjectAlt => write!(f, "empty subject without critical subjectAlt"), - } - } -} - -impl From> for Error { - fn from(e: nom::Err) -> Self { - match e { - nom::Err::Incomplete(nom::Needed::Unknown) => Error::Incomplete(None), - nom::Err::Incomplete(nom::Needed::Size(n)) => Error::Incomplete(Some(n)), - nom::Err::Error(e) | nom::Err::Failure(e) => Error::Parse(e), - } - } -} - -impl std::error::Error for Error { - // fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - // match self { - // Error::Parse(e) => Some(e), - // _ => None - // } - // } -} diff --git a/core/lib/src/mtls/config.rs b/core/lib/src/mtls/config.rs new file mode 100644 index 0000000000..96fc12c9d1 --- /dev/null +++ b/core/lib/src/mtls/config.rs @@ -0,0 +1,212 @@ +use std::io; + +use figment::value::magic::{RelativePathBuf, Either}; +use serde::{Serialize, Deserialize}; + +/// Mutual TLS configuration. +/// +/// Configuration works in concert with the [`mtls`](crate::mtls) module, which +/// provides a request guard to validate, verify, and retrieve client +/// certificates in routes. +/// +/// By default, mutual TLS is disabled and client certificates are not required, +/// validated or verified. To enable mutual TLS, the `mtls` feature must be +/// enabled and support configured via two `tls.mutual` parameters: +/// +/// * `ca_certs` +/// +/// A required path to a PEM file or raw bytes to a DER-encoded X.509 TLS +/// certificate chain for the certificate authority to verify client +/// certificates against. When a path is configured in a file, such as +/// `Rocket.toml`, relative paths are interpreted as relative to the source +/// file's directory. +/// +/// * `mandatory` +/// +/// An optional boolean that control whether client authentication is +/// required. +/// +/// When `true`, client authentication is required. TLS connections where +/// the client does not present a certificate are immediately terminated. +/// When `false`, the client is not required to present a certificate. In +/// either case, if a certificate _is_ presented, it must be valid or the +/// connection is terminated. +/// +/// In a `Rocket.toml`, configuration might look like: +/// +/// ```toml +/// [default.tls.mutual] +/// ca_certs = "/ssl/ca_cert.pem" +/// mandatory = true # when absent, defaults to false +/// ``` +/// +/// Programmatically, configuration might look like: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::mtls::MtlsConfig; +/// use rocket::figment::providers::Serialized; +/// +/// #[launch] +/// fn rocket() -> _ { +/// let mtls = MtlsConfig::from_path("/ssl/ca_cert.pem"); +/// rocket::custom(rocket::Config::figment().merge(("tls.mutual", mtls))) +/// } +/// ``` +/// +/// Once mTLS is configured, the [`mtls::Certificate`](crate::mtls::Certificate) +/// request guard can be used to retrieve client certificates in routes. +#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)] +pub struct MtlsConfig { + /// Path to a PEM file with, or raw bytes for, DER-encoded Certificate + /// Authority certificates which will be used to verify client-presented + /// certificates. + // TODO: Support more than one CA root. + pub(crate) ca_certs: Either>, + /// Whether the client is required to present a certificate. + /// + /// When `true`, the client is required to present a valid certificate to + /// proceed with TLS. When `false`, the client is not required to present a + /// certificate. In either case, if a certificate _is_ presented, it must be + /// valid or the connection is terminated. + #[serde(default)] + #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] + pub mandatory: bool, +} + +impl MtlsConfig { + /// Constructs a `MtlsConfig` from a path to a PEM file with a certificate + /// authority `ca_certs` DER-encoded X.509 TLS certificate chain. This + /// method does no validation; it simply creates a structure suitable for + /// passing into a [`TlsConfig`]. + /// + /// These certificates will be used to verify client-presented certificates + /// in TLS connections. + /// + /// # Example + /// + /// ```rust + /// use rocket::mtls::MtlsConfig; + /// + /// let tls_config = MtlsConfig::from_path("/ssl/ca_certs.pem"); + /// ``` + pub fn from_path>(ca_certs: C) -> Self { + MtlsConfig { + ca_certs: Either::Left(ca_certs.as_ref().to_path_buf().into()), + mandatory: Default::default() + } + } + + /// Constructs a `MtlsConfig` from a byte buffer to a certificate authority + /// `ca_certs` DER-encoded X.509 TLS certificate chain. This method does no + /// validation; it simply creates a structure suitable for passing into a + /// [`TlsConfig`]. + /// + /// These certificates will be used to verify client-presented certificates + /// in TLS connections. + /// + /// # Example + /// + /// ```rust + /// use rocket::mtls::MtlsConfig; + /// + /// # let ca_certs_buf = &[]; + /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf); + /// ``` + pub fn from_bytes(ca_certs: &[u8]) -> Self { + MtlsConfig { + ca_certs: Either::Right(ca_certs.to_vec()), + mandatory: Default::default() + } + } + + /// Sets whether client authentication is required. Disabled by default. + /// + /// When `true`, client authentication will be required. TLS connections + /// where the client does not present a certificate will be immediately + /// terminated. When `false`, the client is not required to present a + /// certificate. In either case, if a certificate _is_ presented, it must be + /// valid or the connection is terminated. + /// + /// # Example + /// + /// ```rust + /// use rocket::mtls::MtlsConfig; + /// + /// # let ca_certs_buf = &[]; + /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf).mandatory(true); + /// ``` + pub fn mandatory(mut self, mandatory: bool) -> Self { + self.mandatory = mandatory; + self + } + + /// Returns the value of the `ca_certs` parameter. + /// # Example + /// + /// ```rust + /// use rocket::mtls::MtlsConfig; + /// + /// # let ca_certs_buf = &[]; + /// let mtls_config = MtlsConfig::from_bytes(ca_certs_buf).mandatory(true); + /// assert_eq!(mtls_config.ca_certs().unwrap_right(), ca_certs_buf); + /// ``` + pub fn ca_certs(&self) -> either::Either { + match &self.ca_certs { + Either::Left(path) => either::Either::Left(path.relative()), + Either::Right(bytes) => either::Either::Right(&bytes), + } + } + + #[inline(always)] + pub fn ca_certs_reader(&self) -> io::Result> { + crate::tls::config::to_reader(&self.ca_certs) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use figment::{Figment, providers::{Toml, Format}}; + + use crate::mtls::MtlsConfig; + + #[test] + fn test_mtls_config() { + figment::Jail::expect_with(|jail| { + jail.create_file("MTLS.toml", r#" + certs = "/ssl/cert.pem" + key = "/ssl/key.pem" + "#)?; + + let figment = || Figment::from(Toml::file("MTLS.toml")); + figment().extract::().expect_err("no ca"); + + jail.create_file("MTLS.toml", r#" + ca_certs = "/ssl/ca.pem" + "#)?; + + let mtls: MtlsConfig = figment().extract()?; + assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem")); + assert!(!mtls.mandatory); + + jail.create_file("MTLS.toml", r#" + ca_certs = "/ssl/ca.pem" + mandatory = true + "#)?; + + let mtls: MtlsConfig = figment().extract()?; + assert_eq!(mtls.ca_certs().unwrap_left(), Path::new("/ssl/ca.pem")); + assert!(mtls.mandatory); + + jail.create_file("MTLS.toml", r#" + ca_certs = "relative/ca.pem" + "#)?; + + let mtls: MtlsConfig = figment().extract()?; + assert_eq!(mtls.ca_certs().unwrap_left(), jail.directory().join("relative/ca.pem")); + + Ok(()) + }); + } +} diff --git a/core/lib/src/mtls/error.rs b/core/lib/src/mtls/error.rs new file mode 100644 index 0000000000..56b8d01ee1 --- /dev/null +++ b/core/lib/src/mtls/error.rs @@ -0,0 +1,74 @@ +use std::fmt; +use std::num::NonZeroUsize; + +use crate::mtls::x509::{self, nom}; + +/// An error returned by the [`Certificate`] request guard. +/// +/// To retrieve this error in a handler, use an `mtls::Result` +/// guard type: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::get; +/// use rocket::mtls::{self, Certificate}; +/// +/// #[get("/auth")] +/// fn auth(cert: mtls::Result>) { +/// match cert { +/// Ok(cert) => { /* do something with the client cert */ }, +/// Err(e) => { /* do something with the error */ }, +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum Error { + /// The certificate chain presented by the client had no certificates. + Empty, + /// The certificate contained neither a subject nor a subjectAlt extension. + NoSubject, + /// There is no subject and the subjectAlt is not marked as critical. + NonCriticalSubjectAlt, + /// An error occurred while parsing the certificate. + Parse(x509::X509Error), + /// The certificate parsed partially but is incomplete. + /// + /// If `Some(n)`, then `n` more bytes were expected. Otherwise, the number + /// of expected bytes is unknown. + Incomplete(Option), + /// The certificate contained `.0` bytes of trailing data. + Trailing(usize), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Parse(e) => write!(f, "parse error: {}", e), + Error::Incomplete(_) => write!(f, "incomplete certificate data"), + Error::Trailing(n) => write!(f, "found {} trailing bytes", n), + Error::Empty => write!(f, "empty certificate chain"), + Error::NoSubject => write!(f, "empty subject without subjectAlt"), + Error::NonCriticalSubjectAlt => write!(f, "empty subject without critical subjectAlt"), + } + } +} + +impl From> for Error { + fn from(e: nom::Err) -> Self { + match e { + nom::Err::Incomplete(nom::Needed::Unknown) => Error::Incomplete(None), + nom::Err::Incomplete(nom::Needed::Size(n)) => Error::Incomplete(Some(n)), + nom::Err::Error(e) | nom::Err::Failure(e) => Error::Parse(e), + } + } +} + +impl std::error::Error for Error { + // fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // match self { + // Error::Parse(e) => Some(e), + // _ => None + // } + // } +} diff --git a/core/lib/src/mtls/mod.rs b/core/lib/src/mtls/mod.rs new file mode 100644 index 0000000000..10ce6dc055 --- /dev/null +++ b/core/lib/src/mtls/mod.rs @@ -0,0 +1,56 @@ +//! Support for mutual TLS client certificates. +//! +//! For details on how to configure mutual TLS, see +//! [`MutualTls`](crate::config::MutualTls) and the [TLS +//! guide](https://rocket.rs/master/guide/configuration/#tls). See +//! [`Certificate`] for a request guard that validated, verifies, and retrieves +//! client certificates. + +pub mod oid { + //! Lower-level OID types re-exported from + //! [`oid_registry`](https://docs.rs/oid-registry/0.4) and + //! [`der-parser`](https://docs.rs/der-parser/7). + + pub use x509_parser::oid_registry::*; + pub use x509_parser::objects::*; +} + +pub mod bigint { + //! Signed and unsigned big integer types re-exported from + //! [`num_bigint`](https://docs.rs/num-bigint/0.4). + pub use x509_parser::der_parser::num_bigint::*; +} + +pub mod x509 { + //! Lower-level X.509 types re-exported from + //! [`x509_parser`](https://docs.rs/x509-parser/0.13). + //! + //! Lack of documentation is directly inherited from the source crate. + //! Prefer to use Rocket's wrappers when possible. + + pub(crate) use x509_parser::nom; + pub use x509_parser::certificate::*; + pub use x509_parser::cri_attributes::*; + pub use x509_parser::error::*; + pub use x509_parser::extensions::*; + pub use x509_parser::revocation_list::*; + pub use x509_parser::time::*; + pub use x509_parser::x509::*; + pub use x509_parser::der_parser::der; + pub use x509_parser::der_parser::ber; + pub use x509_parser::traits::*; +} + +mod certificate; +mod error; +mod name; +mod config; + +pub use error::Error; +pub use name::Name; +pub use config::MtlsConfig; +pub use certificate::{Certificate, CertificateDer}; + +/// A type alias for [`Result`](std::result::Result) with the error type set to +/// [`Error`]. +pub type Result = std::result::Result; diff --git a/core/lib/src/mtls/name.rs b/core/lib/src/mtls/name.rs new file mode 100644 index 0000000000..c6198ace36 --- /dev/null +++ b/core/lib/src/mtls/name.rs @@ -0,0 +1,146 @@ +use std::fmt; +use std::ops::Deref; + +use ref_cast::RefCast; + +use crate::mtls::x509::X509Name; +use crate::mtls::oid; + +/// An X.509 Distinguished Name (DN) found in a [`Certificate`]. +/// +/// This type is a wrapper over [`x509::X509Name`] with convenient methods and +/// complete documentation. Should the data exposed by the inherent methods not +/// suffice, this type derefs to [`x509::X509Name`]. +#[repr(transparent)] +#[derive(Debug, PartialEq, RefCast)] +pub struct Name<'a>(X509Name<'a>); + +impl<'a> Name<'a> { + /// Returns the _first_ UTF-8 _string_ common name, if any. + /// + /// Note that common names need not be UTF-8 strings, or strings at all. + /// This method returns the first common name attribute that is. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::mtls::Certificate; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// if let Some(name) = cert.subject().common_name() { + /// println!("Hello, {}!", name); + /// } + /// } + /// ``` + pub fn common_name(&self) -> Option<&'a str> { + self.common_names().next() + } + + /// Returns an iterator over all of the UTF-8 _string_ common names in + /// `self`. + /// + /// Note that common names need not be UTF-8 strings, or strings at all. + /// This method filters the common names in `self` to those that are. Use + /// the raw [`iter_common_name()`](#method.iter_common_name) to iterate over + /// all value types. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::mtls::Certificate; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// for name in cert.issuer().common_names() { + /// println!("Issued by {}.", name); + /// } + /// } + /// ``` + pub fn common_names(&self) -> impl Iterator + '_ { + self.iter_by_oid(&oid::OID_X509_COMMON_NAME).filter_map(|n| n.as_str().ok()) + } + + /// Returns the _first_ UTF-8 _string_ email address, if any. + /// + /// Note that email addresses need not be UTF-8 strings, or strings at all. + /// This method returns the first email address attribute that is. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::mtls::Certificate; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// if let Some(email) = cert.subject().email() { + /// println!("Hello, {}!", email); + /// } + /// } + /// ``` + pub fn email(&self) -> Option<&'a str> { + self.emails().next() + } + + /// Returns an iterator over all of the UTF-8 _string_ email addresses in + /// `self`. + /// + /// Note that email addresses need not be UTF-8 strings, or strings at all. + /// This method filters the email address in `self` to those that are. Use + /// the raw [`iter_email()`](#method.iter_email) to iterate over all value + /// types. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::mtls::Certificate; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// for email in cert.subject().emails() { + /// println!("Reach me at: {}", email); + /// } + /// } + /// ``` + pub fn emails(&self) -> impl Iterator + '_ { + self.iter_by_oid(&oid::OID_PKCS9_EMAIL_ADDRESS).filter_map(|n| n.as_str().ok()) + } + + /// Returns `true` if `self` has no data. + /// + /// When this is the case for a `subject()`, the subject data can be found + /// in the `subjectAlt` [`extension()`](Certificate::extensions()). + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::mtls::Certificate; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// let no_data = cert.subject().is_empty(); + /// } + /// ``` + pub fn is_empty(&self) -> bool { + self.0.as_raw().is_empty() + } +} + +impl<'a> Deref for Name<'a> { + type Target = X509Name<'a>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for Name<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/core/lib/src/phase.rs b/core/lib/src/phase.rs index 3b6ca870c3..b38deeeaf6 100644 --- a/core/lib/src/phase.rs +++ b/core/lib/src/phase.rs @@ -1,6 +1,7 @@ use state::TypeMap; use figment::Figment; +use crate::listener::Endpoint; use crate::{Catcher, Config, Rocket, Route, Shutdown}; use crate::router::Router; use crate::fairing::Fairings; @@ -113,5 +114,6 @@ phases! { pub(crate) config: Config, pub(crate) state: TypeMap![Send + Sync], pub(crate) shutdown: Shutdown, + pub(crate) endpoint: Endpoint, } } diff --git a/core/lib/src/request/atomic_method.rs b/core/lib/src/request/atomic_method.rs new file mode 100644 index 0000000000..6d49f603d5 --- /dev/null +++ b/core/lib/src/request/atomic_method.rs @@ -0,0 +1,43 @@ +use crate::http::Method; + +pub struct AtomicMethod(ref_swap::RefSwap<'static, Method>); + +#[inline(always)] +const fn makeref(method: Method) -> &'static Method { + match method { + Method::Get => &Method::Get, + Method::Put => &Method::Put, + Method::Post => &Method::Post, + Method::Delete => &Method::Delete, + Method::Options => &Method::Options, + Method::Head => &Method::Head, + Method::Trace => &Method::Trace, + Method::Connect => &Method::Connect, + Method::Patch => &Method::Patch, + } +} + +impl AtomicMethod { + pub fn new(value: Method) -> Self { + Self(ref_swap::RefSwap::new(makeref(value))) + } + + pub fn load(&self) -> Method { + *self.0.load(std::sync::atomic::Ordering::Acquire) + } + + pub fn set(&mut self, new: Method) { + *self = Self::new(new); + } + + pub fn store(&self, new: Method) { + self.0.store(makeref(new), std::sync::atomic::Ordering::Release) + } +} + +impl Clone for AtomicMethod { + fn clone(&self) -> Self { + let inner = self.0.load(std::sync::atomic::Ordering::Acquire); + Self(ref_swap::RefSwap::new(inner)) + } +} diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index c95a427f09..279b35cd00 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -1,12 +1,13 @@ use std::convert::Infallible; use std::fmt::Debug; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use crate::{Request, Route}; use crate::outcome::{self, IntoOutcome, Outcome::*}; use crate::http::uri::{Host, Origin}; use crate::http::{Status, ContentType, Accept, Method, ProxyProto, CookieJar}; +use crate::listener::Endpoint; /// Type alias for the `Outcome` of a `FromRequest` conversion. pub type Outcome = outcome::Outcome; @@ -486,14 +487,22 @@ impl<'r> FromRequest<'r> for ProxyProto<'r> { } #[crate::async_trait] -impl<'r> FromRequest<'r> for SocketAddr { +impl<'r> FromRequest<'r> for &'r Endpoint { type Error = Infallible; async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.remote() { - Some(addr) => Success(addr), - None => Forward(Status::InternalServerError) - } + request.remote().or_forward(Status::InternalServerError) + } +} + +#[crate::async_trait] +impl<'r> FromRequest<'r> for std::net::SocketAddr { + type Error = Infallible; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + request.remote() + .and_then(|r| r.tcp()) + .or_forward(Status::InternalServerError) } } diff --git a/core/lib/src/request/mod.rs b/core/lib/src/request/mod.rs index fe565b2e2a..0393f96b51 100644 --- a/core/lib/src/request/mod.rs +++ b/core/lib/src/request/mod.rs @@ -3,6 +3,7 @@ mod request; mod from_param; mod from_request; +mod atomic_method; #[cfg(test)] mod tests; @@ -15,6 +16,7 @@ pub use self::from_param::{FromParam, FromSegments}; pub use crate::response::flash::FlashMessage; pub(crate) use self::request::ConnectionMeta; +pub(crate) use self::atomic_method::AtomicMethod; crate::export! { /// Store and immediately retrieve a vector-like value `$v` (`String` or diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 1d380a9767..a02dff3a4e 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -1,22 +1,24 @@ use std::fmt; use std::ops::RangeFrom; -use std::{future::Future, borrow::Cow, sync::Arc}; -use std::net::{IpAddr, SocketAddr}; +use std::sync::{Arc, atomic::Ordering}; +use std::borrow::Cow; +use std::future::Future; +use std::net::IpAddr; use yansi::Paint; use state::{TypeMap, InitCell}; use futures::future::BoxFuture; -use atomic::{Atomic, Ordering}; +use ref_swap::OptionRefSwap; use crate::{Rocket, Route, Orbit}; -use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; +use crate::request::{FromParam, FromSegments, FromRequest, Outcome, AtomicMethod}; use crate::form::{self, ValueField, FromForm}; use crate::data::Limits; -use crate::http::{hyper, Method, Header, HeaderMap, ProxyProto}; -use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie}; -use crate::http::private::Certificates; +use crate::http::ProxyProto; +use crate::http::{Method, Header, HeaderMap, ContentType, Accept, MediaType, CookieJar, Cookie}; use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority}; +use crate::listener::{Certificates, Endpoint, Connection}; /// The type of an incoming web request. /// @@ -24,26 +26,37 @@ use crate::http::uri::{fmt::Path, Origin, Segments, Host, Authority}; /// should likely only be used when writing [`FromRequest`] implementations. It /// contains all of the information for a given web request except for the body /// data. This includes the HTTP method, URI, cookies, headers, and more. +#[derive(Clone)] pub struct Request<'r> { - method: Atomic, + method: AtomicMethod, uri: Origin<'r>, headers: HeaderMap<'r>, + pub(crate) errors: Vec, pub(crate) connection: ConnectionMeta, pub(crate) state: RequestState<'r>, } /// Information derived from an incoming connection, if any. -#[derive(Clone)] +#[derive(Clone, Default)] pub(crate) struct ConnectionMeta { - pub remote: Option, + pub peer_address: Option>, #[cfg_attr(not(feature = "mtls"), allow(dead_code))] - pub client_certificates: Option, + pub peer_certs: Option>>, +} + +impl From<&C> for ConnectionMeta { + fn from(conn: &C) -> Self { + ConnectionMeta { + peer_address: conn.peer_address().ok().map(Arc::new), + peer_certs: conn.peer_certificates().map(|c| c.into_owned()).map(Arc::new), + } + } } /// Information derived from the request. pub(crate) struct RequestState<'r> { pub rocket: &'r Rocket, - pub route: Atomic>, + pub route: OptionRefSwap<'r, Route>, pub cookies: CookieJar<'r>, pub accept: InitCell>, pub content_type: InitCell>, @@ -51,23 +64,11 @@ pub(crate) struct RequestState<'r> { pub host: Option>, } -impl Request<'_> { - pub(crate) fn clone(&self) -> Self { - Request { - method: Atomic::new(self.method()), - uri: self.uri.clone(), - headers: self.headers.clone(), - connection: self.connection.clone(), - state: self.state.clone(), - } - } -} - -impl RequestState<'_> { +impl Clone for RequestState<'_> { fn clone(&self) -> Self { RequestState { rocket: self.rocket, - route: Atomic::new(self.route.load(Ordering::Acquire)), + route: OptionRefSwap::new(self.route.load(Ordering::Acquire)), cookies: self.cookies.clone(), accept: self.accept.clone(), content_type: self.content_type.clone(), @@ -87,15 +88,13 @@ impl<'r> Request<'r> { ) -> Request<'r> { Request { uri, - method: Atomic::new(method), + method: AtomicMethod::new(method), headers: HeaderMap::new(), - connection: ConnectionMeta { - remote: None, - client_certificates: None, - }, + errors: Vec::new(), + connection: ConnectionMeta::default(), state: RequestState { rocket, - route: Atomic::new(None), + route: OptionRefSwap::new(None), cookies: CookieJar::new(None, rocket), accept: InitCell::new(), content_type: InitCell::new(), @@ -120,7 +119,7 @@ impl<'r> Request<'r> { /// ``` #[inline(always)] pub fn method(&self) -> Method { - self.method.load(Ordering::Acquire) + self.method.load() } /// Set the method of `self` to `method`. @@ -140,7 +139,7 @@ impl<'r> Request<'r> { /// ``` #[inline(always)] pub fn set_method(&mut self, method: Method) { - self._set_method(method); + self.method.set(method); } /// Borrow the [`Origin`] URI from `self`. @@ -324,20 +323,20 @@ impl<'r> Request<'r> { /// /// assert_eq!(request.remote(), None); /// - /// let localhost = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000).into(); + /// let localhost = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000); /// request.set_remote(localhost); - /// assert_eq!(request.remote(), Some(localhost)); + /// assert_eq!(request.remote().unwrap(), &localhost); /// ``` #[inline(always)] - pub fn remote(&self) -> Option { - self.connection.remote + pub fn remote(&self) -> Option<&Endpoint> { + self.connection.peer_address.as_deref() } /// Sets the remote address of `self` to `address`. /// /// # Example /// - /// Set the remote address to be 127.0.0.1:8000: + /// Set the remote address to be 127.0.0.1:8111: /// /// ```rust /// use std::net::{SocketAddrV4, Ipv4Addr}; @@ -347,13 +346,13 @@ impl<'r> Request<'r> { /// /// assert_eq!(request.remote(), None); /// - /// let localhost = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000).into(); + /// let localhost = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8111); /// request.set_remote(localhost); - /// assert_eq!(request.remote(), Some(localhost)); + /// assert_eq!(request.remote().unwrap(), &localhost); /// ``` #[inline(always)] - pub fn set_remote(&mut self, address: SocketAddr) { - self.connection.remote = Some(address); + pub fn set_remote>(&mut self, address: A) { + self.connection.peer_address = Some(Arc::new(address.into())); } /// Returns the IP address of the configured @@ -489,25 +488,26 @@ impl<'r> Request<'r> { /// /// ```rust /// # use rocket::http::Header; - /// # use std::net::{SocketAddr, IpAddr, Ipv4Addr}; /// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap(); /// # let mut req = c.get("/"); /// # let request = req.inner_mut(); + /// # use std::net::{SocketAddrV4, Ipv4Addr}; /// /// // starting without an "X-Real-IP" header or remote address /// assert!(request.client_ip().is_none()); /// /// // add a remote address; this is done by Rocket automatically - /// request.set_remote("127.0.0.1:8000".parse().unwrap()); - /// assert_eq!(request.client_ip(), Some("127.0.0.1".parse().unwrap())); + /// let localhost_9190 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9190); + /// request.set_remote(localhost_9190); + /// assert_eq!(request.client_ip().unwrap(), Ipv4Addr::LOCALHOST); /// /// // now with an X-Real-IP header, the default value for `ip_header`. /// request.add_header(Header::new("X-Real-IP", "8.8.8.8")); - /// assert_eq!(request.client_ip(), Some("8.8.8.8".parse().unwrap())); + /// assert_eq!(request.client_ip().unwrap(), Ipv4Addr::new(8, 8, 8, 8)); /// ``` #[inline] pub fn client_ip(&self) -> Option { - self.real_ip().or_else(|| self.remote().map(|r| r.ip())) + self.real_ip().or_else(|| Some(self.remote()?.tcp()?.ip())) } /// Returns a wrapped borrow to the cookies in `self`. @@ -691,7 +691,7 @@ impl<'r> Request<'r> { if self.method().supports_payload() { self.content_type().map(|ct| ct.media_type()) } else { - // FIXME: Should we be using `accept_first` or `preferred`? Or + // TODO: Should we be using `accept_first` or `preferred`? Or // should we be checking neither and instead pass things through // where the client accepts the thing at all? self.accept() @@ -1056,11 +1056,9 @@ impl<'r> Request<'r> { self.state.route.store(Some(route), Ordering::Release) } - /// Set the method of `self`, even when `self` is a shared reference. Used - /// during routing to override methods for re-routing. #[inline(always)] pub(crate) fn _set_method(&self, method: Method) { - self.method.store(method, Ordering::Release) + self.method.store(method) } pub(crate) fn cookies_mut(&mut self) -> &mut CookieJar<'r> { @@ -1070,18 +1068,28 @@ impl<'r> Request<'r> { /// Convert from Hyper types into a Rocket Request. pub(crate) fn from_hyp( rocket: &'r Rocket, - hyper: &'r hyper::request::Parts, - connection: Option, - ) -> Result, BadRequest<'r>> { + hyper: &'r hyper::http::request::Parts, + connection: ConnectionMeta, + ) -> Result, Request<'r>> { // Keep track of parsing errors; emit a `BadRequest` if any exist. let mut errors = vec![]; // Ensure that the method is known. TODO: Allow made-up methods? - let method = Method::from_hyp(&hyper.method) - .unwrap_or_else(|| { - errors.push(Kind::BadMethod(&hyper.method)); + let method = match hyper.method { + hyper::Method::GET => Method::Get, + hyper::Method::PUT => Method::Put, + hyper::Method::POST => Method::Post, + hyper::Method::DELETE => Method::Delete, + hyper::Method::OPTIONS => Method::Options, + hyper::Method::HEAD => Method::Head, + hyper::Method::TRACE => Method::Trace, + hyper::Method::CONNECT => Method::Connect, + hyper::Method::PATCH => Method::Patch, + _ => { + errors.push(RequestError::BadMethod(hyper.method.clone())); Method::Get - }); + } + }; // TODO: Keep around not just the path/query, but the rest, if there? let uri = hyper.uri.path_and_query() @@ -1100,20 +1108,20 @@ impl<'r> Request<'r> { Origin::new(uri.path(), uri.query().map(Cow::Borrowed)) }) .unwrap_or_else(|| { - errors.push(Kind::InvalidUri(&hyper.uri)); + errors.push(RequestError::InvalidUri(hyper.uri.clone())); Origin::ROOT }); // Construct the request object; fill in metadata and headers next. let mut request = Request::new(rocket, method, uri); + request.errors = errors; // Set the passed in connection metadata. - if let Some(connection) = connection { - request.connection = connection; - } + request.connection = connection; // Determine + set host. On HTTP < 2, use the `HOST` header. Otherwise, // use the `:authority` pseudo-header which hyper makes part of the URI. + // TODO: Use an `InitCell` to compute this later. request.state.host = if hyper.version < hyper::Version::HTTP_2 { hyper.headers.get("host").and_then(|h| Host::parse_bytes(h.as_bytes()).ok()) } else { @@ -1122,9 +1130,8 @@ impl<'r> Request<'r> { // Set the request cookies, if they exist. for header in hyper.headers.get_all("Cookie") { - let raw_str = match std::str::from_utf8(header.as_bytes()) { - Ok(string) => string, - Err(_) => continue + let Ok(raw_str) = std::str::from_utf8(header.as_bytes()) else { + continue }; for cookie_str in raw_str.split(';').map(|s| s.trim()) { @@ -1137,43 +1144,33 @@ impl<'r> Request<'r> { // Set the rest of the headers. This is rather unfortunate and slow. for (name, value) in hyper.headers.iter() { // FIXME: This is rather unfortunate. Header values needn't be UTF8. - let value = match std::str::from_utf8(value.as_bytes()) { - Ok(value) => value, - Err(_) => { - warn!("Header '{}' contains invalid UTF-8", name); - warn_!("Rocket only supports UTF-8 header values. Dropping header."); - continue; - } + let Ok(value) = std::str::from_utf8(value.as_bytes()) else { + warn!("Header '{}' contains invalid UTF-8", name); + warn_!("Rocket only supports UTF-8 header values. Dropping header."); + continue; }; request.add_header(Header::new(name.as_str(), value)); } - if errors.is_empty() { - Ok(request) - } else { - Err(BadRequest { request, errors }) + match request.errors.is_empty() { + true => Ok(request), + false => Err(request), } } } -#[derive(Debug)] -pub(crate) struct BadRequest<'r> { - pub request: Request<'r>, - pub errors: Vec>, -} - -#[derive(Debug)] -pub(crate) enum Kind<'r> { - InvalidUri(&'r hyper::Uri), - BadMethod(&'r hyper::Method), +#[derive(Debug, Clone)] +pub(crate) enum RequestError { + InvalidUri(hyper::Uri), + BadMethod(hyper::Method), } -impl fmt::Display for Kind<'_> { +impl fmt::Display for RequestError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Kind::InvalidUri(u) => write!(f, "invalid origin URI: {}", u), - Kind::BadMethod(m) => write!(f, "invalid or unrecognized method: {}", m), + RequestError::InvalidUri(u) => write!(f, "invalid origin URI: {}", u), + RequestError::BadMethod(m) => write!(f, "invalid or unrecognized method: {}", m), } } } @@ -1181,8 +1178,8 @@ impl fmt::Display for Kind<'_> { impl fmt::Debug for Request<'_> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("Request") - .field("method", &self.method) - .field("uri", &self.uri) + .field("method", &self.method()) + .field("uri", &self.uri()) .field("headers", &self.headers()) .field("remote", &self.remote()) .field("cookies", &self.cookies()) diff --git a/core/lib/src/request/tests.rs b/core/lib/src/request/tests.rs index a349aeda39..5af4b8543c 100644 --- a/core/lib/src/request/tests.rs +++ b/core/lib/src/request/tests.rs @@ -1,14 +1,16 @@ use std::collections::HashMap; -use crate::Request; +use crate::request::{Request, ConnectionMeta}; use crate::local::blocking::Client; -use crate::http::hyper; macro_rules! assert_headers { ($($key:expr => [$($value:expr),+]),+) => ({ // Create a new Hyper request. Add all of the passed in headers. let mut req = hyper::Request::get("/test").body(()).unwrap(); - $($(req.headers_mut().append($key, hyper::HeaderValue::from_str($value).unwrap());)+)+ + $($( + req.headers_mut() + .append($key, hyper::header::HeaderValue::from_str($value).unwrap()); + )+)+ // Build up what we expect the headers to actually be. let mut expected = HashMap::new(); @@ -17,7 +19,8 @@ macro_rules! assert_headers { // Create a valid `Rocket` and convert the hyper req to a Rocket one. let client = Client::debug_with(vec![]).unwrap(); let hyper = req.into_parts().0; - let req = Request::from_hyp(client.rocket(), &hyper, None).unwrap(); + let meta = ConnectionMeta::default(); + let req = Request::from_hyp(client.rocket(), &hyper, meta).unwrap(); // Dispatch the request and check that the headers match. let actual_headers = req.headers(); diff --git a/core/lib/src/response/response.rs b/core/lib/src/response/response.rs index 588497e1c2..4d399e9174 100644 --- a/core/lib/src/response/response.rs +++ b/core/lib/src/response/response.rs @@ -1,7 +1,6 @@ use std::{fmt, str}; use std::borrow::Cow; use std::collections::HashMap; -use std::pin::Pin; use tokio::io::{AsyncRead, AsyncSeek}; @@ -146,19 +145,18 @@ impl<'r> Builder<'r> { /// potentially different values to be present in the `Response`. /// /// The type of `header` can be any type that implements `Into

    `. - /// This includes `Header` itself, [`ContentType`](crate::http::ContentType) and - /// [hyper::header types](crate::http::hyper::header). + /// This includes `Header` itself, [`ContentType`](crate::http::ContentType) + /// and [`Accept`](crate::http::Accept). /// /// # Example /// /// ```rust /// use rocket::Response; - /// use rocket::http::Header; - /// use rocket::http::hyper::header::ACCEPT; + /// use rocket::http::{Header, Accept}; /// /// let response = Response::build() - /// .header_adjoin(Header::new(ACCEPT.as_str(), "application/json")) - /// .header_adjoin(Header::new(ACCEPT.as_str(), "text/plain")) + /// .header_adjoin(Header::new("Accept", "application/json")) + /// .header_adjoin(Accept::XML) /// .finalize(); /// /// assert_eq!(response.headers().get("Accept").count(), 2); @@ -287,7 +285,7 @@ impl<'r> Builder<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + /// async fn io(self: Box, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -488,7 +486,7 @@ pub struct Response<'r> { status: Option, headers: HeaderMap<'r>, body: Body<'r>, - upgrade: HashMap, Pin>>, + upgrade: HashMap, Box>, } impl<'r> Response<'r> { @@ -700,23 +698,22 @@ impl<'r> Response<'r> { /// name `header.name`, another header with the same name and value /// `header.value` is added. The type of `header` can be any type that /// implements `Into
    `. This includes `Header` itself, - /// [`ContentType`](crate::http::ContentType) and [`hyper::header` - /// types](crate::http::hyper::header). + /// [`ContentType`](crate::http::ContentType), + /// [`Accept`](crate::http::Accept). /// /// # Example /// /// ```rust /// use rocket::Response; - /// use rocket::http::Header; - /// use rocket::http::hyper::header::ACCEPT; + /// use rocket::http::{Header, Accept}; /// /// let mut response = Response::new(); - /// response.adjoin_header(Header::new(ACCEPT.as_str(), "application/json")); - /// response.adjoin_header(Header::new(ACCEPT.as_str(), "text/plain")); + /// response.adjoin_header(Accept::JSON); + /// response.adjoin_header(Header::new("Accept", "text/plain")); /// /// let mut accept_headers = response.headers().iter(); - /// assert_eq!(accept_headers.next(), Some(Header::new(ACCEPT.as_str(), "application/json"))); - /// assert_eq!(accept_headers.next(), Some(Header::new(ACCEPT.as_str(), "text/plain"))); + /// assert_eq!(accept_headers.next(), Some(Header::new("Accept", "application/json"))); + /// assert_eq!(accept_headers.next(), Some(Header::new("Accept", "text/plain"))); /// assert_eq!(accept_headers.next(), None); /// ``` #[inline(always)] @@ -801,10 +798,10 @@ impl<'r> Response<'r> { /// the comma-separated protocols any of the strings in `I`. Returns /// `Ok(None)` if `self` doesn't support any kind of upgrade. Returns /// `Err(_)` if `protocols` is non-empty but no match was found in `self`. - pub(crate) fn take_upgrade>( + pub(crate) fn search_upgrades<'a, I: Iterator>( &mut self, protocols: I - ) -> Result, Pin>)>, ()> { + ) -> Result, Box)>, ()> { if self.upgrade.is_empty() { return Ok(None); } @@ -839,7 +836,7 @@ impl<'r> Response<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + /// async fn io(self: Box, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -854,7 +851,7 @@ impl<'r> Response<'r> { /// assert!(response.upgrade("raw-echo").is_some()); /// # }) /// ``` - pub fn upgrade(&mut self, proto: &str) -> Option> { + pub fn upgrade(&mut self, proto: &str) -> Option<&mut (dyn IoHandler + 'r)> { self.upgrade.get_mut(proto.as_uncased()).map(|h| h.as_mut()) } @@ -972,7 +969,7 @@ impl<'r> Response<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + /// async fn io(self: Box, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -990,7 +987,7 @@ impl<'r> Response<'r> { pub fn add_upgrade(&mut self, protocol: N, handler: H) where N: Into>, H: IoHandler + 'r { - self.upgrade.insert(protocol.into(), Box::pin(handler)); + self.upgrade.insert(protocol.into(), Box::new(handler)); } /// Sets the body's maximum chunk size to `size` bytes. diff --git a/core/lib/src/response/stream/sse.rs b/core/lib/src/response/stream/sse.rs index 27044e27af..398596ccbf 100644 --- a/core/lib/src/response/stream/sse.rs +++ b/core/lib/src/response/stream/sse.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; use tokio::io::AsyncRead; -use tokio::time::Duration; -use futures::stream::{self, Stream, StreamExt}; -use futures::future::ready; +use tokio::time::{interval, Duration}; +use futures::{stream::{self, Stream}, future::Either}; +use tokio_stream::{StreamExt, wrappers::IntervalStream}; use crate::request::Request; use crate::response::{self, Response, Responder, stream::{ReaderStream, RawLinedEvent}}; @@ -336,7 +336,7 @@ impl Event { Some(RawLinedEvent::raw("")), ]; - stream::iter(events).filter_map(ready) + stream::iter(events).filter_map(|x| x) } } @@ -528,25 +528,19 @@ impl> EventStream { self } - fn heartbeat_stream(&self) -> Option> { - use tokio::time::interval; - use tokio_stream::wrappers::IntervalStream; - + fn heartbeat_stream(&self) -> impl Stream { self.heartbeat .map(|beat| IntervalStream::new(interval(beat))) .map(|stream| stream.map(|_| RawLinedEvent::raw(":"))) + .map_or_else(|| Either::Right(stream::empty()), Either::Left) } fn into_stream(self) -> impl Stream { - use futures::future::Either; - use crate::ext::StreamExt; - - let heartbeat_stream = self.heartbeat_stream(); - let raw_events = self.stream.map(|e| e.into_stream()).flatten(); - match heartbeat_stream { - Some(heartbeat) => Either::Left(raw_events.join(heartbeat)), - None => Either::Right(raw_events) - } + use futures::StreamExt; + + let heartbeats = self.heartbeat_stream(); + let events = StreamExt::map(self.stream, |e| e.into_stream()).flatten(); + crate::util::join(events, heartbeats) } fn into_reader(self) -> impl AsyncRead { @@ -621,10 +615,11 @@ mod sse_tests { impl> EventStream { fn into_string(self) -> String { + use std::pin::pin; + crate::async_test(async move { let mut string = String::new(); - let reader = self.into_reader(); - tokio::pin!(reader); + let mut reader = pin!(self.into_reader()); reader.read_to_string(&mut string).await.expect("event stream -> string"); string }) diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index ef9fdfecfa..40570e7b45 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -1,14 +1,14 @@ use std::fmt; use std::ops::{Deref, DerefMut}; -use std::net::SocketAddr; use yansi::Paint; use either::Either; use figment::{Figment, Provider}; use crate::{Catcher, Config, Route, Shutdown, sentinel, shield::Shield}; +use crate::listener::{Endpoint, Bindable, DefaultListener}; use crate::router::Router; -use crate::trip_wire::TripWire; +use crate::util::TripWire; use crate::fairing::{Fairing, Fairings}; use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting}; use crate::phase::{Stateful, StateRef, State}; @@ -203,35 +203,31 @@ impl Rocket { /// # Example /// /// ```rust - /// use rocket::Config; + /// use rocket::config::{Config, Ident}; /// # use std::net::Ipv4Addr; /// # use std::path::{Path, PathBuf}; /// # type Result = std::result::Result<(), rocket::Error>; /// /// let config = Config { - /// port: 7777, - /// address: Ipv4Addr::new(18, 127, 0, 1).into(), + /// ident: Ident::try_new("MyServer").expect("valid ident"), /// temp_dir: "/tmp/config-example".into(), /// ..Config::debug_default() /// }; /// /// # let _: Result = rocket::async_test(async move { /// let rocket = rocket::custom(&config).ignite().await?; - /// assert_eq!(rocket.config().port, 7777); - /// assert_eq!(rocket.config().address, Ipv4Addr::new(18, 127, 0, 1)); + /// assert_eq!(rocket.config().ident.as_str(), Some("MyServer")); /// assert_eq!(rocket.config().temp_dir.relative(), Path::new("/tmp/config-example")); /// /// // Create a new figment which modifies _some_ keys the existing figment: /// let figment = rocket.figment().clone() - /// .merge((Config::PORT, 8888)) - /// .merge((Config::ADDRESS, "171.64.200.10")); + /// .merge((Config::IDENT, "Example")); /// /// let rocket = rocket::custom(&config) /// .configure(figment) /// .ignite().await?; /// - /// assert_eq!(rocket.config().port, 8888); - /// assert_eq!(rocket.config().address, Ipv4Addr::new(171, 64, 200, 10)); + /// assert_eq!(rocket.config().ident.as_str(), Some("Example")); /// assert_eq!(rocket.config().temp_dir.relative(), Path::new("/tmp/config-example")); /// # Ok(()) /// # }); @@ -664,8 +660,9 @@ impl Rocket { self.shutdown.clone() } - fn into_orbit(self) -> Rocket { + pub(crate) fn into_orbit(self, address: Endpoint) -> Rocket { Rocket(Orbiting { + endpoint: address, router: self.0.router, fairings: self.0.fairings, figment: self.0.figment, @@ -675,28 +672,24 @@ impl Rocket { }) } - async fn _local_launch(self) -> Rocket { - let rocket = self.into_orbit(); - rocket.fairings.handle_liftoff(&rocket).await; - launch_info!("{}{}", "๐Ÿš€ ".emoji(), "Rocket has launched locally".primary().bold()); + async fn _local_launch(self, addr: Endpoint) -> Rocket { + let rocket = self.into_orbit(addr); + Rocket::liftoff(&rocket).await; rocket } async fn _launch(self) -> Result, Error> { - self.into_orbit() - .default_tcp_http_server(|rkt| Box::pin(async move { - rkt.fairings.handle_liftoff(&rkt).await; - - let proto = rkt.config.tls_enabled().then(|| "https").unwrap_or("http"); - let socket_addr = SocketAddr::new(rkt.config.address, rkt.config.port); - let addr = format!("{}://{}", proto, socket_addr); - launch_info!("{}{} {}", - "๐Ÿš€ ".emoji(), - "Rocket has launched from".bold().primary().linger(), - addr.underline()); - })) - .await - .map(|rocket| rocket.into_ignite()) + let config = self.figment().extract::()?; + either::for_both!(config.base_bindable()?, base => { + either::for_both!(config.tls_bindable(base), bindable => { + self._launch_on(bindable).await + }) + }) + } + + async fn _launch_on(self, bindable: B) -> Result, Error> { + let listener = bindable.bind().await.map_err(|e| ErrorKind::Bind(Box::new(e)))?; + self.serve(listener).await } } @@ -712,6 +705,21 @@ impl Rocket { }) } + pub(crate) async fn liftoff>(rocket: R) { + let rocket = rocket.deref(); + rocket.fairings.handle_liftoff(rocket).await; + + if !crate::running_within_rocket_async_rt().await { + warn!("Rocket is executing inside of a custom runtime."); + info_!("Rocket's runtime is enabled via `#[rocket::main]` or `#[launch]`."); + info_!("Forced shutdown is disabled. Runtime settings may be suboptimal."); + } + + launch_info!("{}{} {}", "๐Ÿš€ ".emoji(), + "Rocket has launched on".bold().primary().linger(), + rocket.endpoint().underline()); + } + /// Returns the finalized, active configuration. This is guaranteed to /// remain stable after [`Rocket::ignite()`], through ignition and into /// orbit. @@ -734,6 +742,10 @@ impl Rocket { &self.config } + pub fn endpoint(&self) -> &Endpoint { + &self.endpoint + } + /// Returns a handle which can be used to trigger a shutdown and detect a /// triggered shutdown. /// @@ -867,10 +879,10 @@ impl Rocket

    { } } - pub(crate) async fn local_launch(self) -> Result, Error> { + pub(crate) async fn local_launch(self, l: Endpoint) -> Result, Error> { let rocket = match self.0.into_state() { - State::Build(s) => Rocket::from(s).ignite().await?._local_launch().await, - State::Ignite(s) => Rocket::from(s)._local_launch().await, + State::Build(s) => Rocket::from(s).ignite().await?._local_launch(l).await, + State::Ignite(s) => Rocket::from(s)._local_launch(l).await, State::Orbit(s) => Rocket::from(s) }; @@ -928,6 +940,14 @@ impl Rocket

    { State::Orbit(s) => Ok(Rocket::from(s).into_ignite()) } } + + pub async fn launch_on(self, bindable: B) -> Result, Error> { + match self.0.into_state() { + State::Build(s) => Rocket::from(s).ignite().await?._launch_on(bindable).await, + State::Ignite(s) => Rocket::from(s)._launch_on(bindable).await, + State::Orbit(s) => Ok(Rocket::from(s).into_ignite()) + } + } } #[doc(hidden)] diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index e29be6d570..b42d81e0fc 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -167,7 +167,6 @@ impl Handler for F } } -// FIXME! impl<'r, 'o: 'r> Outcome<'o> { /// Return the `Outcome` of response to `req` from `responder`. /// diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index da2626e327..3fbe2ae702 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -1,540 +1,142 @@ use std::io; +use std::pin::pin; use std::sync::Arc; use std::time::Duration; -use std::pin::Pin; -use yansi::Paint; -use tokio::sync::oneshot; +use hyper::service::service_fn; +use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer}; +use hyper_util::server::conn::auto::Builder; +use futures::{Future, TryFutureExt, future::{select, Either::*}}; use tokio::time::sleep; -use futures::stream::StreamExt; -use futures::future::{FutureExt, Future, BoxFuture}; -use crate::{route, Rocket, Orbit, Request, Response, Data, Config}; -use crate::form::Form; -use crate::outcome::Outcome; -use crate::error::{Error, ErrorKind}; -use crate::ext::{AsyncReadExt, CancellableListener, CancellableIo}; +use crate::{Request, Rocket, Orbit, Data, Ignite}; use crate::request::ConnectionMeta; -use crate::data::IoHandler; - -use crate::http::{hyper, uncased, Method, Status, Header}; -use crate::http::private::{TcpListener, Listener, Connection, Incoming}; - -// A token returned to force the execution of one method before another. -pub(crate) struct RequestToken; - -async fn handle(name: Option<&str>, run: F) -> Option - where F: FnOnce() -> Fut, Fut: Future, -{ - use std::panic::AssertUnwindSafe; - - macro_rules! panic_info { - ($name:expr, $e:expr) => {{ - match $name { - Some(name) => error_!("Handler {} panicked.", name.primary()), - None => error_!("A handler panicked.") - }; - - info_!("This is an application bug."); - info_!("A panic in Rust must be treated as an exceptional event."); - info_!("Panicking is not a suitable error handling mechanism."); - info_!("Unwinding, the result of a panic, is an expensive operation."); - info_!("Panics will degrade application performance."); - info_!("Instead of panicking, return `Option` and/or `Result`."); - info_!("Values of either type can be returned directly from handlers."); - warn_!("A panic is treated as an internal server error."); - $e - }} - } - - let run = AssertUnwindSafe(run); - let fut = std::panic::catch_unwind(move || run()) - .map_err(|e| panic_info!(name, e)) - .ok()?; - - AssertUnwindSafe(fut) - .catch_unwind() - .await - .map_err(|e| panic_info!(name, e)) - .ok() -} - -// This function tries to hide all of the Hyper-ness from Rocket. It essentially -// converts Hyper types into Rocket types, then calls the `dispatch` function, -// which knows nothing about Hyper. Because responding depends on the -// `HyperResponse` type, this function does the actual response processing. -async fn hyper_service_fn( - rocket: Arc>, - conn: ConnectionMeta, - mut hyp_req: hyper::Request, -) -> Result, io::Error> { - // This future must return a hyper::Response, but the response body might - // borrow from the request. Instead, write the body in another future that - // sends the response metadata (and a body channel) prior. - let (tx, rx) = oneshot::channel(); - - #[cfg(not(broken_fmt))] - debug!("received request: {:#?}", hyp_req); - - tokio::spawn(async move { - // We move the request next, so get the upgrade future now. - let pending_upgrade = hyper::upgrade::on(&mut hyp_req); - - // Convert a Hyper request into a Rocket request. - let (h_parts, mut h_body) = hyp_req.into_parts(); - match Request::from_hyp(&rocket, &h_parts, Some(conn)) { - Ok(mut req) => { - // Convert into Rocket `Data`, dispatch request, write response. - let mut data = Data::from(&mut h_body); - let token = rocket.preprocess_request(&mut req, &mut data).await; - let mut response = rocket.dispatch(token, &req, data).await; - let upgrade = response.take_upgrade(req.headers().get("upgrade")); - if let Ok(Some((proto, handler))) = upgrade { - rocket.handle_upgrade(response, proto, handler, pending_upgrade, tx).await; - } else { - if upgrade.is_err() { - warn_!("Request wants upgrade but no I/O handler matched."); - info_!("Request is not being upgraded."); - } - - rocket.send_response(response, tx).await; - } - }, - Err(e) => { - warn!("Bad incoming HTTP request."); - e.errors.iter().for_each(|e| warn_!("Error: {}.", e)); - warn_!("Dispatching salvaged request to catcher: {}.", e.request); - - let response = rocket.handle_error(Status::BadRequest, &e.request).await; - rocket.send_response(response, tx).await; - } - } - }); - - // Receive the response written to `tx` by the task above. - rx.await.map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e)) -} +use crate::erased::{ErasedRequest, ErasedResponse, ErasedIoHandler}; +use crate::listener::{Listener, CancellableExt, BouncedExt}; +use crate::error::{Error, ErrorKind}; +use crate::data::IoStream; +use crate::util::ReaderStream; +use crate::http::Status; impl Rocket { - /// Wrapper around `_send_response` to log a success or error. - #[inline] - async fn send_response( - &self, - response: Response<'_>, - tx: oneshot::Sender>, - ) { - let remote_hungup = |e: &io::Error| match e.kind() { - | io::ErrorKind::BrokenPipe - | io::ErrorKind::ConnectionReset - | io::ErrorKind::ConnectionAborted => true, - _ => false, - }; - - match self._send_response(response, tx).await { - Ok(()) => info_!("{}", "Response succeeded.".green()), - Err(e) if remote_hungup(&e) => warn_!("Remote left: {}.", e), - Err(e) => warn_!("Failed to write response: {}.", e), - } - } - - /// Attempts to create a hyper response from `response` and send it to `tx`. - #[inline] - async fn _send_response( - &self, - mut response: Response<'_>, - tx: oneshot::Sender>, - ) -> io::Result<()> { - let mut hyp_res = hyper::Response::builder(); - - hyp_res = hyp_res.status(response.status().code); - for header in response.headers().iter() { - let name = header.name.as_str(); - let value = header.value.as_bytes(); - hyp_res = hyp_res.header(name, value); - } - - let body = response.body_mut(); - if let Some(n) = body.size().await { - hyp_res = hyp_res.header(hyper::header::CONTENT_LENGTH, n); - } - - let (mut sender, hyp_body) = hyper::Body::channel(); - let hyp_response = hyp_res.body(hyp_body) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - #[cfg(not(broken_fmt))] - debug!("sending response: {:#?}", hyp_response); - - tx.send(hyp_response).map_err(|_| { - let msg = "client disconnect before response started"; - io::Error::new(io::ErrorKind::BrokenPipe, msg) - })?; - - let max_chunk_size = body.max_chunk_size(); - let mut stream = body.into_bytes_stream(max_chunk_size); - while let Some(next) = stream.next().await { - sender.send_data(next?).await - .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?; - } - - Ok(()) - } - - async fn handle_upgrade<'r>( - &self, - mut response: Response<'r>, - proto: uncased::Uncased<'r>, - io_handler: Pin>, - pending_upgrade: hyper::upgrade::OnUpgrade, - tx: oneshot::Sender>, - ) { - info_!("Upgrading connection to {}.", Paint::white(&proto).bold()); - response.set_status(Status::SwitchingProtocols); - response.set_raw_header("Connection", "Upgrade"); - response.set_raw_header("Upgrade", proto.clone().into_cow()); - self.send_response(response, tx).await; + async fn service( + self: Arc, + mut req: hyper::Request, + connection: ConnectionMeta, + ) -> Result>, http::Error> { + let upgrade = hyper::upgrade::on(&mut req); + let (parts, incoming) = req.into_parts(); + let request = ErasedRequest::new(self, parts, |rocket, parts| { + Request::from_hyp(rocket, parts, connection).unwrap_or_else(|e| e) + }); - match pending_upgrade.await { - Ok(io_stream) => { - info_!("Upgrade successful."); - if let Err(e) = io_handler.io(io_stream.into()).await { - if e.kind() == io::ErrorKind::BrokenPipe { - warn!("Upgraded {} I/O handler was closed.", proto); - } else { - error!("Upgraded {} I/O handler failed: {}", proto, e); - } + let mut response = request.into_response( + incoming, + |incoming| Data::from(incoming), + |rocket, request, data| Box::pin(rocket.preprocess(request, data)), + |token, rocket, request, data| Box::pin(async move { + if !request.errors.is_empty() { + return rocket.dispatch_error(Status::BadRequest, request).await; } - }, - Err(e) => { - warn!("Response indicated upgrade, but upgrade failed."); - warn_!("Upgrade error: {}", e); - } - } - } - /// Preprocess the request for Rocket things. Currently, this means: - /// - /// * Rewriting the method in the request if _method form field exists. - /// * Run the request fairings. - /// - /// Keep this in-sync with derive_form when preprocessing form fields. - pub(crate) async fn preprocess_request( - &self, - req: &mut Request<'_>, - data: &mut Data<'_> - ) -> RequestToken { - // Check if this is a form and if the form contains the special _method - // field which we use to reinterpret the request's method. - let (min_len, max_len) = ("_method=get".len(), "_method=delete".len()); - let peek_buffer = data.peek(max_len).await; - let is_form = req.content_type().map_or(false, |ct| ct.is_form()); + let mut response = rocket.dispatch(token, request, data).await; + response.body_mut().size().await; + response + }) + ).await; - if is_form && req.method() == Method::Post && peek_buffer.len() >= min_len { - let method = std::str::from_utf8(peek_buffer).ok() - .and_then(|raw_form| Form::values(raw_form).next()) - .filter(|field| field.name == "_method") - .and_then(|field| field.value.parse().ok()); - - if let Some(method) = method { - req._set_method(method); - } + let io_handler = response.to_io_handler(Rocket::extract_io_handler); + if let Some(handler) = io_handler { + let upgrade = upgrade.map_ok(IoStream::from).map_err(io::Error::other); + tokio::task::spawn(io_handler_task(upgrade, handler)); } - // Run request fairings. - self.fairings.handle_request(req, data).await; - - RequestToken - } - - #[inline] - pub(crate) async fn dispatch<'s, 'r: 's>( - &'s self, - _token: RequestToken, - request: &'r Request<'s>, - data: Data<'r> - ) -> Response<'r> { - info!("{}:", request); - - // Remember if the request is `HEAD` for later body stripping. - let was_head_request = request.method() == Method::Head; - - // Route the request and run the user's handlers. - let mut response = self.route_and_process(request, data).await; - - // Add a default 'Server' header if it isn't already there. - // TODO: If removing Hyper, write out `Date` header too. - if let Some(ident) = request.rocket().config.ident.as_str() { - if !response.headers().contains("Server") { - response.set_header(Header::new("Server", ident)); - } + let mut builder = hyper::Response::builder(); + builder = builder.status(response.inner().status().code); + for header in response.inner().headers().iter() { + builder = builder.header(header.name().as_str(), header.value()); } - // Run the response fairings. - self.fairings.handle_response(request, &mut response).await; - - // Strip the body if this is a `HEAD` request. - if was_head_request { - response.strip_body(); + if let Some(size) = response.inner().body().preset_size() { + builder = builder.header("Content-Length", size); } - response - } - - async fn route_and_process<'s, 'r: 's>( - &'s self, - request: &'r Request<'s>, - data: Data<'r> - ) -> Response<'r> { - let mut response = match self.route(request, data).await { - Outcome::Success(response) => response, - Outcome::Forward((data, _)) if request.method() == Method::Head => { - info_!("Autohandling {} request.", "HEAD".primary().bold()); - - // Dispatch the request again with Method `GET`. - request._set_method(Method::Get); - match self.route(request, data).await { - Outcome::Success(response) => response, - Outcome::Error(status) => self.handle_error(status, request).await, - Outcome::Forward((_, status)) => self.handle_error(status, request).await, - } - } - Outcome::Forward((_, status)) => self.handle_error(status, request).await, - Outcome::Error(status) => self.handle_error(status, request).await, - }; - - // Set the cookies. Note that error responses will only include cookies - // set by the error handler. See `handle_error` for more. - let delta_jar = request.cookies().take_delta_jar(); - for cookie in delta_jar.delta() { - response.adjoin_header(cookie); - } - - response - } - - /// Tries to find a `Responder` for a given `request`. It does this by - /// routing the request and calling the handler for each matching route - /// until one of the handlers returns success or error, or there are no - /// additional routes to try (forward). The corresponding outcome for each - /// condition is returned. - #[inline] - async fn route<'s, 'r: 's>( - &'s self, - request: &'r Request<'s>, - mut data: Data<'r>, - ) -> route::Outcome<'r> { - // Go through all matching routes until we fail or succeed or run out of - // routes to try, in which case we forward with the last status. - let mut status = Status::NotFound; - for route in self.router.route(request) { - // Retrieve and set the requests parameters. - info_!("Matched: {}", route); - request.set_route(route); - - let name = route.name.as_deref(); - let outcome = handle(name, || route.handler.handle(request, data)).await - .unwrap_or(Outcome::Error(Status::InternalServerError)); - - // Check if the request processing completed (Some) or if the - // request needs to be forwarded. If it does, continue the loop - // (None) to try again. - info_!("{}", outcome.log_display()); - match outcome { - o@Outcome::Success(_) | o@Outcome::Error(_) => return o, - Outcome::Forward(forwarded) => (data, status) = forwarded, - } - } - - error_!("No matching routes for {}.", request); - Outcome::Forward((data, status)) - } - - /// Invokes the handler with `req` for catcher with status `status`. - /// - /// In order of preference, invoked handler is: - /// * the user's registered handler for `status` - /// * the user's registered `default` handler - /// * Rocket's default handler for `status` - /// - /// Return `Ok(result)` if the handler succeeded. Returns `Ok(Some(Status))` - /// if the handler ran to completion but failed. Returns `Ok(None)` if the - /// handler panicked while executing. - async fn invoke_catcher<'s, 'r: 's>( - &'s self, - status: Status, - req: &'r Request<'s> - ) -> Result, Option> { - // For now, we reset the delta state to prevent any modifications - // from earlier, unsuccessful paths from being reflected in error - // response. We may wish to relax this in the future. - req.cookies().reset_delta(); - - if let Some(catcher) = self.router.catch(status, req) { - warn_!("Responding with registered {} catcher.", catcher); - let name = catcher.name.as_deref(); - handle(name, || catcher.handler.handle(status, req)).await - .map(|result| result.map_err(Some)) - .unwrap_or_else(|| Err(None)) - } else { - let code = status.code.blue().bold(); - warn_!("No {} catcher registered. Using Rocket default.", code); - Ok(crate::catcher::default_handler(status, req)) - } + let chunk_size = response.inner().body().max_chunk_size(); + builder.body(ReaderStream::with_capacity(response, chunk_size)) } +} - // Invokes the catcher for `status`. Returns the response on success. - // - // On catcher error, the 500 error catcher is attempted. If _that_ errors, - // the (infallible) default 500 error cather is used. - pub(crate) async fn handle_error<'s, 'r: 's>( - &'s self, - mut status: Status, - req: &'r Request<'s> - ) -> Response<'r> { - // Dispatch to the `status` catcher. - if let Ok(r) = self.invoke_catcher(status, req).await { - return r; - } +async fn io_handler_task(stream: S, mut handler: ErasedIoHandler) + where S: Future> +{ + let stream = match stream.await { + Ok(stream) => stream, + Err(e) => return warn_!("Upgrade failed: {e}"), + }; - // If it fails and it's not a 500, try the 500 catcher. - if status != Status::InternalServerError { - error_!("Catcher failed. Attempting 500 error catcher."); - status = Status::InternalServerError; - if let Ok(r) = self.invoke_catcher(status, req).await { - return r; - } + info_!("Upgrade succeeded."); + if let Err(e) = handler.take().io(stream).await { + match e.kind() { + io::ErrorKind::BrokenPipe => warn!("Upgrade I/O handler was closed."), + e => error!("Upgrade I/O handler failed: {e}"), } - - // If it failed again or if it was already a 500, use Rocket's default. - error_!("{} catcher failed. Using Rocket default 500.", status.code); - crate::catcher::default_handler(Status::InternalServerError, req) } +} - pub(crate) async fn default_tcp_http_server(mut self, ready: C) -> Result - where C: for<'a> Fn(&'a Self) -> BoxFuture<'a, ()> +impl Rocket { + pub(crate) async fn serve(self, listener: L) -> Result + where L: Listener + 'static { - use std::net::ToSocketAddrs; - - // Determine the address we're going to serve on. - let addr = format!("{}:{}", self.config.address, self.config.port); - let mut addr = addr.to_socket_addrs() - .map(|mut addrs| addrs.next().expect(">= 1 socket addr")) - .map_err(|e| Error::new(ErrorKind::Io(e)))?; - - #[cfg(feature = "tls")] - if self.config.tls_enabled() { - if let Some(ref config) = self.config.tls { - use crate::http::tls::TlsListener; - - let conf = config.to_native_config().map_err(ErrorKind::Io)?; - let l = TlsListener::bind(addr, conf).await.map_err(ErrorKind::TlsBind)?; - addr = l.local_addr().unwrap_or(addr); - self.config.address = addr.ip(); - self.config.port = addr.port(); - ready(&mut self).await; - return self.http_server(l).await; + let mut builder = Builder::new(TokioExecutor::new()); + let keep_alive = Duration::from_secs(self.config.keep_alive.into()); + builder.http1() + .half_close(true) + .timer(TokioTimer::new()) + .keep_alive(keep_alive > Duration::ZERO) + .preserve_header_case(true) + .header_read_timeout(Duration::from_secs(15)); + + #[cfg(feature = "http2")] { + builder.http2().timer(TokioTimer::new()); + if keep_alive > Duration::ZERO { + builder.http2() + .timer(TokioTimer::new()) + .keep_alive_interval(keep_alive / 4) + .keep_alive_timeout(keep_alive); } } - let l = TcpListener::bind(addr).await.map_err(ErrorKind::Bind)?; - addr = l.local_addr().unwrap_or(addr); - self.config.address = addr.ip(); - self.config.port = addr.port(); - ready(&mut self).await; - self.http_server(l).await - } - - // TODO.async: Solidify the Listener APIs and make this function public - pub(crate) async fn http_server(self, listener: L) -> Result - where L: Listener + Send, ::Connection: Send + Unpin + 'static - { - // Emit a warning if we're not running inside of Rocket's async runtime. - if self.config.profile == Config::DEBUG_PROFILE { - tokio::task::spawn_blocking(|| { - let this = std::thread::current(); - if !this.name().map_or(false, |s| s.starts_with("rocket-worker")) { - warn!("Rocket is executing inside of a custom runtime."); - info_!("Rocket's runtime is enabled via `#[rocket::main]` or `#[launch]`."); - info_!("Forced shutdown is disabled. Runtime settings may be suboptimal."); - } - }); - } - - // Set up cancellable I/O from the given listener. Shutdown occurs when - // `Shutdown` (`TripWire`) resolves. This can occur directly through a - // notification or indirectly through an external signal which, when - // received, results in triggering the notify. - let shutdown = self.shutdown(); - let sig_stream = self.config.shutdown.signal_stream(); - let grace = self.config.shutdown.grace as u64; - let mercy = self.config.shutdown.mercy as u64; - - // Start a task that listens for external signals and notifies shutdown. - if let Some(mut stream) = sig_stream { - let shutdown = shutdown.clone(); - tokio::spawn(async move { - while let Some(sig) = stream.next().await { - if shutdown.0.tripped() { - warn!("Received {}. Shutdown already in progress.", sig); - } else { - warn!("Received {}. Requesting shutdown.", sig); + let listener = listener.bounced().cancellable(self.shutdown(), &self.config.shutdown); + let rocket = Arc::new(self.into_orbit(listener.socket_addr()?)); + let _ = tokio::spawn(Rocket::liftoff(rocket.clone())).await; + + let (server, listener) = (Arc::new(builder), Arc::new(listener)); + while let Some(accept) = listener.accept_next().await { + let (listener, rocket, server) = (listener.clone(), rocket.clone(), server.clone()); + tokio::spawn({ + let result = async move { + let conn = TokioIo::new(listener.connect(accept).await?); + let meta = ConnectionMeta::from(conn.inner()); + let service = service_fn(|req| rocket.clone().service(req, meta.clone())); + let serve = pin!(server.serve_connection_with_upgrades(conn, service)); + match select(serve, rocket.shutdown()).await { + Left((result, _)) => result, + Right((_, mut conn)) => { + conn.as_mut().graceful_shutdown(); + conn.await + } } + }; - shutdown.0.trip(); - } + result.inspect_err(crate::error::log_server_error) }); } - // Save the keep-alive value for later use; we're about to move `self`. - let keep_alive = self.config.keep_alive; - - // Create the Hyper `Service`. - let rocket = Arc::new(self); - let service_fn = |conn: &CancellableIo<_, L::Connection>| { - let rocket = rocket.clone(); - let connection = ConnectionMeta { - remote: conn.peer_address(), - client_certificates: conn.peer_certificates(), - }; - - async move { - Ok::<_, std::convert::Infallible>(hyper::service::service_fn(move |req| { - hyper_service_fn(rocket.clone(), connection.clone(), req) - })) - } - }; - - // NOTE: `hyper` uses `tokio::spawn()` as the default executor. - let listener = CancellableListener::new(shutdown.clone(), listener, grace, mercy); - let builder = hyper::server::Server::builder(Incoming::new(listener).nodelay(true)); - - #[cfg(feature = "http2")] - let builder = builder.http2_keep_alive_interval(match keep_alive { - 0 => None, - n => Some(Duration::from_secs(n as u64)) - }); - - let server = builder - .http1_keepalive(keep_alive != 0) - .http1_preserve_header_case(true) - .serve(hyper::service::make_service_fn(service_fn)) - .with_graceful_shutdown(shutdown.clone()); - - // This deserves some explanation. - // - // This is largely to deal with Hyper's dreadful and largely nonexistent - // handling of shutdown, in general, nevermind graceful. - // - // When Hyper receives a "graceful shutdown" request, it stops accepting - // new requests. That's it. It continues to process existing requests - // and outgoing responses forever and never cancels them. As a result, - // Rocket must take it upon itself to cancel any existing I/O. - // - // To do so, Rocket wraps all connections in a `CancellableIo` struct, - // an internal structure that gracefully closes I/O when it receives a - // signal. That signal is the `shutdown` future. When the future - // resolves, `CancellableIo` begins to terminate in grace, mercy, and - // finally force close phases. Since all connections are wrapped in + // Rocket wraps all connections in a `CancellableIo` struct, an internal + // structure that gracefully closes I/O when it receives a signal. That + // signal is the `shutdown` future. When the future resolves, + // `CancellableIo` begins to terminate in grace, mercy, and finally + // force close phases. Since all connections are wrapped in // `CancellableIo`, this eventually ends all I/O. // // At that point, unless a user spawned an infinite, stand-alone task @@ -543,69 +145,35 @@ impl Rocket { // we can return the owned instance of `Rocket`. // // Unfortunately, the Hyper `server` future resolves as soon as it has - // finishes processing requests without respect for ongoing responses. + // finished processing requests without respect for ongoing responses. // That is, `server` resolves even when there are running tasks that are // generating a response. So, `server` resolving implies little to // nothing about the state of connections. As a result, we depend on the // timing of grace + mercy + some buffer to determine when all // connections should be closed, thus all tasks should be complete, thus - // all references to `Arc` should be dropped and we can get a - // unique reference. - tokio::pin!(server); - tokio::select! { - biased; - - _ = shutdown => { - // Run shutdown fairings. We compute `sleep()` for grace periods - // beforehand to ensure we don't add shutdown fairing completion - // time, which is arbitrary, to these periods. - info!("Shutdown requested. Waiting for pending I/O..."); - let grace_timer = sleep(Duration::from_secs(grace)); - let mercy_timer = sleep(Duration::from_secs(grace + mercy)); - let shutdown_timer = sleep(Duration::from_secs(grace + mercy + 1)); - rocket.fairings.handle_shutdown(&*rocket).await; - - tokio::pin!(grace_timer, mercy_timer, shutdown_timer); - tokio::select! { - biased; + // all references to `Arc` should be dropped and we can get back + // a unique reference. + info!("Shutting down. Waiting for shutdown fairings and pending I/O..."); + tokio::spawn({ + let rocket = rocket.clone(); + async move { rocket.fairings.handle_shutdown(&*rocket).await } + }); - result = &mut server => { - if let Err(e) = result { - warn!("Server failed while shutting down: {}", e); - return Err(Error::shutdown(rocket.clone(), e)); - } + let config = &rocket.config.shutdown; + let wait = Duration::from_micros(250); + for period in [wait, config.grace(), wait, config.mercy(), wait * 4] { + if Arc::strong_count(&rocket) == 1 { break } + sleep(period).await; + } - if Arc::strong_count(&rocket) != 1 { grace_timer.await; } - if Arc::strong_count(&rocket) != 1 { mercy_timer.await; } - if Arc::strong_count(&rocket) != 1 { shutdown_timer.await; } - match Arc::try_unwrap(rocket) { - Ok(rocket) => { - info!("Graceful shutdown completed successfully."); - Ok(rocket) - } - Err(rocket) => { - warn!("Shutdown failed: outstanding background I/O."); - Err(Error::shutdown(rocket, None)) - } - } - } - _ = &mut shutdown_timer => { - warn!("Shutdown failed: server executing after timeouts."); - return Err(Error::shutdown(rocket.clone(), None)); - }, - } + match Arc::try_unwrap(rocket) { + Ok(rocket) => { + info!("Graceful shutdown completed successfully."); + Ok(rocket.into_ignite()) } - result = &mut server => { - match result { - Ok(()) => { - info!("Server shutdown nominally."); - Ok(Arc::try_unwrap(rocket).map_err(|r| Error::shutdown(r, None))?) - } - Err(e) => { - info!("Server failed prior to shutdown: {}:", e); - Err(Error::shutdown(rocket.clone(), e)) - } - } + Err(rocket) => { + warn!("Shutdown failed: outstanding background I/O."); + Err(Error::new(ErrorKind::Shutdown(rocket))) } } } diff --git a/core/lib/src/shield/shield.rs b/core/lib/src/shield/shield.rs index ea44814484..f3a3aeb241 100644 --- a/core/lib/src/shield/shield.rs +++ b/core/lib/src/shield/shield.rs @@ -198,7 +198,7 @@ impl Fairing for Shield { } async fn on_liftoff(&self, rocket: &Rocket) { - let force_hsts = rocket.config().tls_enabled() + let force_hsts = rocket.endpoint().is_tls() && rocket.figment().profile() != Config::DEBUG_PROFILE && !self.is_enabled::(); diff --git a/core/lib/src/shutdown.rs b/core/lib/src/shutdown.rs index 490114f51c..43a667af0a 100644 --- a/core/lib/src/shutdown.rs +++ b/core/lib/src/shutdown.rs @@ -5,7 +5,7 @@ use std::pin::Pin; use futures::FutureExt; use crate::request::{FromRequest, Outcome, Request}; -use crate::trip_wire::TripWire; +use crate::util::TripWire; /// A request guard and future for graceful shutdown. /// diff --git a/core/lib/src/config/tls.rs b/core/lib/src/tls/config.rs similarity index 56% rename from core/lib/src/config/tls.rs rename to core/lib/src/tls/config.rs index 12face0015..3131e16d5c 100644 --- a/core/lib/src/config/tls.rs +++ b/core/lib/src/tls/config.rs @@ -1,3 +1,5 @@ +use std::io; + use figment::value::magic::{Either, RelativePathBuf}; use serde::{Deserialize, Serialize}; use indexmap::IndexSet; @@ -29,7 +31,7 @@ use indexmap::IndexSet; /// /// Additionally, the `mutual` parameter controls if and how the server /// authenticates clients via mutual TLS. It works in concert with the -/// [`mtls`](crate::mtls) module. See [`MutualTls`] for configuration details. +/// [`mtls`](crate::mtls) module. See [`MtlsConfig`] for configuration details. /// /// In `Rocket.toml`, configuration might look like: /// @@ -43,41 +45,36 @@ use indexmap::IndexSet; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// use rocket::config::{Config, TlsConfig, CipherSuite}; +/// use rocket::tls::{TlsConfig, CipherSuite}; +/// use rocket::figment::providers::Serialized; /// /// #[launch] /// fn rocket() -> _ { -/// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem") +/// let tls = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem") /// .with_ciphers(CipherSuite::TLS_V13_SET) /// .with_preferred_server_cipher_order(true); /// -/// let config = Config { -/// tls: Some(tls_config), -/// ..Default::default() -/// }; -/// -/// rocket::custom(config) +/// rocket::custom(rocket::Config::figment().merge(("tls", tls))) /// } /// ``` /// /// Or by creating a custom figment: /// /// ```rust -/// use rocket::config::Config; +/// use rocket::figment::Figment; +/// use rocket::tls::TlsConfig; /// -/// let figment = Config::figment() -/// .merge(("tls.certs", "path/to/certs.pem")) -/// .merge(("tls.key", vec![0; 32])); +/// let figment = Figment::new() +/// .merge(("certs", "path/to/certs.pem")) +/// .merge(("key", vec![0; 32])); /// # -/// # let config = rocket::Config::from(figment); -/// # let tls_config = config.tls.as_ref().unwrap(); +/// # let tls_config: TlsConfig = figment.extract().unwrap(); /// # assert!(tls_config.certs().is_left()); /// # assert!(tls_config.key().is_right()); /// # assert_eq!(tls_config.ciphers().count(), 9); /// # assert!(!tls_config.prefer_server_cipher_order()); /// ``` #[derive(PartialEq, Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(nightly, doc(cfg(feature = "tls")))] pub struct TlsConfig { /// Path to a PEM file with, or raw bytes for, a DER-encoded X.509 TLS /// certificate chain. @@ -95,92 +92,12 @@ pub struct TlsConfig { #[serde(default)] #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] - pub(crate) mutual: Option, -} - -/// Mutual TLS configuration. -/// -/// Configuration works in concert with the [`mtls`](crate::mtls) module, which -/// provides a request guard to validate, verify, and retrieve client -/// certificates in routes. -/// -/// By default, mutual TLS is disabled and client certificates are not required, -/// validated or verified. To enable mutual TLS, the `mtls` feature must be -/// enabled and support configured via two `tls.mutual` parameters: -/// -/// * `ca_certs` -/// -/// A required path to a PEM file or raw bytes to a DER-encoded X.509 TLS -/// certificate chain for the certificate authority to verify client -/// certificates against. When a path is configured in a file, such as -/// `Rocket.toml`, relative paths are interpreted as relative to the source -/// file's directory. -/// -/// * `mandatory` -/// -/// An optional boolean that control whether client authentication is -/// required. -/// -/// When `true`, client authentication is required. TLS connections where -/// the client does not present a certificate are immediately terminated. -/// When `false`, the client is not required to present a certificate. In -/// either case, if a certificate _is_ presented, it must be valid or the -/// connection is terminated. -/// -/// In a `Rocket.toml`, configuration might look like: -/// -/// ```toml -/// [default.tls.mutual] -/// ca_certs = "/ssl/ca_cert.pem" -/// mandatory = true # when absent, defaults to false -/// ``` -/// -/// Programmatically, configuration might look like: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// use rocket::config::{Config, TlsConfig, MutualTls}; -/// -/// #[launch] -/// fn rocket() -> _ { -/// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem") -/// .with_mutual(MutualTls::from_path("/ssl/ca_cert.pem")); -/// -/// let config = Config { -/// tls: Some(tls_config), -/// ..Default::default() -/// }; -/// -/// rocket::custom(config) -/// } -/// ``` -/// -/// Once mTLS is configured, the [`mtls::Certificate`](crate::mtls::Certificate) -/// request guard can be used to retrieve client certificates in routes. -#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)] -#[cfg(feature = "mtls")] -#[cfg_attr(nightly, doc(cfg(feature = "mtls")))] -pub struct MutualTls { - /// Path to a PEM file with, or raw bytes for, DER-encoded Certificate - /// Authority certificates which will be used to verify client-presented - /// certificates. - // TODO: We should support more than one root. - pub(crate) ca_certs: Either>, - /// Whether the client is required to present a certificate. - /// - /// When `true`, the client is required to present a valid certificate to - /// proceed with TLS. When `false`, the client is not required to present a - /// certificate. In either case, if a certificate _is_ presented, it must be - /// valid or the connection is terminated. - #[serde(default)] - #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] - pub mandatory: bool, + pub(crate) mutual: Option, } /// A supported TLS cipher suite. #[allow(non_camel_case_types)] #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, Deserialize, Serialize)] -#[cfg_attr(nightly, doc(cfg(feature = "tls")))] #[non_exhaustive] pub enum CipherSuite { /// The TLS 1.3 `TLS_CHACHA20_POLY1305_SHA256` cipher suite. @@ -204,50 +121,7 @@ pub enum CipherSuite { TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } -impl CipherSuite { - /// The default set and order of cipher suites. These are all of the - /// variants in [`CipherSuite`] in their declaration order. - pub const DEFAULT_SET: [CipherSuite; 9] = [ - // TLS v1.3 suites... - CipherSuite::TLS_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_AES_256_GCM_SHA384, - CipherSuite::TLS_AES_128_GCM_SHA256, - - // TLS v1.2 suites... - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - ]; - - /// The default set and order of cipher suites. These are the TLS 1.3 - /// variants in [`CipherSuite`] in their declaration order. - pub const TLS_V13_SET: [CipherSuite; 3] = [ - CipherSuite::TLS_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_AES_256_GCM_SHA384, - CipherSuite::TLS_AES_128_GCM_SHA256, - ]; - - /// The default set and order of cipher suites. These are the TLS 1.2 - /// variants in [`CipherSuite`] in their declaration order. - pub const TLS_V12_SET: [CipherSuite; 6] = [ - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - ]; - - /// Used as the `serde` default for `ciphers`. - fn default_set() -> IndexSet { - Self::DEFAULT_SET.iter().copied().collect() - } -} - -impl TlsConfig { +impl Default for TlsConfig { fn default() -> Self { TlsConfig { certs: Either::Right(vec![]), @@ -258,7 +132,9 @@ impl TlsConfig { mutual: None, } } +} +impl TlsConfig { /// Constructs a `TlsConfig` from paths to a `certs` certificate chain /// a `key` private-key. This method does no validation; it simply creates a /// structure suitable for passing into a [`Config`](crate::Config). @@ -266,12 +142,12 @@ impl TlsConfig { /// # Example /// /// ```rust - /// use rocket::config::TlsConfig; + /// use rocket::tls::TlsConfig; /// /// let tls_config = TlsConfig::from_paths("/ssl/certs.pem", "/ssl/key.pem"); /// ``` pub fn from_paths(certs: C, key: K) -> Self - where C: AsRef, K: AsRef + where C: AsRef, K: AsRef, { TlsConfig { certs: Either::Left(certs.as_ref().to_path_buf().into()), @@ -288,7 +164,7 @@ impl TlsConfig { /// # Example /// /// ```rust - /// use rocket::config::TlsConfig; + /// use rocket::tls::TlsConfig; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -315,7 +191,7 @@ impl TlsConfig { /// Disable TLS v1.2 by selecting only TLS v1.3 cipher suites: /// /// ```rust - /// use rocket::config::{TlsConfig, CipherSuite}; + /// use rocket::tls::{TlsConfig, CipherSuite}; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -326,7 +202,7 @@ impl TlsConfig { /// Enable only ChaCha20-Poly1305 based TLS v1.2 and TLS v1.3 cipher suites: /// /// ```rust - /// use rocket::config::{TlsConfig, CipherSuite}; + /// use rocket::tls::{TlsConfig, CipherSuite}; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -341,7 +217,7 @@ impl TlsConfig { /// Later duplicates are ignored. /// /// ```rust - /// use rocket::config::{TlsConfig, CipherSuite}; + /// use rocket::tls::{TlsConfig, CipherSuite}; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -361,8 +237,8 @@ impl TlsConfig { /// CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, /// ]); /// ``` - pub fn with_ciphers(mut self, ciphers: I) -> Self - where I: IntoIterator + pub fn with_ciphers(mut self, ciphers: C) -> Self + where C: IntoIterator { self.ciphers = ciphers.into_iter().collect(); self @@ -385,7 +261,7 @@ impl TlsConfig { /// # Example /// /// ```rust - /// use rocket::config::{TlsConfig, CipherSuite}; + /// use rocket::tls::{TlsConfig, CipherSuite}; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -398,22 +274,24 @@ impl TlsConfig { self } - /// Configures mutual TLS. See [`MutualTls`] for details. + /// Set mutual TLS configuration. See + /// [`MtlsConfig`](crate::mtls::MtlsConfig) for details. /// /// # Example /// /// ```rust - /// use rocket::config::{TlsConfig, MutualTls}; + /// use rocket::tls::TlsConfig; + /// use rocket::mtls::MtlsConfig; /// /// # let certs = &[]; /// # let key = &[]; - /// let mtls_config = MutualTls::from_path("path/to/cert.pem").mandatory(true); + /// let mtls_config = MtlsConfig::from_path("path/to/cert.pem").mandatory(true); /// let tls_config = TlsConfig::from_bytes(certs, key).with_mutual(mtls_config); /// assert!(tls_config.mutual().is_some()); /// ``` #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] - pub fn with_mutual(mut self, config: MutualTls) -> Self { + pub fn with_mutual(mut self, config: crate::mtls::MtlsConfig) -> Self { self.mutual = Some(config); self } @@ -423,16 +301,17 @@ impl TlsConfig { /// # Example /// /// ```rust - /// use rocket::Config; + /// # use std::path::Path; + /// use rocket::tls::TlsConfig; + /// use rocket::figment::Figment; /// - /// let figment = Config::figment() - /// .merge(("tls.certs", vec![0; 32])) - /// .merge(("tls.key", "/etc/ssl/key.pem")); + /// let figment = Figment::new() + /// .merge(("certs", "/path/to/certs.pem")) + /// .merge(("key", vec![0; 32])); /// - /// let config = rocket::Config::from(figment); - /// let tls_config = config.tls.as_ref().unwrap(); - /// let cert_bytes = tls_config.certs().right().unwrap(); - /// assert!(cert_bytes.iter().all(|&b| b == 0)); + /// let tls_config: TlsConfig = figment.extract().unwrap(); + /// let cert_path = tls_config.certs().left().unwrap(); + /// assert_eq!(cert_path, Path::new("/path/to/certs.pem")); /// ``` pub fn certs(&self) -> either::Either { match &self.certs { @@ -441,20 +320,24 @@ impl TlsConfig { } } + pub fn certs_reader(&self) -> io::Result> { + to_reader(&self.certs) + } + /// Returns the value of the `key` parameter. /// /// # Example /// /// ```rust - /// use std::path::Path; - /// use rocket::Config; + /// # use std::path::Path; + /// use rocket::tls::TlsConfig; + /// use rocket::figment::Figment; /// - /// let figment = Config::figment() - /// .merge(("tls.certs", vec![0; 32])) - /// .merge(("tls.key", "/etc/ssl/key.pem")); + /// let figment = Figment::new() + /// .merge(("certs", vec![0; 32])) + /// .merge(("key", "/etc/ssl/key.pem")); /// - /// let config = rocket::Config::from(figment); - /// let tls_config = config.tls.as_ref().unwrap(); + /// let tls_config: TlsConfig = figment.extract().unwrap(); /// let key_path = tls_config.key().left().unwrap(); /// assert_eq!(key_path, Path::new("/etc/ssl/key.pem")); /// ``` @@ -465,13 +348,17 @@ impl TlsConfig { } } + pub fn key_reader(&self) -> io::Result> { + to_reader(&self.key) + } + /// Returns an iterator over the enabled cipher suites in their order of /// preference from most to least preferred. /// /// # Example /// /// ```rust - /// use rocket::config::{TlsConfig, CipherSuite}; + /// use rocket::tls::{TlsConfig, CipherSuite}; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -496,7 +383,7 @@ impl TlsConfig { /// # Example /// /// ```rust - /// use rocket::config::TlsConfig; + /// use rocket::tls::TlsConfig; /// /// # let certs_buf = &[]; /// # let key_buf = &[]; @@ -520,11 +407,13 @@ impl TlsConfig { /// /// ```rust /// use std::path::Path; - /// use rocket::config::{TlsConfig, MutualTls}; + /// + /// use rocket::tls::TlsConfig; + /// use rocket::mtls::MtlsConfig; /// /// # let certs = &[]; /// # let key = &[]; - /// let mtls_config = MutualTls::from_path("path/to/cert.pem").mandatory(true); + /// let mtls_config = MtlsConfig::from_path("path/to/cert.pem").mandatory(true); /// let tls_config = TlsConfig::from_bytes(certs, key).with_mutual(mtls_config); /// /// let mtls = tls_config.mutual().unwrap(); @@ -533,171 +422,232 @@ impl TlsConfig { /// ``` #[cfg(feature = "mtls")] #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] - pub fn mutual(&self) -> Option<&MutualTls> { + pub fn mutual(&self) -> Option<&crate::mtls::MtlsConfig> { self.mutual.as_ref() } -} -#[cfg(feature = "mtls")] -impl MutualTls { - /// Constructs a `MutualTls` from a path to a PEM file with a certificate - /// authority `ca_certs` DER-encoded X.509 TLS certificate chain. This - /// method does no validation; it simply creates a structure suitable for - /// passing into a [`TlsConfig`]. - /// - /// These certificates will be used to verify client-presented certificates - /// in TLS connections. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::MutualTls; - /// - /// let tls_config = MutualTls::from_path("/ssl/ca_certs.pem"); - /// ``` - pub fn from_path>(ca_certs: C) -> Self { - MutualTls { - ca_certs: Either::Left(ca_certs.as_ref().to_path_buf().into()), - mandatory: Default::default() - } + pub fn validate(&self) -> Result<(), crate::tls::Error> { + self.acceptor().map(|_| ()) } +} - /// Constructs a `MutualTls` from a byte buffer to a certificate authority - /// `ca_certs` DER-encoded X.509 TLS certificate chain. This method does no - /// validation; it simply creates a structure suitable for passing into a - /// [`TlsConfig`]. - /// - /// These certificates will be used to verify client-presented certificates - /// in TLS connections. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::MutualTls; - /// - /// # let ca_certs_buf = &[]; - /// let mtls_config = MutualTls::from_bytes(ca_certs_buf); - /// ``` - pub fn from_bytes(ca_certs: &[u8]) -> Self { - MutualTls { - ca_certs: Either::Right(ca_certs.to_vec()), - mandatory: Default::default() - } - } +impl CipherSuite { + /// The default set and order of cipher suites. These are all of the + /// variants in [`CipherSuite`] in their declaration order. + pub const DEFAULT_SET: [CipherSuite; 9] = [ + // TLS v1.3 suites... + CipherSuite::TLS_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_AES_256_GCM_SHA384, + CipherSuite::TLS_AES_128_GCM_SHA256, - /// Sets whether client authentication is required. Disabled by default. - /// - /// When `true`, client authentication will be required. TLS connections - /// where the client does not present a certificate will be immediately - /// terminated. When `false`, the client is not required to present a - /// certificate. In either case, if a certificate _is_ presented, it must be - /// valid or the connection is terminated. - /// - /// # Example - /// - /// ```rust - /// use rocket::config::MutualTls; - /// - /// # let ca_certs_buf = &[]; - /// let mtls_config = MutualTls::from_bytes(ca_certs_buf).mandatory(true); - /// ``` - pub fn mandatory(mut self, mandatory: bool) -> Self { - self.mandatory = mandatory; - self + // TLS v1.2 suites... + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ]; + + /// The default set and order of cipher suites. These are the TLS 1.3 + /// variants in [`CipherSuite`] in their declaration order. + pub const TLS_V13_SET: [CipherSuite; 3] = [ + CipherSuite::TLS_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_AES_256_GCM_SHA384, + CipherSuite::TLS_AES_128_GCM_SHA256, + ]; + + /// The default set and order of cipher suites. These are the TLS 1.2 + /// variants in [`CipherSuite`] in their declaration order. + pub const TLS_V12_SET: [CipherSuite; 6] = [ + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + ]; + + /// Used as the `serde` default for `ciphers`. + fn default_set() -> IndexSet { + Self::DEFAULT_SET.iter().copied().collect() } +} - /// Returns the value of the `ca_certs` parameter. - /// # Example - /// - /// ```rust - /// use rocket::config::MutualTls; - /// - /// # let ca_certs_buf = &[]; - /// let mtls_config = MutualTls::from_bytes(ca_certs_buf).mandatory(true); - /// assert_eq!(mtls_config.ca_certs().unwrap_right(), ca_certs_buf); - /// ``` - pub fn ca_certs(&self) -> either::Either { - match &self.ca_certs { - Either::Left(path) => either::Either::Left(path.relative()), - Either::Right(bytes) => either::Either::Right(&bytes), +impl From for rustls::SupportedCipherSuite { + fn from(cipher: CipherSuite) -> Self { + use rustls::crypto::ring::cipher_suite; + + match cipher { + CipherSuite::TLS_CHACHA20_POLY1305_SHA256 => + cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_AES_256_GCM_SHA384 => + cipher_suite::TLS13_AES_256_GCM_SHA384, + CipherSuite::TLS_AES_128_GCM_SHA256 => + cipher_suite::TLS13_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => + cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => + cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => + cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => + cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => + cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => + cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } } } -#[cfg(feature = "tls")] -mod with_tls_feature { - use std::fs; - use std::io::{self, Error}; - - use crate::http::tls::Config; - use crate::http::tls::rustls::SupportedCipherSuite as RustlsCipher; - use crate::http::tls::rustls::crypto::ring::cipher_suite; - - use yansi::Paint; - - use super::{Either, RelativePathBuf, TlsConfig, CipherSuite}; - - type Reader = Box; - - fn to_reader(value: &Either>) -> io::Result { - match value { - Either::Left(path) => { - let path = path.relative(); - let file = fs::File::open(&path).map_err(move |e| { +pub(crate) fn to_reader( + value: &Either> +) -> io::Result> { + match value { + Either::Left(path) => { + let path = path.relative(); + let file = std::fs::File::open(&path) + .map_err(move |e| { let source = figment::Source::File(path); - let msg = format!("error reading TLS file `{}`: {}", source.primary(), e); - Error::new(e.kind(), msg) + let msg = format!("error reading TLS file `{source}`: {e}"); + io::Error::new(e.kind(), msg) })?; - Ok(Box::new(io::BufReader::new(file))) - } - Either::Right(vec) => Ok(Box::new(io::Cursor::new(vec.clone()))), + Ok(Box::new(io::BufReader::new(file))) } + Either::Right(vec) => Ok(Box::new(io::Cursor::new(vec.clone()))), } +} - impl TlsConfig { - /// This is only called when TLS is enabled. - pub(crate) fn to_native_config(&self) -> io::Result> { - Ok(Config { - cert_chain: to_reader(&self.certs)?, - private_key: to_reader(&self.key)?, - ciphersuites: self.rustls_ciphers().collect(), - prefer_server_order: self.prefer_server_cipher_order, - #[cfg(not(feature = "mtls"))] - mandatory_mtls: false, - #[cfg(not(feature = "mtls"))] - ca_certs: None, - #[cfg(feature = "mtls")] - mandatory_mtls: self.mutual.as_ref().map_or(false, |m| m.mandatory), - #[cfg(feature = "mtls")] - ca_certs: match self.mutual { - Some(ref mtls) => Some(to_reader(&mtls.ca_certs)?), - None => None - }, - }) - } - - fn rustls_ciphers(&self) -> impl Iterator + '_ { - self.ciphers().map(|ciphersuite| match ciphersuite { - CipherSuite::TLS_CHACHA20_POLY1305_SHA256 => - cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_AES_256_GCM_SHA384 => - cipher_suite::TLS13_AES_256_GCM_SHA384, - CipherSuite::TLS_AES_128_GCM_SHA256 => - cipher_suite::TLS13_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => - cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => - cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => - cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => - cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - }) - } +#[cfg(test)] +mod tests { + use figment::{Figment, providers::{Toml, Format}}; + + #[test] + fn test_tls_config_from_file() { + use crate::tls::{TlsConfig, CipherSuite}; + use pretty_assertions::assert_eq; + + figment::Jail::expect_with(|jail| { + jail.create_file("Rocket.toml", r#" + [global] + shutdown.ctrlc = 0 + ident = false + + [global.tls] + certs = "/ssl/cert.pem" + key = "/ssl/key.pem" + + [global.limits] + forms = "1mib" + json = "10mib" + stream = "50kib" + "#)?; + + let config: TlsConfig = crate::Config::figment().extract_inner("tls")?; + assert_eq!(config, TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")); + + jail.create_file("Rocket.toml", r#" + [global.tls] + certs = "cert.pem" + key = "key.pem" + "#)?; + + let config: TlsConfig = crate::Config::figment().extract_inner("tls")?; + assert_eq!(config, TlsConfig::from_paths( + jail.directory().join("cert.pem"), + jail.directory().join("key.pem") + )); + + jail.create_file("TLS.toml", r#" + certs = "cert.pem" + key = "key.pem" + prefer_server_cipher_order = true + ciphers = [ + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + ] + "#)?; + + let config: TlsConfig = Figment::from(Toml::file("TLS.toml")).extract()?; + let cert_path = jail.directory().join("cert.pem"); + let key_path = jail.directory().join("key.pem"); + assert_eq!(config, TlsConfig::from_paths(cert_path, key_path) + .with_preferred_server_cipher_order(true) + .with_ciphers([ + CipherSuite::TLS_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_AES_256_GCM_SHA384, + CipherSuite::TLS_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + ])); + + jail.create_file("Rocket.toml", r#" + [global] + shutdown.ctrlc = 0 + ident = false + + [global.tls] + certs = "/ssl/cert.pem" + key = "/ssl/key.pem" + + [global.limits] + forms = "1mib" + json = "10mib" + stream = "50kib" + "#)?; + + let config: TlsConfig = crate::Config::figment().extract_inner("tls")?; + assert_eq!(config, TlsConfig::from_paths("/ssl/cert.pem", "/ssl/key.pem")); + + jail.create_file("Rocket.toml", r#" + [global.tls] + certs = "cert.pem" + key = "key.pem" + "#)?; + + let config: TlsConfig = crate::Config::figment().extract_inner("tls")?; + assert_eq!(config, TlsConfig::from_paths( + jail.directory().join("cert.pem"), + jail.directory().join("key.pem") + )); + + jail.create_file("Rocket.toml", r#" + [global.tls] + certs = "cert.pem" + key = "key.pem" + prefer_server_cipher_order = true + ciphers = [ + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + ] + "#)?; + + let config: TlsConfig = crate::Config::figment().extract_inner("tls")?; + let cert_path = jail.directory().join("cert.pem"); + let key_path = jail.directory().join("key.pem"); + assert_eq!(config, TlsConfig::from_paths(cert_path, key_path) + .with_preferred_server_cipher_order(true) + .with_ciphers([ + CipherSuite::TLS_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_AES_256_GCM_SHA384, + CipherSuite::TLS_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + ])); + + Ok(()) + }); } } diff --git a/core/http/src/tls/error.rs b/core/lib/src/tls/error.rs similarity index 94% rename from core/http/src/tls/error.rs rename to core/lib/src/tls/error.rs index 429f4a9d1f..ab2faf3e77 100644 --- a/core/http/src/tls/error.rs +++ b/core/lib/src/tls/error.rs @@ -11,6 +11,7 @@ pub enum KeyError { #[derive(Debug)] pub enum Error { Io(std::io::Error), + Bind(Box), Tls(rustls::Error), Mtls(rustls::server::VerifierBuilderError), CertChain(std::io::Error), @@ -29,6 +30,7 @@ impl std::fmt::Display for Error { CertChain(e) => write!(f, "failed to process certificate chain: {e}"), PrivKey(e) => write!(f, "failed to process private key: {e}"), CertAuth(e) => write!(f, "failed to process certificate authority: {e}"), + Bind(e) => write!(f, "failed to bind to network interface: {e}"), } } } @@ -66,6 +68,7 @@ impl std::error::Error for Error { Error::CertChain(e) => Some(e), Error::PrivKey(e) => Some(e), Error::CertAuth(e) => Some(e), + Error::Bind(e) => Some(&**e), } } } diff --git a/core/lib/src/tls/mod.rs b/core/lib/src/tls/mod.rs new file mode 100644 index 0000000000..d6128e3b71 --- /dev/null +++ b/core/lib/src/tls/mod.rs @@ -0,0 +1,7 @@ +mod error; +pub(crate) mod config; +pub(crate) mod util; + +pub use error::Result; +pub use config::{TlsConfig, CipherSuite}; +pub use error::Error; diff --git a/core/http/src/tls/util.rs b/core/lib/src/tls/util.rs similarity index 100% rename from core/http/src/tls/util.rs rename to core/lib/src/tls/util.rs diff --git a/core/lib/src/util/chain.rs b/core/lib/src/util/chain.rs new file mode 100644 index 0000000000..c60a193b0c --- /dev/null +++ b/core/lib/src/util/chain.rs @@ -0,0 +1,52 @@ +use std::io; +use std::task::{Poll, Context}; +use std::pin::Pin; + +use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, ReadBuf}; + +pin_project! { + /// Stream for the [`chain`](super::AsyncReadExt::chain) method. + #[must_use = "streams do nothing unless polled"] + pub struct Chain { + #[pin] + first: Option, + #[pin] + second: U, + } +} + +impl Chain { + pub(crate) fn new(first: T, second: U) -> Self { + Self { first: Some(first), second } + } +} + +impl Chain { + /// Gets references to the underlying readers in this `Chain`. + pub fn get_ref(&self) -> (Option<&T>, &U) { + (self.first.as_ref(), &self.second) + } +} + +impl AsyncRead for Chain { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let me = self.as_mut().project(); + if let Some(first) = me.first.as_pin_mut() { + let init_rem = buf.remaining(); + futures::ready!(first.poll_read(cx, buf))?; + if buf.remaining() == init_rem { + self.as_mut().project().first.set(None); + } else { + return Poll::Ready(Ok(())); + } + } + + let me = self.as_mut().project(); + me.second.poll_read(cx, buf) + } +} diff --git a/core/lib/src/util/join.rs b/core/lib/src/util/join.rs new file mode 100644 index 0000000000..d8ffc9d2d3 --- /dev/null +++ b/core/lib/src/util/join.rs @@ -0,0 +1,77 @@ +use std::pin::Pin; +use std::task::{Poll, Context}; + +use pin_project_lite::pin_project; + +use futures::stream::Stream; +use futures::ready; + +/// Join two streams, `a` and `b`, into a new `Join` stream that returns items +/// from both streams, biased to `a`, until `a` finishes. The joined stream +/// completes when `a` completes, irrespective of `b`. If `b` stops producing +/// values, then the joined stream acts exactly like a fused `a`. +/// +/// Values are biased to those of `a`: if `a` provides a value, it is always +/// emitted before a value provided by `b`. In other words, values from `b` are +/// emitted when and only when `a` is not producing a value. +pub fn join(a: A, b: B) -> Join { + Join { a, b: Some(b), done: false, } +} + +pin_project! { + /// Stream returned by [`join`]. + pub struct Join { + #[pin] + a: T, + #[pin] + b: Option, + // Set when `a` returns `None`. + done: bool, + } +} + +impl Stream for Join + where T: Stream, + U: Stream, +{ + type Item = T::Item; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if self.done { + return Poll::Ready(None); + } + + let me = self.as_mut().project(); + match me.a.poll_next(cx) { + Poll::Ready(opt) => { + *me.done = opt.is_none(); + Poll::Ready(opt) + }, + Poll::Pending => match me.b.as_pin_mut() { + None => Poll::Pending, + Some(b) => match ready!(b.poll_next(cx)) { + Some(value) => Poll::Ready(Some(value)), + None => { + self.as_mut().project().b.set(None); + Poll::Pending + } + } + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let (left_low, left_high) = self.a.size_hint(); + let (right_low, right_high) = self.b.as_ref() + .map(|b| b.size_hint()) + .unwrap_or_default(); + + let low = left_low.saturating_add(right_low); + let high = match (left_high, right_high) { + (Some(h1), Some(h2)) => h1.checked_add(h2), + _ => None, + }; + + (low, high) + } +} diff --git a/core/lib/src/util/mod.rs b/core/lib/src/util/mod.rs new file mode 100644 index 0000000000..d3055f36ce --- /dev/null +++ b/core/lib/src/util/mod.rs @@ -0,0 +1,12 @@ +mod chain; +mod tripwire; +mod reader_stream; +mod join; + +#[cfg(unix)] +pub mod unix; + +pub use chain::Chain; +pub use tripwire::TripWire; +pub use reader_stream::ReaderStream; +pub use join::join; diff --git a/core/lib/src/util/reader_stream.rs b/core/lib/src/util/reader_stream.rs new file mode 100644 index 0000000000..da0b1ab049 --- /dev/null +++ b/core/lib/src/util/reader_stream.rs @@ -0,0 +1,124 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::{Bytes, BytesMut}; +use futures::stream::Stream; +use pin_project_lite::pin_project; +use tokio::io::AsyncRead; + +pin_project! { + /// Convert an [`AsyncRead`] into a [`Stream`] of byte chunks. + /// + /// This stream is fused. It performs the inverse operation of + /// [`StreamReader`]. + /// + /// # Example + /// + /// ``` + /// # #[tokio::main] + /// # async fn main() -> std::io::Result<()> { + /// use tokio_stream::StreamExt; + /// use tokio_util::io::ReaderStream; + /// + /// // Create a stream of data. + /// let data = b"hello, world!"; + /// let mut stream = ReaderStream::new(&data[..]); + /// + /// // Read all of the chunks into a vector. + /// let mut stream_contents = Vec::new(); + /// while let Some(chunk) = stream.next().await { + /// stream_contents.extend_from_slice(&chunk?); + /// } + /// + /// // Once the chunks are concatenated, we should have the + /// // original data. + /// assert_eq!(stream_contents, data); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`AsyncRead`]: tokio::io::AsyncRead + /// [`StreamReader`]: crate::io::StreamReader + /// [`Stream`]: futures_core::Stream + #[derive(Debug)] + pub struct ReaderStream { + // Reader itself. + // + // This value is `None` if the stream has terminated. + #[pin] + reader: R, + // Working buffer, used to optimize allocations. + buf: BytesMut, + capacity: usize, + done: bool, + } +} + +impl ReaderStream { + /// Convert an [`AsyncRead`] into a [`Stream`] with item type + /// `Result`, + /// with a specific read buffer initial capacity. + /// + /// [`AsyncRead`]: tokio::io::AsyncRead + /// [`Stream`]: futures_core::Stream + pub fn with_capacity(reader: R, capacity: usize) -> Self { + ReaderStream { + reader: reader, + buf: BytesMut::with_capacity(capacity), + capacity, + done: false, + } + } +} + +impl Stream for ReaderStream { + type Item = std::io::Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use tokio_util::io::poll_read_buf; + + let mut this = self.as_mut().project(); + + if *this.done { + return Poll::Ready(None); + } + + if this.buf.capacity() == 0 { + this.buf.reserve(*this.capacity); + } + + let reader = this.reader; + match poll_read_buf(reader, cx, &mut this.buf) { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(err)) => { + *this.done = true; + Poll::Ready(Some(Err(err))) + } + Poll::Ready(Ok(0)) => { + *this.done = true; + Poll::Ready(None) + } + Poll::Ready(Ok(_)) => { + let chunk = this.buf.split(); + Poll::Ready(Some(Ok(chunk.freeze()))) + } + } + } + + // fn size_hint(&self) -> (usize, Option) { + // self.reader.size_hint() + // } +} + +impl hyper::body::Body for ReaderStream { + type Data = bytes::Bytes; + + type Error = std::io::Error; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + self.poll_next(cx).map_ok(hyper::body::Frame::data) + } +} diff --git a/core/lib/src/trip_wire.rs b/core/lib/src/util/tripwire.rs similarity index 100% rename from core/lib/src/trip_wire.rs rename to core/lib/src/util/tripwire.rs diff --git a/core/lib/src/util/unix.rs b/core/lib/src/util/unix.rs new file mode 100644 index 0000000000..8889b78d14 --- /dev/null +++ b/core/lib/src/util/unix.rs @@ -0,0 +1,25 @@ +use std::io; +use std::os::fd::AsRawFd; + +pub fn lock_exlusive_nonblocking(file: &T) -> io::Result<()> { + let raw_fd = file.as_raw_fd(); + let res = unsafe { + libc::flock(raw_fd, libc::LOCK_EX | libc::LOCK_NB) + }; + + match res { + 0 => Ok(()), + _ => Err(io::Error::last_os_error()), + } +} + +pub fn unlock_nonblocking(file: &T) -> io::Result<()> { + let res = unsafe { + libc::flock(file.as_raw_fd(), libc::LOCK_UN | libc::LOCK_NB) + }; + + match res { + 0 => Ok(()), + _ => Err(io::Error::last_os_error()), + } +} diff --git a/core/lib/tests/can-launch-tls.rs b/core/lib/tests/can-launch-tls.rs index fdd864b192..1fc4cfbef0 100644 --- a/core/lib/tests/can-launch-tls.rs +++ b/core/lib/tests/can-launch-tls.rs @@ -1,8 +1,9 @@ #![cfg(feature = "tls")] use rocket::fs::relative; -use rocket::config::{Config, TlsConfig, CipherSuite}; use rocket::local::asynchronous::Client; +use rocket::tls::{TlsConfig, CipherSuite}; +use rocket::figment::providers::Serialized; #[rocket::async_test] async fn can_launch_tls() { @@ -15,9 +16,8 @@ async fn can_launch_tls() { CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, ]); - let rocket = rocket::custom(Config { tls: Some(tls), ..Config::debug_default() }); - let client = Client::debug(rocket).await.unwrap(); - + let config = rocket::Config::figment().merge(Serialized::defaults(tls)); + let client = Client::debug(rocket::custom(config)).await.unwrap(); client.rocket().shutdown().notify(); client.rocket().shutdown().await; } diff --git a/core/lib/tests/on_launch_fairing_can_inspect_port.rs b/core/lib/tests/on_launch_fairing_can_inspect_port.rs index a0e35572f2..8060128027 100644 --- a/core/lib/tests/on_launch_fairing_can_inspect_port.rs +++ b/core/lib/tests/on_launch_fairing_can_inspect_port.rs @@ -1,3 +1,5 @@ +use std::net::{SocketAddr, Ipv4Addr}; + use rocket::config::Config; use rocket::fairing::AdHoc; use rocket::futures::channel::oneshot; @@ -5,13 +7,13 @@ use rocket::futures::channel::oneshot; #[rocket::async_test] async fn on_ignite_fairing_can_inspect_port() { let (tx, rx) = oneshot::channel(); - let rocket = rocket::custom(Config { port: 0, ..Config::debug_default() }) + let rocket = rocket::custom(Config::debug_default()) .attach(AdHoc::on_liftoff("Send Port -> Channel", move |rocket| { Box::pin(async move { - tx.send(rocket.config().port).unwrap(); + tx.send(rocket.endpoint().tcp().unwrap().port()).unwrap(); }) })); - rocket::tokio::spawn(rocket.launch()); + rocket::tokio::spawn(rocket.launch_on(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))); assert_ne!(rx.await.unwrap(), 0); } diff --git a/core/lib/tests/sentinel.rs b/core/lib/tests/sentinel.rs index f29f829d77..89f7cb5467 100644 --- a/core/lib/tests/sentinel.rs +++ b/core/lib/tests/sentinel.rs @@ -155,7 +155,7 @@ fn inner_sentinels_detected() { impl<'r, 'o: 'r> response::Responder<'r, 'o> for ResponderSentinel { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - todo!() + unimplemented!() } } diff --git a/core/lib/tests/tls-config-from-source-1503.rs b/core/lib/tests/tls-config-from-source-1503.rs index 92085f6861..3db9073140 100644 --- a/core/lib/tests/tls-config-from-source-1503.rs +++ b/core/lib/tests/tls-config-from-source-1503.rs @@ -8,19 +8,14 @@ macro_rules! relative { #[test] fn tls_config_from_source() { - use rocket::config::{Config, TlsConfig}; - use rocket::figment::Figment; + use rocket::tls::TlsConfig; + use rocket::figment::{Figment, providers::Serialized}; let cert_path = relative!("examples/tls/private/cert.pem"); let key_path = relative!("examples/tls/private/key.pem"); + let config = TlsConfig::from_paths(cert_path, key_path); - let rocket_config = Config { - tls: Some(TlsConfig::from_paths(cert_path, key_path)), - ..Default::default() - }; - - let config: Config = Figment::from(rocket_config).extract().unwrap(); - let tls = config.tls.expect("have TLS config"); + let tls: TlsConfig = Figment::from(Serialized::globals(config)).extract().unwrap(); assert_eq!(tls.certs().unwrap_left(), cert_path); assert_eq!(tls.key().unwrap_left(), key_path); } diff --git a/examples/config/src/tests.rs b/examples/config/src/tests.rs index 7cabb9dc51..e774f7ec05 100644 --- a/examples/config/src/tests.rs +++ b/examples/config/src/tests.rs @@ -6,15 +6,11 @@ async fn test_config(profile: &str) { let config = rocket.config(); match &*profile { "debug" => { - assert_eq!(config.address, std::net::Ipv4Addr::LOCALHOST); - assert_eq!(config.port, 8000); assert_eq!(config.workers, 1); assert_eq!(config.keep_alive, 0); assert_eq!(config.log_level, LogLevel::Normal); } "release" => { - assert_eq!(config.address, std::net::Ipv4Addr::LOCALHOST); - assert_eq!(config.port, 8000); assert_eq!(config.workers, 12); assert_eq!(config.keep_alive, 5); assert_eq!(config.log_level, LogLevel::Critical); diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 79b4588aca..0f8c55cb1a 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -74,19 +74,8 @@ fn hello(lang: Option, opt: Options<'_>) -> String { #[launch] fn rocket() -> _ { - use rocket::fairing::AdHoc; - rocket::build() .mount("/", routes![hello]) .mount("/hello", routes![world, mir]) .mount("/wave", routes![wave]) - .attach(AdHoc::on_request("Compatibility Normalizer", |req, _| Box::pin(async move { - if !req.uri().is_normalized_nontrailing() { - let normal = req.uri().clone().into_normalized_nontrailing(); - warn!("Incoming request URI was normalized for compatibility."); - info_!("{} -> {}", req.uri(), normal); - req.set_uri(normal); - } - }))) - } diff --git a/examples/tls/src/redirector.rs b/examples/tls/src/redirector.rs index aeffe9ad3a..e490ee1b1e 100644 --- a/examples/tls/src/redirector.rs +++ b/examples/tls/src/redirector.rs @@ -1,33 +1,38 @@ //! Redirect all HTTP requests to HTTPs. -use std::sync::OnceLock; +use std::net::SocketAddr; use rocket::http::Status; use rocket::log::LogLevel; -use rocket::{route, Error, Request, Data, Route, Orbit, Rocket, Ignite, Config}; +use rocket::{route, Error, Request, Data, Route, Orbit, Rocket, Ignite}; use rocket::fairing::{Fairing, Info, Kind}; use rocket::response::Redirect; +use yansi::Paint; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Redirector(u16); + #[derive(Debug, Clone)] -pub struct Redirector { - pub listen_port: u16, - pub tls_port: OnceLock, +pub struct Config { + server: rocket::Config, + tls_addr: SocketAddr, } impl Redirector { pub fn on(port: u16) -> Self { - Redirector { listen_port: port, tls_port: OnceLock::new() } + Redirector(port) } // Route function that gets called on every single request. fn redirect<'r>(req: &'r Request, _: Data<'r>) -> route::BoxFuture<'r> { // FIXME: Check the host against a whitelist! - let redirector = req.rocket().state::().expect("managed Self"); + let config = req.rocket().state::().expect("managed Self"); if let Some(host) = req.host() { let domain = host.domain(); - let https_uri = match redirector.tls_port.get() { - Some(443) | None => format!("https://{domain}{}", req.uri()), - Some(port) => format!("https://{domain}:{port}{}", req.uri()), + let https_uri = match config.tls_addr.port() { + 443 => format!("https://{domain}{}", req.uri()), + port => format!("https://{domain}:{port}{}", req.uri()), }; route::Outcome::from(req, Redirect::permanent(https_uri)).pin() @@ -37,21 +42,12 @@ impl Redirector { } // Launch an instance of Rocket than handles redirection on `self.port`. - pub async fn try_launch(self, mut config: Config) -> Result, Error> { - use yansi::Paint; + pub async fn try_launch(self, config: Config) -> Result, Error> { use rocket::http::Method::*; - // Determine the port TLS is being served on. - let tls_port = self.tls_port.get_or_init(|| config.port); - - // Adjust config for redirector: disable TLS, set port, disable logging. - config.tls = None; - config.port = self.listen_port; - config.log_level = LogLevel::Critical; - info!("{}{}", "๐Ÿ”’ ".mask(), "HTTP -> HTTPS Redirector:".magenta()); - info_!("redirecting on insecure port {} to TLS port {}", - self.listen_port.yellow(), tls_port.green()); + info_!("redirecting insecure port {} to TLS port {}", + self.0.yellow(), config.tls_addr.port().green()); // Build a vector of routes to `redirect` on `` for each method. let redirects = [Get, Put, Post, Delete, Options, Head, Trace, Connect, Patch] @@ -59,10 +55,11 @@ impl Redirector { .map(|m| Route::new(m, "/", Self::redirect)) .collect::>(); - rocket::custom(config) - .manage(self) + let addr = SocketAddr::new(config.tls_addr.ip(), self.0); + rocket::custom(&config.server) + .manage(config) .mount("/", redirects) - .launch() + .launch_on(addr) .await } } @@ -76,8 +73,24 @@ impl Fairing for Redirector { } } - async fn on_liftoff(&self, rkt: &Rocket) { - let (this, shutdown, config) = (self.clone(), rkt.shutdown(), rkt.config().clone()); + async fn on_liftoff(&self, rocket: &Rocket) { + let Some(tls_addr) = rocket.endpoint().tls().and_then(|tls| tls.tcp()) else { + info!("{}{}", "๐Ÿ”’ ".mask(), "HTTP -> HTTPS Redirector:".magenta()); + warn_!("Main instance is not being served over TLS/TCP."); + warn_!("Redirector refusing to start."); + return; + }; + + let config = Config { + tls_addr, + server: rocket::Config { + log_level: LogLevel::Critical, + ..rocket.config().clone() + }, + }; + + let this = *self; + let shutdown = rocket.shutdown(); let _ = rocket::tokio::spawn(async move { if let Err(e) = this.try_launch(config).await { error!("Failed to start HTTP -> HTTPS redirector."); diff --git a/examples/tls/src/tests.rs b/examples/tls/src/tests.rs index 2629e3c487..61efbec9ff 100644 --- a/examples/tls/src/tests.rs +++ b/examples/tls/src/tests.rs @@ -1,11 +1,21 @@ use std::fs::{self, File}; +use rocket::http::{CookieJar, Cookie}; use rocket::local::blocking::Client; use rocket::fs::relative; +#[get("/cookie")] +fn cookie(jar: &CookieJar<'_>) { + jar.add(("k1", "v1")); + jar.add_private(("k2", "v2")); + + jar.add(Cookie::build(("k1u", "v1u")).secure(false)); + jar.add_private(Cookie::build(("k2u", "v2u")).secure(false)); +} + #[test] fn hello_mutual() { - let client = Client::tracked(super::rocket()).unwrap(); + let client = Client::tracked_secure(super::rocket()).unwrap(); let cert_paths = fs::read_dir(relative!("private")).unwrap() .map(|entry| entry.unwrap().path().to_string_lossy().into_owned()) .filter(|path| path.ends_with("_cert.pem") && !path.ends_with("ca_cert.pem")); @@ -23,35 +33,43 @@ fn hello_mutual() { #[test] fn secure_cookies() { - use rocket::http::{CookieJar, Cookie}; - - #[get("/cookie")] - fn cookie(jar: &CookieJar<'_>) { - jar.add(("k1", "v1")); - jar.add_private(("k2", "v2")); - - jar.add(Cookie::build(("k1u", "v1u")).secure(false)); - jar.add_private(Cookie::build(("k2u", "v2u")).secure(false)); - } + let rocket = super::rocket().mount("/", routes![cookie]); + let client = Client::tracked_secure(rocket).unwrap(); - let client = Client::tracked(super::rocket().mount("/", routes![cookie])).unwrap(); let response = client.get("/cookie").dispatch(); - let c1 = response.cookies().get("k1").unwrap(); - assert_eq!(c1.secure(), Some(true)); - let c2 = response.cookies().get_private("k2").unwrap(); + let c3 = response.cookies().get("k1u").unwrap(); + let c4 = response.cookies().get_private("k2u").unwrap(); + + assert_eq!(c1.secure(), Some(true)); assert_eq!(c2.secure(), Some(true)); + assert_ne!(c3.secure(), Some(true)); + assert_ne!(c4.secure(), Some(true)); +} - let c1 = response.cookies().get("k1u").unwrap(); - assert_ne!(c1.secure(), Some(true)); +#[test] +fn insecure_cookies() { + let rocket = super::rocket().mount("/", routes![cookie]); + let client = Client::tracked(rocket).unwrap(); + + let response = client.get("/cookie").dispatch(); + let c1 = response.cookies().get("k1").unwrap(); + let c2 = response.cookies().get_private("k2").unwrap(); + let c3 = response.cookies().get("k1u").unwrap(); + let c4 = response.cookies().get_private("k2u").unwrap(); - let c2 = response.cookies().get_private("k2u").unwrap(); - assert_ne!(c2.secure(), Some(true)); + assert_eq!(c1.secure(), None); + assert_eq!(c2.secure(), None); + assert_eq!(c3.secure(), None); + assert_eq!(c4.secure(), None); } #[test] fn hello_world() { + use rocket::listener::DefaultListener; + use rocket::config::{Config, SecretKey}; + let profiles = [ "rsa_sha256", "ecdsa_nistp256_sha256_pkcs8", @@ -61,11 +79,20 @@ fn hello_world() { "ed25519", ]; - // TODO: Testing doesn't actually read keys since we don't do TLS locally. for profile in profiles { - let config = rocket::Config::figment().select(profile); - let client = Client::tracked(super::rocket().configure(config)).unwrap(); + let config = Config { + secret_key: SecretKey::generate().unwrap(), + ..Config::debug_default() + }; + + let figment = Config::figment().merge(config).select(profile); + let client = Client::tracked_secure(super::rocket().configure(figment)).unwrap(); let response = client.get("/").dispatch(); assert_eq!(response.into_string().unwrap(), "Hello, world!"); + + let figment = client.rocket().figment(); + let listener: DefaultListener = figment.extract().unwrap(); + assert_eq!(figment.profile(), profile); + listener.tls.as_ref().unwrap().validate().expect("valid TLS config"); } } diff --git a/examples/upgrade/static/index.html b/examples/upgrade/static/index.html index f4bf469445..78c1add1b4 100644 --- a/examples/upgrade/static/index.html +++ b/examples/upgrade/static/index.html @@ -14,7 +14,7 @@

    WebSocket Client Test