diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 2092ab17a..08112df86 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -29,7 +29,7 @@ actix-web = { version = "4", default-features = false } bitflags = "1" bytes = "1" derive_more = "0.99.5" -futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http-range = "0.1.4" log = "0.4" mime = "0.3" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 40327e5e8..0fbe39a8e 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -13,6 +13,7 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible, missing_docs, missing_debug_implementations)] +#![allow(clippy::uninlined_format_args)] use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 0a9ddf947..d70de6164 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -37,9 +37,8 @@ actix-rt = "2.2" actix-server = "2" awc = { version = "3", default-features = false } -base64 = "0.13" bytes = "1" -futures-core = { version = "0.3.7", default-features = false } +futures-core = { version = "0.3.17", default-features = false } http = "0.2.5" log = "0.4" socket2 = "0.4" @@ -48,7 +47,7 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tokio = { version = "1.8.4", features = ["sync"] } +tokio = { version = "1.18.4", features = ["sync"] } [dev-dependencies] actix-web = { version = "4", default-features = false, features = ["cookies"] } diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 8636ef9c4..a66f7b486 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -2,6 +2,7 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] +#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] @@ -87,6 +88,7 @@ pub async fn test_server_with_addr>( // notify TestServer that server and system have shut down // all thread managed resources should be dropped at this point + #[allow(clippy::let_underscore_future)] let _ = thread_stop_tx.send(()); }); @@ -294,6 +296,7 @@ impl Drop for TestServer { // without needing to await anything // signal server to stop + #[allow(clippy::let_underscore_future)] let _ = self.server.stop(true); // signal system to stop diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 045ae461f..fa254ccf2 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,14 +2,35 @@ ## Unreleased - 2022-xx-xx ### Added +- Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959] - Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868] - Implement `MessageBody` for `Pin` where `B::Target: MessageBody`. [#2868] +- Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957] +- `HeaderMap::retain()` [#2955]. +- Header name constants in `header` module. [#2956] [#2968] + - `CACHE_STATUS` + - `CDN_CACHE_CONTROL` + - `CROSS_ORIGIN_EMBEDDER_POLICY` + - `CROSS_ORIGIN_OPENER_POLICY` + - `PERMISSIONS_POLICY` + - `X_FORWARDED_FOR` + - `X_FORWARDED_HOST` + - `X_FORWARDED_PROTO` + +### Fixed +- Fix non-empty body of HTTP/2 HEAD responses. [#2920] ### Performance - Improve overall performance of operations on `Extensions`. [#2890] +[#2959]: https://github.com/actix/actix-web/pull/2959 [#2868]: https://github.com/actix/actix-web/pull/2868 [#2890]: https://github.com/actix/actix-web/pull/2890 +[#2920]: https://github.com/actix/actix-web/pull/2920 +[#2957]: https://github.com/actix/actix-web/pull/2957 +[#2955]: https://github.com/actix/actix-web/pull/2955 +[#2956]: https://github.com/actix/actix-web/pull/2956 +[#2968]: https://github.com/actix/actix-web/pull/2968 ## 3.2.2 - 2022-09-11 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a8b888ef4..f10a069c6 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -67,7 +67,7 @@ bytes = "1" bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" -futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http = "0.2.5" httparse = "1.5.1" httpdate = "1.0.1" @@ -77,7 +77,7 @@ mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" smallvec = "1.6.1" -tokio = { version = "1.13.1", features = [] } +tokio = { version = "1.18.4", features = [] } tokio-util = { version = "0.7", features = ["io", "codec"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] } @@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true } # websockets local-channel = { version = "0.1", optional = true } -base64 = { version = "0.13", optional = true } +base64 = { version = "0.21", optional = true } rand = { version = "0.8", optional = true } sha1 = { version = "0.10", optional = true } @@ -96,7 +96,7 @@ actix-tls = { version = "3", default-features = false, optional = true } # compress-* brotli = { version = "3.3.3", optional = true } flate2 = { version = "1.0.13", optional = true } -zstd = { version = "0.11", optional = true } +zstd = { version = "0.12", optional = true } [dev-dependencies] actix-http-test = { version = "3", features = ["openssl"] } @@ -105,9 +105,9 @@ actix-tls = { version = "3", features = ["openssl"] } actix-web = "4" async-stream = "0.3" -criterion = { version = "0.3", features = ["html_reports"] } +criterion = { version = "0.4", features = ["html_reports"] } env_logger = "0.9" -futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } memchr = "2.4" once_cell = "1.9" rcgen = "0.9" @@ -119,7 +119,7 @@ serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } +tokio = { version = "1.18.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" diff --git a/actix-http/benches/quality-value.rs b/actix-http/benches/quality-value.rs index 33ba9c4c8..0ed274ded 100644 --- a/actix-http/benches/quality-value.rs +++ b/actix-http/benches/quality-value.rs @@ -1,3 +1,5 @@ +#![allow(clippy::uninlined_format_args)] + use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; const CODES: &[u16] = &[0, 1000, 201, 800, 550]; diff --git a/actix-http/examples/h2c-detect.rs b/actix-http/examples/h2c-detect.rs new file mode 100644 index 000000000..aa3dd5d31 --- /dev/null +++ b/actix-http/examples/h2c-detect.rs @@ -0,0 +1,29 @@ +//! An example that supports automatic selection of plaintext h1/h2c connections. +//! +//! Notably, both the following commands will work. +//! ```console +//! $ curl --http1.1 'http://localhost:8080/' +//! $ curl --http2-prior-knowledge 'http://localhost:8080/' +//! ``` + +use std::{convert::Infallible, io}; + +use actix_http::{HttpService, Request, Response, StatusCode}; +use actix_server::Server; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("h2c-detect", ("127.0.0.1", 8080), || { + HttpService::build() + .finish(|_req: Request| async move { + Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!")) + }) + .tcp_auto_h2c() + })? + .workers(2) + .run() + .await +} diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 0cfaa8653..e274cf8aa 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -120,7 +120,7 @@ pub trait MessageBody { } mod foreign_impls { - use std::ops::DerefMut; + use std::{borrow::Cow, ops::DerefMut}; use super::*; @@ -324,6 +324,39 @@ mod foreign_impls { } } + impl MessageBody for Cow<'static, [u8]> { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = match mem::take(self.get_mut()) { + Cow::Borrowed(b) => Bytes::from_static(b), + Cow::Owned(b) => Bytes::from(b), + }; + Poll::Ready(Some(Ok(bytes))) + } + } + + #[inline] + fn try_into_bytes(self) -> Result { + match self { + Cow::Borrowed(b) => Ok(Bytes::from_static(b)), + Cow::Owned(b) => Ok(Bytes::from(b)), + } + } + } + impl MessageBody for &'static str { type Error = Infallible; @@ -379,6 +412,39 @@ mod foreign_impls { } } + impl MessageBody for Cow<'static, str> { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = match mem::take(self.get_mut()) { + Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()), + Cow::Owned(s) => Bytes::from(s.into_bytes()), + }; + Poll::Ready(Some(Ok(bytes))) + } + } + + #[inline] + fn try_into_bytes(self) -> Result { + match self { + Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())), + Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())), + } + } + } + impl MessageBody for bytestring::ByteString { type Error = Infallible; diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index e5e27b287..08cd81a0d 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -44,7 +44,7 @@ where #[inline] fn size(&self) -> BodySize { - BodySize::Sized(self.size as u64) + BodySize::Sized(self.size) } /// Attempts to pull out the next value of the underlying [`Stream`]. diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 71b933835..e2693acaf 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -186,7 +186,7 @@ where self } - /// Finish service configuration and create a HTTP Service for HTTP/1 protocol. + /// Finish service configuration and create a service for the HTTP/1 protocol. pub fn h1(self, service: F) -> H1Service where B: MessageBody, @@ -209,7 +209,7 @@ where .on_connect_ext(self.on_connect_ext) } - /// Finish service configuration and create a HTTP service for HTTP/2 protocol. + /// Finish service configuration and create a service for the HTTP/2 protocol. #[cfg(feature = "http2")] #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn h2(self, service: F) -> crate::h2::H2Service diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index 4005ed892..fc9081b81 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -71,7 +71,7 @@ impl ChunkedState { match size.checked_mul(radix) { Some(n) => { - *size = n as u64; + *size = n; *size += rem as u64; Poll::Ready(Ok(ChunkedState::Size)) diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs index 3eea859bf..d39c5bd69 100644 --- a/actix-http/src/h1/dispatcher_tests.rs +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -64,7 +64,7 @@ fn drop_payload_service( fn echo_payload_service() -> impl Service, Error = Error> { fn_service(|mut req: Request| { Box::pin(async move { - use futures_util::stream::StreamExt as _; + use futures_util::StreamExt as _; let mut pl = req.take_payload(); let mut body = BytesMut::new(); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 21cfd75c4..abe396ce2 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -450,7 +450,7 @@ impl TransferEncoding { buf.extend_from_slice(&msg[..len as usize]); - *remaining -= len as u64; + *remaining -= len; Ok(*remaining == 0) } else { Ok(true) diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 680936f0f..3e618820e 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -29,7 +29,7 @@ use crate::{ HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }, service::HttpFlow, - Extensions, OnConnectData, Payload, Request, Response, ResponseHead, + Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead, }; const CHUNK_SIZE: usize = 16_384; @@ -118,6 +118,7 @@ where let payload = crate::h2::Payload::new(body); let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); + let head_req = parts.method == Method::HEAD; let head = req.head_mut(); head.uri = parts.uri; @@ -135,10 +136,10 @@ where actix_rt::spawn(async move { // resolve service call and send response. let res = match fut.await { - Ok(res) => handle_response(res.into(), tx, config).await, + Ok(res) => handle_response(res.into(), tx, config, head_req).await, Err(err) => { let res: Response = err.into(); - handle_response(res, tx, config).await + handle_response(res, tx, config, head_req).await } }; @@ -206,6 +207,7 @@ async fn handle_response( res: Response, mut tx: SendResponse, config: ServiceConfig, + head_req: bool, ) -> Result<(), DispatchError> where B: MessageBody, @@ -215,14 +217,14 @@ where // prepare response. let mut size = body.size(); let res = prepare_response(config, res.head(), &mut size); - let eof = size.is_eof(); + let eof_or_head = size.is_eof() || head_req; // send response head and return on eof. let mut stream = tx - .send_response(res, eof) + .send_response(res, eof_or_head) .map_err(DispatchError::SendResponse)?; - if eof { + if eof_or_head { return Ok(()); } diff --git a/actix-http/src/header/common.rs b/actix-http/src/header/common.rs new file mode 100644 index 000000000..67b0a9069 --- /dev/null +++ b/actix-http/src/header/common.rs @@ -0,0 +1,51 @@ +//! Common header names not defined in [`http`]. +//! +//! Any headers added to this file will need to be re-exported from the list at `crate::headers`. + +use http::header::HeaderName; + +/// Response header field that indicates how caches have handled that response and its corresponding +/// request. +/// +/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics. +pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status"); + +/// Response header field that allows origin servers to control the behavior of CDN caches +/// interposed between them and clients separately from other caches that might handle the response. +/// +/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics. +pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control"); + +/// Response header that prevents a document from loading any cross-origin resources that don't +/// explicitly grant the document permission (using [CORP] or [CORS]). +/// +/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP) +/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName = + HeaderName::from_static("cross-origin-embedder-policy"); + +/// Response header that allows you to ensure a top-level document does not share a browsing context +/// group with cross-origin documents. +pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName = + HeaderName::from_static("cross-origin-opener-policy"); + +/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site +/// requests to the given resource. +pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName = + HeaderName::from_static("cross-origin-resource-policy"); + +/// Response header that provides a mechanism to allow and deny the use of browser features in a +/// document or within any `