1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-03 17:41:30 +02:00

Compare commits

...

43 Commits

Author SHA1 Message Date
dd8692e000 add send error response state
add changelog

add send error response state

remove comment
2022-07-24 15:27:54 +01:00
e0a88cea8d remove unwindsafe assertions 2022-07-24 02:47:12 +01:00
d78ff283af prepare actix-test release 0.1.0 2022-07-24 02:13:46 +01:00
ce6d520215 prepare actix-http-test release 3.0.0 2022-07-24 02:11:21 +01:00
3e25742a41 prepare actix-files release 0.6.2 2022-07-23 16:37:59 +01:00
20f4cfe6b5 fix partial ranges for video content (#2817)
fixes #2815
2022-07-23 16:27:01 +01:00
6408291ab0 appease clippy by deriving Eq on a bunch of items (#2818) 2022-07-23 16:26:48 +01:00
8d260e599f clippy 2022-07-23 02:48:28 +01:00
14bcf72ec1 web utilizes const header names 2022-07-22 20:21:58 +01:00
6485434a33 update bump script 2022-07-22 20:19:15 +01:00
16c7c16463 reduce scope of once_cell change 2022-07-22 20:19:02 +01:00
9b0fdca6e9 Remove some unnecessary uses of once_cell::sync::Lazy (#2816) 2022-07-22 20:18:38 +01:00
8759d79b03 routes macro allowing multiple paths per handler (#2718)
* WIP: basic implementation for `routes` macro

* chore: changelog, docs, tests

* error on missing methods

* Apply suggestions from code review

Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>

* update test stderr expectation

* add additional tests

* fix stderr output

* remove useless ResourceType

this is dead code from back when .to and .to_async were different ways to add a service

Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-07-04 04:31:49 +00:00
c0d5d7bdb5 add octal-ish CL test 2022-07-02 21:04:37 +01:00
40eab1f091 simplify simple decoder tests 2022-07-02 20:07:27 +01:00
75517cce82 install cargo hack in CI faster 2022-07-02 20:00:59 +01:00
9b51624b27 update cargo-cache to 0.8.2 2022-07-02 18:43:19 +01:00
8e2ae8cd40 install nextest faster 2022-07-02 18:38:08 +01:00
9a2f8450e0 install older cargo-edit 2022-07-02 17:40:03 +01:00
23ef51609e s/cargo-add/cargo-edit 2022-07-02 17:29:06 +01:00
f7d629a61a fix cargo-add in CI 2022-07-02 17:20:46 +01:00
e0845d9ad9 add msrv workarounds to ci 2022-07-02 17:12:24 +01:00
2f79daec16 only run tests on stable 2022-07-02 17:05:48 +01:00
f3f41a0cc7 prepare actix-http release 3.2.1 2022-07-02 16:50:54 +01:00
987067698b use sparse registry in CI 2022-07-01 12:45:26 +01:00
b62f1b4ef7 migrate deprecated method in docs 2022-07-01 12:40:00 +01:00
df5257c373 update trust dns resolver 2022-07-01 10:21:46 +01:00
226ea696ce update dev deps 2022-07-01 10:19:28 +01:00
e524fc86ea add HTTP/0.9 rejection test 2022-07-01 09:03:57 +01:00
7e990e423f add http/1.0 GET parsing tests 2022-07-01 08:24:45 +01:00
8f9a12ed5d fix parsing ambiguities for HTTP/1.0 requests (#2794)
* fix HRS vuln when first CL header is 0

* ignore TE headers in http/1.0 reqs

* update changelog

* disallow HTTP/1.0 requests without a CL header

* fix test

* broken fix for http1.0 post requests
2022-07-01 08:23:40 +01:00
c6eba2da9b prepare actix-http release 3.2.0 (#2801) 2022-07-01 06:16:17 +01:00
06c7945801 retain previously set vary headers when using compress (#2798)
* retain previously set vary headers when using compress
2022-06-30 09:19:16 +01:00
0dba6310c6 Expose option for setting TLS handshake timeout (#2752)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-06-27 02:57:21 +00:00
f7d7d92984 address clippy lints 2022-06-27 03:12:36 +01:00
3d6ea7fe9b Improve documentation for actix-web-actors (#2788) 2022-06-26 16:45:02 +00:00
8dbf7da89f Fix common grammar mistakes and add small documentation for AppConfig's Default implementation (#2793) 2022-06-25 14:01:06 +00:00
de92b3be2e fix unrecoverable Err(Overflow) in websocket frame parser (#2790) 2022-06-24 03:46:17 +00:00
5d0e8138ee Add getters for &ServiceRequest (#2786) 2022-06-22 21:02:03 +01:00
6b7196225e Bump up MSRV to 1.57 (#2789) 2022-06-22 12:08:06 +01:00
265fa0d050 Add link to MongoDB example in README (#2783) 2022-06-15 22:38:10 +01:00
062127a210 Revert "actix-http: Pull actix-web dev-dep from Git repo"
This reverts commit 3926416580.
2022-06-12 00:55:06 +09:00
3926416580 actix-http: Pull actix-web dev-dep from Git repo
The published version of actix-web depends on a buggy version of zstd crate,
temporarily use actix-web on git repo to avoid the build failure.

Signed-off-by: Yuki Okushi <jtitor@2k36.org>
2022-06-12 00:48:08 +09:00
94 changed files with 1475 additions and 500 deletions

View File

@ -23,6 +23,7 @@ jobs:
CI: 1 CI: 1
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1 VCPKGRS_DYNAMIC: 1
CARGO_UNSTABLE_SPARSE_REGISTRY: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -44,18 +45,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check minimal - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-min } with: { command: ci-check-min }
@ -80,12 +78,17 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
cargo-cache cargo-cache
ci_feature_powerset_check: ci_feature_powerset_check:
name: Verify Feature Combinations name: Verify Feature Combinations
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: 1
CARGO_INCREMENTAL: 0
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -96,18 +99,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check feature combinations - name: check feature combinations
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset } with: { command: ci-check-all-feature-powerset }
@ -119,6 +119,11 @@ jobs:
nextest: nextest:
name: nextest name: nextest
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: 1
CARGO_INCREMENTAL: 0
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -129,18 +134,15 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0 uses: Swatinem/rust-cache@v1.3.0
- name: Install cargo-nextest
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-nextest
- name: Test with cargo-nextest - name: Test with cargo-nextest
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:

View File

@ -16,7 +16,7 @@ jobs:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
version: version:
- 1.56.0 # MSRV - 1.57.0 # MSRV
- stable - stable
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
@ -47,18 +47,22 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Install cargo-hack
uses: taiki-e/install-action@cargo-hack
- name: workaround MSRV issues
if: matrix.version != 'stable'
run: |
cargo install cargo-edit --version=0.8.0
cargo add const-str@0.3 --dev -p=actix-web
cargo add const-str@0.3 --dev -p=awc
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: generate-lockfile } with: { command: generate-lockfile }
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check minimal - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-check-min } with: { command: ci-check-min }
@ -83,7 +87,7 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
cargo-cache cargo-cache
io-uring: io-uring:

View File

@ -1,6 +1,13 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
## 0.6.2 - 2022-07-23
- Allow partial range responses for video content to start streaming sooner. [#2817]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2817]: https://github.com/actix/actix-web/pull/2817
## 0.6.1 - 2022-06-11 ## 0.6.1 - 2022-06-11

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.1" version = "0.6.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -46,6 +46,6 @@ actix-server = { version = "2.1", optional = true } # ensure matching tokio-urin
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
actix-web = "4" actix-web = "4"
tempfile = "3.2" tempfile = "3.2"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.1)](https://docs.rs/actix-files/0.6.1) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.2)](https://docs.rs/actix-files/0.6.2)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.1/status.svg)](https://deps.rs/crate/actix-files/0.6.1) [![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display; use derive_more::Display;
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
#[derive(Display, Debug, PartialEq)] #[derive(Debug, PartialEq, Eq, Display)]
pub enum FilesError { pub enum FilesError {
/// Path is not a directory /// Path is not a directory
#[allow(dead_code)] #[allow(dead_code)]
@ -22,7 +22,7 @@ impl ResponseError for FilesError {
} }
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
#[derive(Display, Debug, PartialEq)] #[derive(Debug, PartialEq, Eq, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum UriSegmentError { pub enum UriSegmentError {
/// The segment started with the wrapped invalid character. /// The segment started with the wrapped invalid character.

View File

@ -528,11 +528,26 @@ impl NamedFile {
length = ranges[0].length; length = ranges[0].length;
offset = ranges[0].start; offset = ranges[0].start;
// When a Content-Encoding header is present in a 206 partial content response
// for video content, it prevents browser video players from starting playback
// before loading the whole video and also prevents seeking.
//
// See: https://github.com/actix/actix-web/issues/2815
//
// The assumption of this fix is that the video player knows to not send an
// Accept-Encoding header for this request and that downstream middleware will
// not attempt compression for requests without it.
//
// TODO: Solve question around what to do if self.encoding is set and partial
// range is requested. Reject request? Ignoring self.encoding seems wrong, too.
// In practice, it should not come up.
if req.headers().contains_key(&header::ACCEPT_ENCODING) {
// don't allow compression middleware to modify partial content // don't allow compression middleware to modify partial content
res.insert_header(( res.insert_header((
header::CONTENT_ENCODING, header::CONTENT_ENCODING,
HeaderValue::from_static("identity"), HeaderValue::from_static("identity"),
)); ));
}
res.insert_header(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,

View File

@ -1,11 +1,11 @@
use actix_files::Files; use actix_files::{Files, NamedFile};
use actix_web::{ use actix_web::{
http::{ http::{
header::{self, HeaderValue}, header::{self, HeaderValue},
StatusCode, StatusCode,
}, },
test::{self, TestRequest}, test::{self, TestRequest},
App, web, App,
}; };
#[actix_web::test] #[actix_web::test]
@ -36,3 +36,31 @@ async fn test_utf8_file_contents() {
Some(&HeaderValue::from_static("text/plain")), Some(&HeaderValue::from_static("text/plain")),
); );
} }
#[actix_web::test]
async fn partial_range_response_encoding() {
let srv = test::init_service(App::new().default_service(web::to(|| async {
NamedFile::open_async("./tests/test.binary").await.unwrap()
})))
.await;
// range request without accept-encoding returns no content-encoding header
let req = TestRequest::with_uri("/")
.append_header((header::RANGE, "bytes=10-20"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
// range request with accept-encoding returns a content-encoding header
let req = TestRequest::with_uri("/")
.append_header((header::RANGE, "bytes=10-20"))
.append_header((header::ACCEPT_ENCODING, "identity"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(
res.headers().get(header::CONTENT_ENCODING).unwrap(),
"identity"
);
}

View File

@ -1,9 +1,24 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
## 3.0.0 - 2022-07-24
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Added `TestServer::client_headers` method. [#2097]
- Update `actix-server` dependency to `2`.
- Update `actix-tls` dependency to `3`.
- Update `bytes` to `1.0`. [#1813]
- Minimum supported Rust version (MSRV) is now 1.57.
[#2442]: https://github.com/actix/actix-web/pull/2442
[#2097]: https://github.com/actix/actix-web/pull/2097
[#1813]: https://github.com/actix/actix-web/pull/1813
<details>
<summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.13 - 2022-02-16 ## 3.0.0-beta.13 - 2022-02-16
- No significant changes since `3.0.0-beta.12`. - No significant changes since `3.0.0-beta.12`.
@ -69,6 +84,7 @@
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
</details>
## 2.1.0 - 2020-11-25 ## 2.1.0 - 2020-11-25
- Add ability to set address for `TestServer`. [#1645] - Add ability to set address for `TestServer`. [#1645]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.13" version = "3.0.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]

View File

@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http-test/3.0.0-beta.13) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0)](https://docs.rs/actix-http-test/3.0.0)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.13) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -1,6 +1,29 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
### Fixed
- Dropping the payload early and causing unclean connections no longer causes erroneous 500 responses. [#2745]
[#2745]: https://github.com/actix/actix-web/issues/2745
## 3.2.1 - 2022-07-02
### Fixed
- Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794]
[#2794]: https://github.com/actix/actix-web/pull/2794
## 3.2.0 - 2022-06-30
### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
### Fixed
- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790]
- Retain previously set Vary headers when using compression encoder. [#2798]
[#2790]: https://github.com/actix/actix-web/pull/2790
[#2798]: https://github.com/actix/actix-web/pull/2798
## 3.1.0 - 2022-06-11 ## 3.1.0 - 2022-06-11

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.1.0" version = "3.2.1"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -97,7 +97,7 @@ flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.11", optional = true } zstd = { version = "0.11", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-tls = { version = "3", features = ["openssl"] } actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4" actix-web = "4"
@ -108,10 +108,10 @@ env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.9"
rcgen = "0.8" rcgen = "0.9"
regex = "1.3" regex = "1.3"
rustversion = "1" rustversion = "1"
rustls-pemfile = "0.2" rustls-pemfile = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
static_assertions = "1" static_assertions = "1"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.1.0)](https://docs.rs/actix-http/3.1.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.2.1)](https://docs.rs/actix-http/3.2.1)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.1.0/status.svg)](https://deps.rs/crate/actix-http/3.1.0) [![dependency status](https://deps.rs/crate/actix-http/3.2.1/status.svg)](https://deps.rs/crate/actix-http/3.2.1)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -481,6 +481,7 @@ mod tests {
assert_poll_next_none!(pl); assert_poll_next_none!(pl);
} }
#[allow(clippy::let_unit_value)]
#[actix_rt::test] #[actix_rt::test]
async fn test_unit() { async fn test_unit() {
let pl = (); let pl = ();

View File

@ -257,7 +257,7 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut() head.headers_mut()
.insert(header::CONTENT_ENCODING, encoding.to_header_value()); .insert(header::CONTENT_ENCODING, encoding.to_header_value());
head.headers_mut() head.headers_mut()
.insert(header::VARY, HeaderValue::from_static("accept-encoding")); .append(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false); head.no_chunking(false);
} }

View File

@ -388,7 +388,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type. /// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentTypeError { pub enum ContentTypeError {
/// Can not parse content type /// Can not parse content type

View File

@ -15,7 +15,7 @@ macro_rules! byte (
}) })
); );
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ChunkedState { pub(super) enum ChunkedState {
Size, Size,
SizeLws, SizeLws,

View File

@ -46,6 +46,23 @@ pub(crate) enum PayloadLength {
None, None,
} }
impl PayloadLength {
/// Returns true if variant is `None`.
fn is_none(&self) -> bool {
matches!(self, Self::None)
}
/// Returns true if variant is represents zero-length (not none) payload.
fn is_zero(&self) -> bool {
matches!(
self,
PayloadLength::Payload(PayloadType::Payload(PayloadDecoder {
kind: Kind::Length(0)
}))
)
}
}
pub(crate) trait MessageType: Sized { pub(crate) trait MessageType: Sized {
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>); fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
@ -59,6 +76,7 @@ pub(crate) trait MessageType: Sized {
&mut self, &mut self,
slice: &Bytes, slice: &Bytes,
raw_headers: &[HeaderIndex], raw_headers: &[HeaderIndex],
version: Version,
) -> Result<PayloadLength, ParseError> { ) -> Result<PayloadLength, ParseError> {
let mut ka = None; let mut ka = None;
let mut has_upgrade_websocket = false; let mut has_upgrade_websocket = false;
@ -87,21 +105,23 @@ pub(crate) trait MessageType: Sized {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
header::CONTENT_LENGTH => match value.to_str() { header::CONTENT_LENGTH => match value.to_str().map(str::trim) {
Ok(s) if s.trim().starts_with('+') => { Ok(val) if val.starts_with('+') => {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
Ok(s) => {
if let Ok(len) = s.parse::<u64>() { Ok(val) => {
if len != 0 { if let Ok(len) = val.parse::<u64>() {
// accept 0 lengths here and remove them in `decode` after all
// headers have been processed to prevent request smuggling issues
content_length = Some(len); content_length = Some(len);
}
} else { } else {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} }
Err(_) => { Err(_) => {
debug!("illegal Content-Length: {:?}", value); debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header); return Err(ParseError::Header);
@ -114,22 +134,23 @@ pub(crate) trait MessageType: Sized {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
header::TRANSFER_ENCODING => { header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
seen_te = true; seen_te = true;
if let Ok(s) = value.to_str().map(str::trim) { if let Ok(val) = value.to_str().map(str::trim) {
if s.eq_ignore_ascii_case("chunked") { if val.eq_ignore_ascii_case("chunked") {
chunked = true; chunked = true;
} else if s.eq_ignore_ascii_case("identity") { } else if val.eq_ignore_ascii_case("identity") {
// allow silently since multiple TE headers are already checked // allow silently since multiple TE headers are already checked
} else { } else {
debug!("illegal Transfer-Encoding: {:?}", s); debug!("illegal Transfer-Encoding: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} else { } else {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} }
// connection keep-alive state // connection keep-alive state
header::CONNECTION => { header::CONNECTION => {
ka = if let Ok(conn) = value.to_str().map(str::trim) { ka = if let Ok(conn) = value.to_str().map(str::trim) {
@ -146,6 +167,7 @@ pub(crate) trait MessageType: Sized {
None None
}; };
} }
header::UPGRADE => { header::UPGRADE => {
if let Ok(val) = value.to_str().map(str::trim) { if let Ok(val) = value.to_str().map(str::trim) {
if val.eq_ignore_ascii_case("websocket") { if val.eq_ignore_ascii_case("websocket") {
@ -153,19 +175,23 @@ pub(crate) trait MessageType: Sized {
} }
} }
} }
header::EXPECT => { header::EXPECT => {
let bytes = value.as_bytes(); let bytes = value.as_bytes();
if bytes.len() >= 4 && &bytes[0..4] == b"100-" { if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
expect = true; expect = true;
} }
} }
_ => {} _ => {}
} }
headers.append(name, value); headers.append(name, value);
} }
} }
self.set_connection_type(ka); self.set_connection_type(ka);
if expect { if expect {
self.set_expect() self.set_expect()
} }
@ -249,7 +275,22 @@ impl MessageType for Request {
let mut msg = Request::new(); let mut msg = Request::new();
// convert headers // convert headers
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
debug!("no Content-Length specified for HTTP/1.0 POST request");
return Err(ParseError::Header);
}
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks.
// See https://github.com/actix/actix-web/issues/2767.
if length.is_zero() {
length = PayloadLength::None;
}
// payload decoder // payload decoder
let decoder = match length { let decoder = match length {
@ -337,7 +378,15 @@ impl MessageType for ResponseHead {
msg.version = ver; msg.version = ver;
// convert headers // convert headers
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks.
// See https://github.com/actix/actix-web/issues/2767.
if length.is_zero() {
length = PayloadLength::None;
}
// message payload // message payload
let decoder = if let PayloadLength::Payload(pl) = length { let decoder = if let PayloadLength::Payload(pl) = length {
@ -391,7 +440,7 @@ impl HeaderIndex {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
/// Chunk type yielded while decoding a payload. /// Chunk type yielded while decoding a payload.
pub enum PayloadItem { pub enum PayloadItem {
Chunk(Bytes), Chunk(Bytes),
@ -401,7 +450,7 @@ pub enum PayloadItem {
/// Decoder that can handle different payload types. /// Decoder that can handle different payload types.
/// ///
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`. /// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayloadDecoder { pub struct PayloadDecoder {
kind: Kind, kind: Kind,
} }
@ -427,7 +476,7 @@ impl PayloadDecoder {
} }
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
enum Kind { enum Kind {
/// A reader used when a `Content-Length` header is passed with a positive integer. /// A reader used when a `Content-Length` header is passed with a positive integer.
Length(u64), Length(u64),
@ -606,14 +655,100 @@ mod tests {
} }
#[test] #[test]
fn test_parse_post() { fn parse_h09_reject() {
let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); let mut buf = BytesMut::from(
"GET /test1 HTTP/0.9\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
reader.decode(&mut buf).unwrap_err();
let mut buf = BytesMut::from(
"POST /test2 HTTP/0.9\r\n\
Content-Length: 3\r\n\
\r\n
abc",
);
let mut reader = MessageDecoder::<Request>::default();
reader.decode(&mut buf).unwrap_err();
}
#[test]
fn parse_h10_get() {
let mut buf = BytesMut::from(
"GET /test1 HTTP/1.0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test1");
let mut buf = BytesMut::from(
"GET /test2 HTTP/1.0\r\n\
Content-Length: 0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test2");
let mut buf = BytesMut::from(
"GET /test3 HTTP/1.0\r\n\
Content-Length: 3\r\n\
\r\n
abc",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test3");
}
#[test]
fn parse_h10_post() {
let mut buf = BytesMut::from(
"POST /test1 HTTP/1.0\r\n\
Content-Length: 3\r\n\
\r\n\
abc",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::POST);
assert_eq!(req.path(), "/test1");
let mut buf = BytesMut::from(
"POST /test2 HTTP/1.0\r\n\
Content-Length: 0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10); assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::POST); assert_eq!(*req.method(), Method::POST);
assert_eq!(req.path(), "/test2"); assert_eq!(req.path(), "/test2");
let mut buf = BytesMut::from(
"POST /test3 HTTP/1.0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let err = reader.decode(&mut buf).unwrap_err();
assert!(err.to_string().contains("Header"))
} }
#[test] #[test]
@ -709,121 +844,98 @@ mod tests {
#[test] #[test]
fn test_conn_default_1_0() { fn test_conn_default_1_0() {
let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_default_1_1() { fn test_conn_default_1_1() {
let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_close() { fn test_conn_close() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
connection: close\r\n\r\n", connection: close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
connection: Close\r\n\r\n", connection: Close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_close_1_0() { fn test_conn_close_1_0() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.0\r\n\ "GET /test HTTP/1.0\r\n\
connection: close\r\n\r\n", connection: close\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_keep_alive_1_0() { fn test_conn_keep_alive_1_0() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.0\r\n\ "GET /test HTTP/1.0\r\n\
connection: keep-alive\r\n\r\n", connection: keep-alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.0\r\n\ "GET /test HTTP/1.0\r\n\
connection: Keep-Alive\r\n\r\n", connection: Keep-Alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_keep_alive_1_1() { fn test_conn_keep_alive_1_1() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
connection: keep-alive\r\n\r\n", connection: keep-alive\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_other_1_0() { fn test_conn_other_1_0() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.0\r\n\ "GET /test HTTP/1.0\r\n\
connection: other\r\n\r\n", connection: other\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::Close); assert_eq!(req.head().connection_type(), ConnectionType::Close);
} }
#[test] #[test]
fn test_conn_other_1_1() { fn test_conn_other_1_1() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
connection: other\r\n\r\n", connection: other\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
} }
#[test] #[test]
fn test_conn_upgrade() { fn test_conn_upgrade() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
upgrade: websockets\r\n\ upgrade: websockets\r\n\
connection: upgrade\r\n\r\n", connection: upgrade\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
upgrade: Websockets\r\n\ upgrade: Websockets\r\n\
connection: Upgrade\r\n\r\n", connection: Upgrade\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
@ -831,59 +943,62 @@ mod tests {
#[test] #[test]
fn test_conn_upgrade_connect_method() { fn test_conn_upgrade_connect_method() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"CONNECT /test HTTP/1.1\r\n\ "CONNECT /test HTTP/1.1\r\n\
content-type: text/plain\r\n\r\n", content-type: text/plain\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert!(req.upgrade()); assert!(req.upgrade());
} }
#[test] #[test]
fn test_headers_content_length_err_1() { fn test_headers_bad_content_length() {
let mut buf = BytesMut::from( // string CL
expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
content-length: line\r\n\r\n", content-length: line\r\n\r\n",
); ));
expect_parse_err!(&mut buf) // negative CL
expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\
content-length: -1\r\n\r\n",
));
} }
#[test] #[test]
fn test_headers_content_length_err_2() { fn octal_ish_cl_parsed_as_decimal() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\ "POST /test HTTP/1.1\r\n\
content-length: -1\r\n\r\n", content-length: 011\r\n\r\n",
); );
let mut reader = MessageDecoder::<Request>::default();
expect_parse_err!(&mut buf); let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap();
assert!(matches!(
pl,
PayloadType::Payload(pl) if pl == PayloadDecoder::length(11)
));
} }
#[test] #[test]
fn test_invalid_header() { fn test_invalid_header() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
test line\r\n\r\n", test line\r\n\r\n",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_invalid_name() { fn test_invalid_name() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
test[]: line\r\n\r\n", test[]: line\r\n\r\n",
); ));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_http_request_bad_status_line() { fn test_http_request_bad_status_line() {
let mut buf = BytesMut::from("getpath \r\n\r\n"); expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
@ -923,11 +1038,10 @@ mod tests {
#[test] #[test]
fn test_http_request_parser_utf8() { fn test_http_request_parser_utf8() {
let mut buf = BytesMut::from( let req = parse_ready!(&mut BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
x-test: тест\r\n\r\n", x-test: тест\r\n\r\n",
); ));
let req = parse_ready!(&mut buf);
assert_eq!( assert_eq!(
req.headers().get("x-test").unwrap().as_bytes(), req.headers().get("x-test").unwrap().as_bytes(),
@ -937,24 +1051,18 @@ mod tests {
#[test] #[test]
fn test_http_request_parser_two_slashes() { fn test_http_request_parser_two_slashes() {
let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n"));
let req = parse_ready!(&mut buf);
assert_eq!(req.path(), "//path"); assert_eq!(req.path(), "//path");
} }
#[test] #[test]
fn test_http_request_parser_bad_method() { fn test_http_request_parser_bad_method() {
let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
fn test_http_request_parser_bad_version() { fn test_http_request_parser_bad_version() {
let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n"));
expect_parse_err!(&mut buf);
} }
#[test] #[test]
@ -971,29 +1079,66 @@ mod tests {
#[test] #[test]
fn hrs_multiple_content_length() { fn hrs_multiple_content_length() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET / HTTP/1.1\r\n\ "GET / HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: 4\r\n\ Content-Length: 4\r\n\
Content-Length: 2\r\n\ Content-Length: 2\r\n\
\r\n\ \r\n\
abcd", abcd",
); ));
expect_parse_err!(&mut buf); expect_parse_err!(&mut BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 0\r\n\
Content-Length: 2\r\n\
\r\n\
ab",
));
} }
#[test] #[test]
fn hrs_content_length_plus() { fn hrs_content_length_plus() {
let mut buf = BytesMut::from( expect_parse_err!(&mut BytesMut::from(
"GET / HTTP/1.1\r\n\ "GET / HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: +3\r\n\ Content-Length: +3\r\n\
\r\n\ \r\n\
000", 000",
));
}
#[test]
fn hrs_te_http10() {
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header
expect_parse_err!(&mut BytesMut::from(
"POST / HTTP/1.0\r\n\
Host: example.com\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
3\r\n\
aaa\r\n\
0\r\n\
",
));
}
#[test]
fn hrs_cl_and_te_http10() {
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both
let mut buf = BytesMut::from(
"GET / HTTP/1.0\r\n\
Host: example.com\r\n\
Content-Length: 3\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
000",
); );
expect_parse_err!(&mut buf); parse_ready!(&mut buf);
} }
#[test] #[test]

View File

@ -22,7 +22,7 @@ use crate::{
config::ServiceConfig, config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError}, error::{DispatchError, ParseError, PayloadError},
service::HttpFlow, service::HttpFlow,
Error, Extensions, OnConnectData, Request, Response, StatusCode, ConnectionType, Error, Extensions, OnConnectData, Request, Response, StatusCode,
}; };
use super::{ use super::{
@ -185,7 +185,9 @@ pin_project! {
None, None,
ExpectCall { #[pin] fut: X::Future }, ExpectCall { #[pin] fut: X::Future },
ServiceCall { #[pin] fut: S::Future }, ServiceCall { #[pin] fut: S::Future },
SendResponse { res: Option<Response<B>> },
SendPayload { #[pin] body: B }, SendPayload { #[pin] body: B },
SendErrorResponse { res: Option<Response<BoxBody>> },
SendErrorPayload { #[pin] body: BoxBody }, SendErrorPayload { #[pin] body: BoxBody },
} }
} }
@ -216,9 +218,15 @@ where
Self::ServiceCall { .. } => { Self::ServiceCall { .. } => {
f.debug_struct("State::ServiceCall").finish_non_exhaustive() f.debug_struct("State::ServiceCall").finish_non_exhaustive()
} }
Self::SendResponse { .. } => f
.debug_struct("State::SendResponse")
.finish_non_exhaustive(),
Self::SendPayload { .. } => { Self::SendPayload { .. } => {
f.debug_struct("State::SendPayload").finish_non_exhaustive() f.debug_struct("State::SendPayload").finish_non_exhaustive()
} }
Self::SendErrorResponse { .. } => f
.debug_struct("State::SendErrorResponse")
.finish_non_exhaustive(),
Self::SendErrorPayload { .. } => f Self::SendErrorPayload { .. } => f
.debug_struct("State::SendErrorPayload") .debug_struct("State::SendErrorPayload")
.finish_non_exhaustive(), .finish_non_exhaustive(),
@ -379,11 +387,8 @@ where
Ok(size) Ok(size)
} }
fn send_response( fn send_response(mut self: Pin<&mut Self>, res: Response<B>) -> Result<(), DispatchError> {
mut self: Pin<&mut Self>, let (res, body) = res.replace_body(());
res: Response<()>,
body: B,
) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(res, &body)?; let size = self.as_mut().send_response_inner(res, &body)?;
let mut this = self.project(); let mut this = self.project();
this.state.set(match size { this.state.set(match size {
@ -397,11 +402,17 @@ where
Ok(()) Ok(())
} }
fn queue_response(self: Pin<&mut Self>, res: Response<B>) {
self.project()
.state
.set(State::SendResponse { res: Some(res) });
}
fn send_error_response( fn send_error_response(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
res: Response<()>, res: Response<BoxBody>,
body: BoxBody,
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let (res, body) = res.replace_body(());
let size = self.as_mut().send_response_inner(res, &body)?; let size = self.as_mut().send_response_inner(res, &body)?;
let mut this = self.project(); let mut this = self.project();
this.state.set(match size { this.state.set(match size {
@ -415,6 +426,12 @@ where
Ok(()) Ok(())
} }
fn queue_error_response(self: Pin<&mut Self>, res: Response<BoxBody>) {
self.project()
.state
.set(State::SendErrorResponse { res: Some(res) });
}
fn send_continue(self: Pin<&mut Self>) { fn send_continue(self: Pin<&mut Self>) {
self.project() self.project()
.write_buf .write_buf
@ -449,7 +466,8 @@ where
// send_response would update InnerDispatcher state to SendPayload or None // send_response would update InnerDispatcher state to SendPayload or None
// (If response body is empty) // (If response body is empty)
// continue loop to poll it // continue loop to poll it
self.as_mut().send_error_response(res, BoxBody::new(()))?; self.as_mut()
.queue_error_response(res.set_body(BoxBody::new(())));
} }
// return with upgrade request and poll it exclusively // return with upgrade request and poll it exclusively
@ -470,15 +488,12 @@ where
match fut.poll(cx) { match fut.poll(cx) {
// service call resolved. send response. // service call resolved. send response.
Poll::Ready(Ok(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); self.as_mut().queue_response(res.into());
self.as_mut().send_response(res, body)?;
} }
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); self.as_mut().queue_error_response(err.into());
let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?;
} }
// service call pending and could be waiting for more chunk messages // service call pending and could be waiting for more chunk messages
@ -486,14 +501,32 @@ where
Poll::Pending => { Poll::Pending => {
// no new message is decoded and no new payload is fed // no new message is decoded and no new payload is fed
// nothing to do except waiting for new incoming data from client // nothing to do except waiting for new incoming data from client
if !self.as_mut().poll_request(cx)? {
// optimisation disabled so that poll_request is called from only one place
// if !self.as_mut().poll_request(cx)? {
return Ok(PollResponse::DoNothing); return Ok(PollResponse::DoNothing);
} // }
// else loop // else loop
} }
} }
} }
StateProj::SendResponse { res } => {
let mut res = res.take().expect("response should be take-able");
if this.flags.contains(Flags::SHUTDOWN) {
trace!("shutdown flag set; assuming dirty read I/O");
// shutdown flags occur when read I/O is not clean so connections should be
// closed to avoid stuck or erroneous errors on next request
res.head_mut().set_connection_type(ConnectionType::Close);
}
self.send_response(res)?;
return Ok(PollResponse::DrainWriteBuf);
}
StateProj::SendPayload { mut body } => { StateProj::SendPayload { mut body } => {
// keep populate writer buffer until buffer size limit hit, // keep populate writer buffer until buffer size limit hit,
// get blocked or finished. // get blocked or finished.
@ -529,6 +562,23 @@ where
return Ok(PollResponse::DrainWriteBuf); return Ok(PollResponse::DrainWriteBuf);
} }
StateProj::SendErrorResponse { res } => {
// TODO: de-dupe impl with SendResponse
let mut res = res.take().expect("response should be take-able");
if this.flags.contains(Flags::SHUTDOWN) {
trace!("shutdown flag set; assuming dirty read I/O");
// shutdown flags occur when read I/O is not clean so connections should be
// closed to avoid stuck or erroneous errors on next request
res.head_mut().set_connection_type(ConnectionType::Close);
}
self.send_error_response(res)?;
return Ok(PollResponse::DrainWriteBuf);
}
StateProj::SendErrorPayload { mut body } => { StateProj::SendErrorPayload { mut body } => {
// TODO: de-dupe impl with SendPayload // TODO: de-dupe impl with SendPayload
@ -583,9 +633,7 @@ where
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); self.as_mut().queue_error_response(err.into());
let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?;
} }
// expect must be solved before progress can be made. // expect must be solved before progress can be made.
@ -637,9 +685,8 @@ where
// on success to notify the dispatcher a new state is set and the outer loop // on success to notify the dispatcher a new state is set and the outer loop
// should be continued // should be continued
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); self.queue_error_response(err.into());
let (res, body) = res.replace_body(()); return Ok(());
return self.send_error_response(res, body);
} }
// future is pending; return Ok(()) to notify that a new state is // future is pending; return Ok(()) to notify that a new state is
@ -655,8 +702,8 @@ where
// to notify the dispatcher a new state is set and the outer loop // to notify the dispatcher a new state is set and the outer loop
// should be continue. // should be continue.
Poll::Ready(Ok(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); self.as_mut().queue_response(res.into());
self.as_mut().send_response(res, body) Ok(())
} }
// see the comment on ExpectCall state branch's Pending // see the comment on ExpectCall state branch's Pending
@ -664,9 +711,8 @@ where
// see the comment on ExpectCall state branch's Ready(Err(_)) // see the comment on ExpectCall state branch's Ready(Err(_))
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<BoxBody> = err.into(); self.as_mut().queue_error_response(err.into());
let (res, body) = res.replace_body(()); Ok(())
self.as_mut().send_error_response(res, body)
} }
}; };
} }
@ -688,15 +734,13 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Result<bool, DispatchError> { ) -> Result<bool, DispatchError> {
let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES; let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES;
let can_not_read = !self.can_read(cx);
// limit amount of non-processed requests // limit amount of non-processed requests
if pipeline_queue_full || can_not_read { if pipeline_queue_full {
return Ok(false); return Ok(false);
} }
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
let mut updated = false; let mut updated = false;
// decode from read buf as many full requests as possible // decode from read buf as many full requests as possible
@ -829,6 +873,72 @@ where
} }
} }
let can_read = self.can_read(cx);
let mut this = self.as_mut().project();
if !can_read {
// request payload is not readable...
tracing::debug!("cannot read request payload");
if let Some(sender) = &this.payload {
// ...maybe handler does not want to read any more payload...
if let PayloadStatus::Dropped = sender.need_read(cx) {
tracing::debug!(
"handler dropped payload early; attempt to clean connection"
);
// ...in which case poll request payload a few times
loop {
match this.codec.decode(this.read_buf)? {
Some(msg) => {
match msg {
// payload decoded did not yield EOF yet
Message::Chunk(Some(_)) => {
// if non-clean connection, next loop iter will detect empty
// read buffer and close connection
}
// connection is in clean state for next request
Message::Chunk(None) => {
tracing::debug!("connection successfully cleaned");
// reset dispatcher state
let _ = this.payload.take();
this.state.set(State::None);
// break out of payload decode loop
break;
}
// Either whole payload is read and loop is broken or more data
// was expected in which case connection is closed. In both
// situations dispatcher cannot get here.
Message::Item(_) => {
unreachable!("dispatcher is in payload receive state")
}
}
}
// not enough info to decide if connection is going to be clean or not
None => {
tracing::debug!(
"handler did not read whole payload and dispatcher could not \
drain read buf; close connection"
);
this.flags.insert(Flags::SHUTDOWN);
return Ok(updated);
}
}
}
}
} else {
// can_not_read and no request payload
return Ok(false);
}
}
Ok(updated) Ok(updated)
} }
@ -844,10 +954,10 @@ where
trace!("timed out on slow request; replying with 408 and closing connection"); trace!("timed out on slow request; replying with 408 and closing connection");
let _ = self.as_mut().send_error_response( let mut res =
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), Response::with_body(StatusCode::REQUEST_TIMEOUT, BoxBody::new(()));
BoxBody::new(()), res.head_mut().set_connection_type(ConnectionType::Close);
); self.as_mut().send_error_response(res)?;
self.project().flags.insert(Flags::SHUTDOWN); self.project().flags.insert(Flags::SHUTDOWN);
} }
@ -1123,6 +1233,7 @@ where
} }
} }
// process request(s) and queue response
inner.as_mut().poll_request(cx)?; inner.as_mut().poll_request(cx)?;
if should_disconnect { if should_disconnect {

View File

@ -861,56 +861,26 @@ async fn handler_drop_payload() {
", ",
)); ));
assert!(h1.as_mut().poll(cx).is_pending());
// polls: manual => manual
assert_eq!(h1.poll_count, 2);
let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res);
let res = &res[..];
// expect response immediately even though request side has not finished reading payload
let exp = http_msg(
r"
HTTP/1.1 200 OK
content-length: 15
date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
",
);
assert_eq!(
res,
exp,
"\nexpected response not in write buffer:\n\
response: {:?}\n\
expected: {:?}",
String::from_utf8_lossy(res),
String::from_utf8_lossy(&exp)
);
})
.await;
lazy(|cx| {
assert!(h1.as_mut().poll(cx).is_ready()); assert!(h1.as_mut().poll(cx).is_ready());
// polls: manual => manual => manual // polls: manual => manual => shutdown
assert_eq!(h1.poll_count, 3); assert_eq!(h1.poll_count, 3);
let mut res = BytesMut::from(buf.take_write_buf().as_ref()); let mut res = BytesMut::from(buf.take_write_buf().as_ref());
stabilize_date_header(&mut res); stabilize_date_header(&mut res);
let res = &res[..]; let res = &res[..];
// expect that unrequested error response is sent back since connection could not be cleaned // expect response immediately even though request side has not finished reading payload
// since write buffer was "too short" we should expect a closed connection hint
let exp = http_msg( let exp = http_msg(
// connection: close
r" r"
HTTP/1.1 500 Internal Server Error HTTP/1.1 200 OK
content-length: 0 content-length: 15
connection: close connection: close
date: Thu, 01 Jan 1970 12:34:56 UTC date: Thu, 01 Jan 1970 12:34:56 UTC
payload dropped
", ",
); );

View File

@ -16,7 +16,7 @@ use crate::error::PayloadError;
/// max buffer size 32k /// max buffer size 32k
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum PayloadStatus { pub enum PayloadStatus {
Read, Read,
Pause, Pause,
@ -252,19 +252,15 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use super::*; use super::*;
assert_impl_all!(Payload: Unpin); assert_impl_all!(Payload: Unpin);
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_not_impl_any!(Payload: Send, Sync);
assert_impl_all!(Inner: Unpin, Send, Sync); assert_impl_all!(Inner: Unpin, Send, Sync);
// assertion not stable wrt rustc versions yet
// assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe);
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -103,11 +103,9 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::assert_impl_all; use static_assertions::assert_impl_all;
use super::*; use super::*;
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe); assert_impl_all!(Payload: Unpin, Send, Sync);
} }

View File

@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
/// - A character sequence representing the actual value (`value`), separated by single quotes. /// - A character sequence representing the actual value (`value`), separated by single quotes.
/// ///
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). /// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExtendedValue { pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string. /// The character set that is used to encode the `value` to a string.
pub charset: Charset, pub charset: Charset,

View File

@ -147,7 +147,7 @@ mod tests {
// copy of encoding from actix-web headers // copy of encoding from actix-web headers
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
#[derive(Clone, PartialEq, Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Encoding { pub enum Encoding {
Chunked, Chunked,
Brotli, Brotli,

View File

@ -25,6 +25,7 @@
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version}; pub use ::http::{Method, StatusCode, Version};
@ -69,6 +70,8 @@ pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
pub use self::requests::{Request, RequestHead, RequestHeadType}; pub use self::requests::{Request, RequestHead, RequestHeadType};
pub use self::responses::{Response, ResponseBuilder, ResponseHead}; pub use self::responses::{Response, ResponseBuilder, ResponseHead};
pub use self::service::HttpService; pub use self::service::HttpService;
#[cfg(any(feature = "openssl", feature = "rustls"))]
pub use self::service::TlsAcceptorConfig;
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]

View File

@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc};
use bitflags::bitflags; use bitflags::bitflags;
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response. /// Close connection after response.
Close, Close,

View File

@ -97,12 +97,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use super::*; use super::*;
assert_impl_all!(Payload: Unpin); assert_impl_all!(Payload: Unpin);
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); assert_not_impl_any!(Payload: Send, Sync);
} }

View File

@ -237,7 +237,7 @@ mod tests {
.await; .await;
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream stream
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n") .write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
.unwrap(); .unwrap();
let mut data = vec![]; let mut data = vec![];
@ -251,7 +251,7 @@ mod tests {
assert!(memmem::find(&data, b"content-length").is_none()); assert!(memmem::find(&data, b"content-length").is_none());
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream stream
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n") .write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
.unwrap(); .unwrap();
let mut data = vec![]; let mut data = vec![];

View File

@ -181,6 +181,25 @@ where
} }
} }
/// Configuration options used when accepting TLS connection.
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>,
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
Self {
handshake_timeout: Some(dur),
// ..self
}
}
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
@ -230,7 +249,28 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) self.openssl_with_config(acceptor, TlsAcceptorConfig::default())
}
/// Create OpenSSL based service with custom TLS acceptor configuration.
pub fn openssl_with_config(
self,
acceptor: SslAcceptor,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<SslError, DispatchError>,
InitError = (),
> {
let mut acceptor = Acceptor::new(acceptor);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| { .map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init") unreachable!("TLS acceptor service factory does not error on init")
}) })
@ -293,8 +333,23 @@ mod rustls {
{ {
/// Create Rustls based service. /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls based service with custom TLS acceptor configuration.
pub fn rustls_with_config(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
TcpStream, TcpStream,
Config = (), Config = (),
@ -306,7 +361,13 @@ mod rustls {
protos.extend_from_slice(&config.alpn_protocols); protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos; config.alpn_protocols = protos;
Acceptor::new(config) let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| { .map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init") unreachable!("TLS acceptor service factory does not error on init")
}) })

View File

@ -11,7 +11,7 @@ use super::{
}; };
/// A WebSocket message. /// A WebSocket message.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Message { pub enum Message {
/// Text message. /// Text message.
Text(ByteString), Text(ByteString),
@ -36,7 +36,7 @@ pub enum Message {
} }
/// A WebSocket frame. /// A WebSocket frame.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Frame { pub enum Frame {
/// Text frame. Note that the codec does not validate UTF-8 encoding. /// Text frame. Note that the codec does not validate UTF-8 encoding.
Text(Bytes), Text(Bytes),
@ -58,7 +58,7 @@ pub enum Frame {
} }
/// A WebSocket continuation item. /// A WebSocket continuation item.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Item { pub enum Item {
FirstText(Bytes), FirstText(Bytes),
FirstBinary(Bytes), FirstBinary(Bytes),

View File

@ -17,7 +17,6 @@ impl Parser {
fn parse_metadata( fn parse_metadata(
src: &[u8], src: &[u8],
server: bool, server: bool,
max_size: usize,
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> { ) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
let chunk_len = src.len(); let chunk_len = src.len();
@ -60,20 +59,12 @@ impl Parser {
return Ok(None); return Ok(None);
} }
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap()); let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
if len > max_size as u64 {
return Err(ProtocolError::Overflow);
}
idx += 8; idx += 8;
len as usize len as usize
} else { } else {
len as usize len as usize
}; };
// check for max allowed size
if length > max_size {
return Err(ProtocolError::Overflow);
}
let mask = if server { let mask = if server {
if chunk_len < idx + 4 { if chunk_len < idx + 4 {
return Ok(None); return Ok(None);
@ -98,8 +89,7 @@ impl Parser {
max_size: usize, max_size: usize,
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> { ) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
// try to parse ws frame metadata // try to parse ws frame metadata
let (idx, finished, opcode, length, mask) = let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? {
match Parser::parse_metadata(src, server, max_size)? {
None => return Ok(None), None => return Ok(None),
Some(res) => res, Some(res) => res,
}; };
@ -112,6 +102,13 @@ impl Parser {
// remove prefix // remove prefix
src.advance(idx); src.advance(idx);
// check for max allowed size
if length > max_size {
// drop the payload
src.advance(length);
return Err(ProtocolError::Overflow);
}
// no need for body // no need for body
if length == 0 { if length == 0 {
return Ok(Some((finished, opcode, None))); return Ok(Some((finished, opcode, None)));
@ -339,6 +336,30 @@ mod tests {
} }
} }
#[test]
fn test_parse_frame_max_size_recoverability() {
let mut buf = BytesMut::new();
// The first text frame with length == 2, payload doesn't matter.
buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
assert_eq!(buf.len(), 8);
assert!(matches!(
Parser::parse(&mut buf, false, 1),
Err(ProtocolError::Overflow)
));
assert_eq!(buf.len(), 4);
let frame = extract(Parser::parse(&mut buf, false, 2));
assert!(!frame.finished);
assert_eq!(frame.opcode, OpCode::Binary);
assert_eq!(
frame.payload,
Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8])
);
assert_eq!(buf.len(), 0);
}
#[test] #[test]
fn test_ping_frame() { fn test_ping_frame() {
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();

View File

@ -67,7 +67,7 @@ pub enum ProtocolError {
} }
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
pub enum HandshakeError { pub enum HandshakeError {
/// Only get method is allowed. /// Only get method is allowed.
#[display(fmt = "Method not allowed.")] #[display(fmt = "Method not allowed.")]

View File

@ -2,13 +2,13 @@
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::{convert::Infallible, io}; use std::{convert::Infallible, io, time::Duration};
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
header::{self, HeaderValue}, header::{self, HeaderValue},
Error, HttpService, Method, Request, Response, StatusCode, Version, Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
@ -89,7 +89,10 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.openssl(tls_config()) .openssl_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
.map_err(|_| ()) .map_err(|_| ())
}) })
.await; .await;

View File

@ -8,13 +8,14 @@ use std::{
net::{SocketAddr, TcpStream as StdTcpStream}, net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc, sync::Arc,
task::Poll, task::Poll,
time::Duration,
}; };
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Error, HttpService, Method, Request, Response, StatusCode, Version, Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::pin; use actix_rt::pin;
@ -160,7 +161,10 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
}) })
.await; .await;

View File

@ -1,7 +1,7 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.4.0 - 2022-02-25 ## 0.4.0 - 2022-02-25

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0)](https://docs.rs/actix-multipart/0.4.0) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0)](https://docs.rs/actix-multipart/0.4.0)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0)

View File

@ -1,7 +1,7 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.5.0 - 2022-02-22 ## 0.5.0 - 2022-02-22

View File

@ -21,7 +21,7 @@ default = ["http"]
[dependencies] [dependencies]
bytestring = ">=0.1.5, <2" bytestring = ">=0.1.5, <2"
http = { version = "0.2.3", optional = true } http = { version = "0.2.5", optional = true }
regex = "1.5" regex = "1.5"
serde = "1" serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }

View File

@ -649,7 +649,7 @@ impl ResourceDef {
/// resource.capture_match_info_fn( /// resource.capture_match_info_fn(
/// path, /// path,
/// // when env var is not set, reject when path contains "admin" /// // when env var is not set, reject when path contains "admin"
/// |res| !(!admin_allowed && res.path().contains("admin")), /// |path| !(!admin_allowed && path.as_str().contains("admin")),
/// ) /// )
/// } /// }
/// ///

View File

@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str {
impl ResourcePath for bytestring::ByteString { impl ResourcePath for bytestring::ByteString {
fn path(&self) -> &str { fn path(&self) -> &str {
&*self self
} }
} }

View File

@ -1,6 +1,6 @@
use crate::{IntoPatterns, Resource, ResourceDef}; use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct ResourceId(pub u16); pub struct ResourceId(pub u16);
/// Resource router. /// Resource router.

View File

@ -1,7 +1,10 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
## 0.1.0 - 2022-07-24
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.1.0-beta.13 - 2022-02-16 ## 0.1.0-beta.13 - 2022-02-16

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.13" version = "0.1.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.5" actix-codec = "0.5"
actix-http = "3" actix-http = "3"
actix-http-test = "3.0.0-beta.13" actix-http-test = "3"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"

View File

@ -1,7 +1,7 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 4.1.0 - 2022-03-02 ## 4.1.0 - 2022-03-02

View File

@ -27,8 +27,11 @@ tokio = { version = "1.13.1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] }
mime = "0.3"
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.1.0)](https://docs.rs/actix-web-actors/4.1.0)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0) [![dependency status](https://deps.rs/crate/actix-web-actors/4.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.0)

View File

@ -14,6 +14,58 @@ use futures_core::Stream;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
/// Execution context for HTTP actors /// Execution context for HTTP actors
///
/// # Example
///
/// A demonstration of [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) using actors:
///
/// ```no_run
/// use std::time::Duration;
///
/// use actix::{Actor, AsyncContext};
/// use actix_web::{get, http::header, App, HttpResponse, HttpServer};
/// use actix_web_actors::HttpContext;
/// use bytes::Bytes;
///
/// struct MyActor {
/// count: usize,
/// }
///
/// impl Actor for MyActor {
/// type Context = HttpContext<Self>;
///
/// fn started(&mut self, ctx: &mut Self::Context) {
/// ctx.run_later(Duration::from_millis(100), Self::write);
/// }
/// }
///
/// impl MyActor {
/// fn write(&mut self, ctx: &mut HttpContext<Self>) {
/// self.count += 1;
/// if self.count > 3 {
/// ctx.write_eof()
/// } else {
/// ctx.write(Bytes::from(format!("event: count\ndata: {}\n\n", self.count)));
/// ctx.run_later(Duration::from_millis(100), Self::write);
/// }
/// }
/// }
///
/// #[get("/")]
/// async fn index() -> HttpResponse {
/// HttpResponse::Ok()
/// .insert_header(header::ContentType(mime::TEXT_EVENT_STREAM))
/// .streaming(HttpContext::create(MyActor { count: 0 }))
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(|| App::new().service(index))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
pub struct HttpContext<A> pub struct HttpContext<A>
where where
A: Actor<Context = HttpContext<A>>, A: Actor<Context = HttpContext<A>>,
@ -210,7 +262,7 @@ mod tests {
type Context = HttpContext<Self>; type Context = HttpContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) { fn started(&mut self, ctx: &mut Self::Context) {
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); ctx.run_later(Duration::from_millis(100), Self::write);
} }
} }
@ -221,7 +273,7 @@ mod tests {
ctx.write_eof() ctx.write_eof()
} else { } else {
ctx.write(Bytes::from(format!("LINE-{}", self.count))); ctx.write(Bytes::from(format!("LINE-{}", self.count)));
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); ctx.run_later(Duration::from_millis(100), Self::write);
} }
} }
} }

View File

@ -1,4 +1,59 @@
//! Actix actors support for Actix Web. //! Actix actors support for Actix Web.
//!
//! # Examples
//!
//! ```no_run
//! use actix::{Actor, StreamHandler};
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
//! use actix_web_actors::ws;
//!
//! /// Define Websocket actor
//! struct MyWs;
//!
//! impl Actor for MyWs {
//! type Context = ws::WebsocketContext<Self>;
//! }
//!
//! /// Handler for ws::Message message
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
//! match msg {
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
//! Ok(ws::Message::Text(text)) => ctx.text(text),
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
//! _ => (),
//! }
//! }
//! }
//!
//! #[get("/ws")]
//! async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! ws::start(MyWs, &req, stream)
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| App::new().service(index))
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! }
//! ```
//!
//! # Documentation & Community Resources
//! In addition to this API documentation, several other resources are available:
//!
//! * [Website & User Guide](https://actix.rs/)
//! * [Documentation for `actix_web`](actix_web)
//! * [Examples Repository](https://github.com/actix/examples)
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
//!
//! To get started navigating the API docs, you may consider looking at the following pages first:
//!
//! * [`ws`]: This module provides actor support for WebSockets.
//!
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
//!
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]

View File

@ -1,4 +1,60 @@
//! Websocket integration. //! Websocket integration.
//!
//! # Examples
//!
//! ```no_run
//! use actix::{Actor, StreamHandler};
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
//! use actix_web_actors::ws;
//!
//! /// Define Websocket actor
//! struct MyWs;
//!
//! impl Actor for MyWs {
//! type Context = ws::WebsocketContext<Self>;
//! }
//!
//! /// Handler for ws::Message message
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
//! match msg {
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
//! Ok(ws::Message::Text(text)) => ctx.text(text),
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
//! _ => (),
//! }
//! }
//! }
//!
//! #[get("/ws")]
//! async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! ws::start(MyWs, &req, stream)
//! }
//!
//! const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
//!
//! #[get("/custom-ws")]
//! async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! // Create a Websocket session with a specific max frame size, and protocols.
//! ws::WsResponseBuilder::new(MyWs, &req, stream)
//! .frame_size(MAX_FRAME_SIZE)
//! .protocols(&["A", "B"])
//! .start()
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| {
//! App::new()
//! .service(websocket)
//! .service(custom_websocket)
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! }
//! ```
//!
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
@ -41,20 +97,51 @@ use tokio::sync::oneshot;
/// ///
/// # Examples /// # Examples
/// ///
/// Create a Websocket session response with default configuration. /// ```no_run
/// ```ignore /// # use actix::{Actor, StreamHandler};
/// WsResponseBuilder::new(WsActor, &req, stream).start() /// # use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
/// ``` /// # use actix_web_actors::ws;
/// #
/// # struct MyWs;
/// #
/// # impl Actor for MyWs {
/// # type Context = ws::WebsocketContext<Self>;
/// # }
/// #
/// # /// Handler for ws::Message message
/// # impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
/// # fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {}
/// # }
/// #
/// #[get("/ws")]
/// async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
/// ws::WsResponseBuilder::new(MyWs, &req, stream).start()
/// }
/// ///
/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols.
/// ```ignore
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB /// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
/// ///
/// ws::WsResponseBuilder::new(WsActor, &req, stream) /// #[get("/custom-ws")]
/// .codec(Codec::new()) /// async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
/// .protocols(&["A", "B"]) /// // Create a Websocket session with a specific max frame size, codec, and protocols.
/// ws::WsResponseBuilder::new(MyWs, &req, stream)
/// .codec(actix_http::ws::Codec::new())
/// // This will overwrite the codec's max frame-size
/// .frame_size(MAX_FRAME_SIZE) /// .frame_size(MAX_FRAME_SIZE)
/// .protocols(&["A", "B"])
/// .start() /// .start()
/// }
/// #
/// # #[actix_web::main]
/// # async fn main() -> std::io::Result<()> {
/// # HttpServer::new(|| {
/// # App::new()
/// # .service(websocket)
/// # .service(custom_websocket)
/// # })
/// # .bind(("127.0.0.1", 8080))?
/// # .run()
/// # .await
/// # }
/// ``` /// ```
pub struct WsResponseBuilder<'a, A, T> pub struct WsResponseBuilder<'a, A, T>
where where

View File

@ -1,6 +1,10 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2718]: https://github.com/actix/actix-web/pull/2718
## 4.0.1 - 2022-06-11 ## 4.0.1 - 2022-06-11

View File

@ -18,12 +18,12 @@ proc-macro = true
actix-router = "0.5.0" actix-router = "0.5.0"
proc-macro2 = "1" proc-macro2 = "1"
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "extra-traits"] }
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" actix-test = "0.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0" actix-web = "4.0.0"

View File

@ -4,7 +4,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.0.1)](https://docs.rs/actix-web-codegen/4.0.1) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.0.1)](https://docs.rs/actix-web-codegen/4.0.1)
![Version](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/4.0.1/status.svg)](https://deps.rs/crate/actix-web-codegen/4.0.1) [![dependency status](https://deps.rs/crate/actix-web-codegen/4.0.1/status.svg)](https://deps.rs/crate/actix-web-codegen/4.0.1)

View File

@ -46,9 +46,20 @@
//! ``` //! ```
//! //!
//! # Multiple Path Handlers //! # Multiple Path Handlers
//! There are no macros to generate multi-path handlers. Let us know in [this issue]. //! Acts as a wrapper for multiple single method handler macros. It takes no arguments and
//! delegates those to the macros for the individual methods. See [macro@routes] macro docs.
//! //!
//! [this issue]: https://github.com/actix/actix-web/issues/1709 //! ```
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::routes;
//! #[routes]
//! #[get("/test")]
//! #[get("/test2")]
//! #[delete("/test")]
//! async fn example() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//! //!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes //! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get //! [GET]: macro@get
@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input) route::with_method(None, args, input)
} }
/// Creates resource handler, allowing multiple HTTP methods and paths.
///
/// # Syntax
/// ```plain
/// #[routes]
/// #[<method>("path", ...)]
/// #[<method>("path", ...)]
/// ...
/// ```
///
/// # Attributes
/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
///
/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::routes;
/// #[routes]
/// #[get("/test")]
/// #[get("/test2")]
/// #[delete("/test")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
route::with_methods(input)
}
macro_rules! method_macro { macro_rules! method_macro {
($variant:ident, $method:ident) => { ($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]

View File

@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom};
use actix_router::ResourceDef; use actix_router::ResourceDef;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path}; use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
enum ResourceType {
Async,
Sync,
}
impl ToTokens for ResourceType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = format_ident!("to");
stream.append(ident);
}
}
macro_rules! method_type { macro_rules! method_type {
( (
$($variant:ident, $upper:ident,)+ $($variant:ident, $upper:ident, $lower:ident,)+
) => { ) => {
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType { pub enum MethodType {
@ -42,20 +30,27 @@ macro_rules! method_type {
_ => Err(format!("Unexpected HTTP method: `{}`", method)), _ => Err(format!("Unexpected HTTP method: `{}`", method)),
} }
} }
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
} }
}; };
} }
method_type! { method_type! {
Get, GET, Get, GET, get,
Post, POST, Post, POST, post,
Put, PUT, Put, PUT, put,
Delete, DELETE, Delete, DELETE, delete,
Head, HEAD, Head, HEAD, head,
Connect, CONNECT, Connect, CONNECT, connect,
Options, OPTIONS, Options, OPTIONS, options,
Trace, TRACE, Trace, TRACE, trace,
Patch, PATCH, Patch, PATCH, patch,
} }
impl ToTokens for MethodType { impl ToTokens for MethodType {
@ -90,6 +85,18 @@ impl Args {
let mut wrappers = Vec::new(); let mut wrappers = Vec::new();
let mut methods = HashSet::new(); let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none(); let is_route_macro = method.is_none();
if let Some(method) = method { if let Some(method) = method {
methods.insert(method); methods.insert(method);
@ -183,55 +190,27 @@ impl Args {
} }
pub struct Route { pub struct Route {
/// Name of the handler function being annotated.
name: syn::Ident, name: syn::Ident,
args: Args,
/// Args passed to routing macro.
///
/// When using `#[routes]`, this will contain args for each specific routing macro.
args: Vec<Args>,
/// AST of the handler function being annotated.
ast: syn::ItemFn, ast: syn::ItemFn,
resource_type: ResourceType,
/// The doc comment attributes to copy to generated struct, if any. /// The doc comment attributes to copy to generated struct, if any.
doc_attributes: Vec<syn::Attribute>, doc_attributes: Vec<syn::Attribute>,
} }
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
let mut guess = ResourceType::Sync;
if let syn::Type::ImplTrait(typ) = typ {
for bound in typ.bounds.iter() {
if let syn::TypeParamBound::Trait(bound) = bound {
for bound in bound.path.segments.iter() {
if bound.ident == "Future" {
guess = ResourceType::Async;
break;
} else if bound.ident == "Responder" {
guess = ResourceType::Sync;
break;
}
}
}
}
}
guess
}
impl Route { impl Route {
pub fn new( pub fn new(
args: AttributeArgs, args: AttributeArgs,
ast: syn::ItemFn, ast: syn::ItemFn,
method: Option<MethodType>, method: Option<MethodType>,
) -> syn::Result<Self> { ) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct. // Try and pull out the doc comments so that we can reapply them to the generated struct.
@ -244,6 +223,7 @@ impl Route {
.collect(); .collect();
let args = Args::new(args, method)?; let args = Args::new(args, method)?;
if args.methods.is_empty() { if args.methods.is_empty() {
return Err(syn::Error::new( return Err(syn::Error::new(
Span::call_site(), Span::call_site(),
@ -251,25 +231,44 @@ impl Route {
)); ));
} }
let resource_type = if ast.sig.asyncness.is_some() { if matches!(ast.sig.output, syn::ReturnType::Default) {
ResourceType::Async
} else {
match ast.sig.output {
syn::ReturnType::Default => {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
ast, ast,
"Function has no return type. Cannot be used as handler", "Function has no return type. Cannot be used as handler",
)); ));
} }
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
Ok(Self {
name,
args: vec![args],
ast,
doc_attributes,
})
}
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct.
// Note that multi line doc comments are converted to multiple doc attributes.
let doc_attributes = ast
.attrs
.iter()
.filter(|attr| attr.path.is_ident("doc"))
.cloned()
.collect();
if matches!(ast.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
} }
};
Ok(Self { Ok(Self {
name, name,
args, args,
ast, ast,
resource_type,
doc_attributes, doc_attributes,
}) })
} }
@ -280,22 +279,28 @@ impl ToTokens for Route {
let Self { let Self {
name, name,
ast, ast,
args: args,
Args { doc_attributes,
} = self;
let registrations: TokenStream2 = args
.iter()
.map(|args| {
let Args {
path, path,
resource_name, resource_name,
guards, guards,
wrappers, wrappers,
methods, methods,
}, } = args;
resource_type,
doc_attributes,
} = self;
let resource_name = resource_name let resource_name = resource_name
.as_ref() .as_ref()
.map_or_else(|| name.to_string(), LitStr::value); .map_or_else(|| name.to_string(), LitStr::value);
let method_guards = { let method_guards = {
let mut others = methods.iter(); let mut others = methods.iter();
// unwrapping since length is checked to be at least one // unwrapping since length is checked to be at least one
let first = others.next().unwrap(); let first = others.next().unwrap();
@ -313,6 +318,19 @@ impl ToTokens for Route {
} }
}; };
quote! {
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.to(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
}
})
.collect();
let stream = quote! { let stream = quote! {
#(#doc_attributes)* #(#doc_attributes)*
#[allow(non_camel_case_types, missing_docs)] #[allow(non_camel_case_types, missing_docs)]
@ -321,14 +339,7 @@ impl ToTokens for Route {
impl ::actix_web::dev::HttpServiceFactory for #name { impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) { fn register(self, __config: &mut actix_web::dev::AppService) {
#ast #ast
let __resource = ::actix_web::Resource::new(#path) #registrations
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
} }
} }
}; };
@ -357,6 +368,57 @@ pub(crate) fn with_method(
} }
} }
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err),
};
let (methods, others) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(&attr.path) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition::<Vec<_>, _>(Result::is_ok);
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
let methods =
match methods
.into_iter()
.map(Result::unwrap)
.map(|(method, attr)| {
attr.parse_meta().and_then(|args| {
if let Meta::List(args) = args {
Args::new(args.nested.into_iter().collect(), Some(method))
} else {
Err(syn::Error::new_spanned(attr, "Invalid input for macro"))
}
})
})
.collect::<Result<Vec<_>, _>>()
{
Ok(methods) if methods.is_empty() => return input_and_compile_error(
input,
syn::Error::new(
Span::call_site(),
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
),
),
Ok(methods) => methods,
Err(err) => return input_and_compile_error(input, err),
};
match Route::multiple(methods, ast) {
Ok(route) => route.into_token_stream().into(),
// on macro related error, make IDEs happy; see fn docs
Err(err) => input_and_compile_error(input, err),
}
}
/// Converts the error to a token stream and appends it to the original input. /// Converts the error to a token stream and appends it to the original input.
/// ///
/// Returning the original input in addition to the error is good for IDEs which can gracefully /// Returning the original input in addition to the error is good for IDEs which can gracefully

View File

@ -8,9 +8,11 @@ use actix_web::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
StatusCode, StatusCode,
}, },
web, App, Error, HttpResponse, Responder, web, App, Error, HttpRequest, HttpResponse, Responder,
};
use actix_web_codegen::{
connect, delete, get, head, options, patch, post, put, route, routes, trace,
}; };
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
// Make sure that we can name function as 'config' // Make sure that we can name function as 'config'
@ -89,8 +91,41 @@ async fn route_test() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
#[routes]
#[get("/routes/test")]
#[get("/routes/test2")]
#[post("/routes/test")]
async fn routes_test() -> impl Responder {
HttpResponse::Ok()
}
// routes overlap with the more specific route first, therefore accessible
#[routes]
#[get("/routes/overlap/test")]
#[get("/routes/overlap/{foo}")]
async fn routes_overlapping_test(req: HttpRequest) -> impl Responder {
// foo is only populated when route is not /routes/overlap/test
match req.match_info().get("foo") {
None => assert!(req.uri() == "/routes/overlap/test"),
Some(_) => assert!(req.uri() != "/routes/overlap/test"),
}
HttpResponse::Ok()
}
// routes overlap with the more specific route last, therefore inaccessible
#[routes]
#[get("/routes/overlap2/{foo}")]
#[get("/routes/overlap2/test")]
async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder {
// foo is always populated even when path is /routes/overlap2/test
assert!(req.match_info().get("foo").is_some());
HttpResponse::Ok()
}
#[get("/custom_resource_name", name = "custom")] #[get("/custom_resource_name", name = "custom")]
async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder { async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
assert!(req.url_for_static("custom").is_ok()); assert!(req.url_for_static("custom").is_ok());
assert!(req.url_for_static("custom_resource_name_test").is_err()); assert!(req.url_for_static("custom_resource_name_test").is_err());
HttpResponse::Ok() HttpResponse::Ok()
@ -201,6 +236,9 @@ async fn test_body() {
.service(patch_test) .service(patch_test)
.service(test_handler) .service(test_handler)
.service(route_test) .service(route_test)
.service(routes_overlapping_test)
.service(routes_overlapping_inaccessible_test)
.service(routes_test)
.service(custom_resource_name_test) .service(custom_resource_name_test)
.service(guard_test) .service(guard_test)
}); });
@ -258,6 +296,38 @@ async fn test_body() {
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(!response.status().is_success()); assert!(!response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/test2"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::POST, srv.url("/routes/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/not-set"));
let response = request.send().await.unwrap();
assert!(response.status().is_client_error());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name")); let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.56)] // MSRV #[rustversion::stable(1.57)] // MSRV
#[test] #[test]
fn compile_macros() { fn compile_macros() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();
@ -12,6 +12,10 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/routes-ok.rs");
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/docstring-ok.rs");
t.pass("tests/trybuild/test-runtime.rs"); t.pass("tests/trybuild/test-runtime.rs");

View File

@ -1,11 +1,13 @@
error: HTTP method defined more than once: `GET` error: HTTP method defined more than once: `GET`
--> $DIR/route-duplicate-method-fail.rs:3:35 --> tests/trybuild/route-duplicate-method-fail.rs:3:35
| |
3 | #[route("/", method="GET", method="GET")] 3 | #[route("/", method="GET", method="GET")]
| ^^^^^ | ^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> $DIR/route-duplicate-method-fail.rs:12:55 --> tests/trybuild/route-duplicate-method-fail.rs:12:55
| |
12 | let srv = actix_test::start(|| App::new().service(index)); 12 | let srv = actix_test::start(|| App::new().service(index));
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@ -10,4 +10,6 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpSer
--> tests/trybuild/route-missing-method-fail.rs:12:55 --> tests/trybuild/route-missing-method-fail.rs:12:55
| |
12 | let srv = actix_test::start(|| App::new().service(index)); 12 | let srv = actix_test::start(|| App::new().service(index));
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@ -1,11 +1,13 @@
error: Unexpected HTTP method: `UNEXPECTED` error: Unexpected HTTP method: `UNEXPECTED`
--> $DIR/route-unexpected-method-fail.rs:3:21 --> tests/trybuild/route-unexpected-method-fail.rs:3:21
| |
3 | #[route("/", method="UNEXPECTED")] 3 | #[route("/", method="UNEXPECTED")]
| ^^^^^^^^^^^^ | ^^^^^^^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> $DIR/route-unexpected-method-fail.rs:12:55 --> tests/trybuild/route-unexpected-method-fail.rs:12:55
| |
12 | let srv = actix_test::start(|| App::new().service(index)); 12 | let srv = actix_test::start(|| App::new().service(index));
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@ -0,0 +1,14 @@
use actix_web_codegen::*;
#[routes]
#[get]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
}

View File

@ -0,0 +1,21 @@
error: invalid service definition, expected #[get("<path>")]
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
4 | #[get]
| ^^^^^^
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Invalid input for macro
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
4 | #[get]
| ^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/routes-missing-args-fail.rs:13:55
|
13 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@ -0,0 +1,13 @@
use actix_web_codegen::*;
#[routes]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
}

View File

@ -0,0 +1,15 @@
error: The #[routes] macro requires at least one `#[<method>(..)]` attribute.
--> tests/trybuild/routes-missing-method-fail.rs:3:1
|
3 | #[routes]
| ^^^^^^^^^
|
= note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/routes-missing-method-fail.rs:12:55
|
12 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@ -0,0 +1,23 @@
use actix_web_codegen::*;
#[routes]
#[get("/")]
#[post("/")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.post("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -1,6 +1,17 @@
# Changelog # Changelog
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
### Added
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2718]: https://github.com/actix/actix-web/pull/2718
[#2752]: https://github.com/actix/actix-web/pull/2752
[#2786]: https://github.com/actix/actix-web/pull/2786
## 4.1.0 - 2022-06-11 ## 4.1.0 - 2022-06-11

View File

@ -84,11 +84,12 @@ derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
http = "0.2.8"
itoa = "1" itoa = "1"
language-tags = "0.3" language-tags = "0.3"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
once_cell = "1.5"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
regex = "1.5.5" regex = "1.5.5"
serde = "1.0" serde = "1.0"
@ -101,18 +102,18 @@ url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-files = "0.6" actix-files = "0.6"
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } actix-test = { version = "0.1", features = ["openssl", "rustls"] }
awc = { version = "3", features = ["openssl"] } awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.4"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.9"
rustls-pemfile = "0.2" rustls-pemfile = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }

View File

@ -7,7 +7,7 @@
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.1.0)](https://docs.rs/actix-web/4.1.0) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.1.0)](https://docs.rs/actix-web/4.1.0)
![MSRV](https://img.shields.io/badge/rustc-1.56+-ab6000.svg) ![MSRV](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.1.0/status.svg)](https://deps.rs/crate/actix-web/4.1.0) [![Dependency Status](https://deps.rs/crate/actix-web/4.1.0/status.svg)](https://deps.rs/crate/actix-web/4.1.0)
<br /> <br />
@ -33,7 +33,7 @@
- SSL support using OpenSSL or Rustls - SSL support using OpenSSL or Rustls
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) - Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
- Integrates with the [`awc` HTTP client](https://docs.rs/awc/) - Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
- Runs on stable Rust 1.56+ - Runs on stable Rust 1.57+
## Documentation ## Documentation
@ -79,6 +79,7 @@ async fn main() -> std::io::Result<()> {
- [Application State](https://github.com/actix/examples/tree/master/basics/state) - [Application State](https://github.com/actix/examples/tree/master/basics/state)
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json) - [JSON Handling](https://github.com/actix/examples/tree/master/json/json)
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart) - [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart)
- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb)
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel) - [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel)
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite) - [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite)
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres) - [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres)

View File

@ -60,7 +60,7 @@ where
/// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime.
/// ///
/// # [`Data<T>`] /// # [`Data<T>`]
/// Any [`Data<T>`] type added here can utilize it's extractor implementation in handlers. /// Any [`Data<T>`] type added here can utilize its extractor implementation in handlers.
/// Types not wrapped in `Data<T>` cannot use this extractor. See [its docs](Data<T>) for more /// Types not wrapped in `Data<T>` cannot use this extractor. See [its docs](Data<T>) for more
/// about its usage and patterns. /// about its usage and patterns.
/// ///

View File

@ -257,7 +257,7 @@ impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
// construct all services factory future with it's resource def and guards. // construct all services factory future with its resource def and guards.
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
let path = path.clone(); let path = path.clone();
let guards = guards.borrow_mut().take().unwrap_or_default(); let guards = guards.borrow_mut().take().unwrap_or_default();

View File

@ -153,6 +153,16 @@ impl AppConfig {
} }
impl Default for AppConfig { impl Default for AppConfig {
/// Returns the default AppConfig.
/// Note: The included socket address is "127.0.0.1".
///
/// 127.0.0.1: non-routable meta address that denotes an unknown, invalid or non-applicable target.
/// If you need a service only accessed by itself, use a loopback address.
/// A loopback address for IPv4 is any loopback address that begins with "127".
/// Loopback addresses should be only used to test your application locally.
/// The default configuration provides a loopback address.
///
/// 0.0.0.0: if configured to use this special address, the application will listen to any IP address configured on the machine.
fn default() -> Self { fn default() -> Self {
AppConfig::new( AppConfig::new(
false, false,

View File

@ -118,7 +118,7 @@ impl<T: ?Sized> Deref for Data<T> {
impl<T: ?Sized> Clone for Data<T> { impl<T: ?Sized> Clone for Data<T> {
fn clone(&self) -> Data<T> { fn clone(&self) -> Data<T> {
Data(self.0.clone()) Data(Arc::clone(&self.0))
} }
} }

View File

@ -42,7 +42,7 @@ pub struct BlockingError;
impl ResponseError for crate::error::BlockingError {} impl ResponseError for crate::error::BlockingError {}
/// Errors which can occur when attempting to generate resource uri. /// Errors which can occur when attempting to generate resource uri.
#[derive(Debug, PartialEq, Display, Error, From)] #[derive(Debug, PartialEq, Eq, Display, Error, From)]
#[non_exhaustive] #[non_exhaustive]
pub enum UrlGenerationError { pub enum UrlGenerationError {
/// Resource not found. /// Resource not found.

View File

@ -254,7 +254,7 @@ impl Guard for AllGuard {
} }
} }
/// Wraps a guard and inverts the outcome of it's `Guard` implementation. /// Wraps a guard and inverts the outcome of its `Guard` implementation.
/// ///
/// # Examples /// # Examples
/// The handler below will be called for any request method apart from `GET`. /// The handler below will be called for any request method apart from `GET`.
@ -459,7 +459,7 @@ impl Guard for HostGuard {
return scheme == req_host_uri_scheme; return scheme == req_host_uri_scheme;
} }
// TODO: is the the correct behavior? // TODO: is this the correct behavior?
// falls through if scheme cannot be determined // falls through if scheme cannot be determined
} }

View File

@ -37,7 +37,7 @@ use crate::{
/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the /// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the
/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on /// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on
/// that tuple, and the `Handler::call` implementation for that particular function arity /// that tuple, and the `Handler::call` implementation for that particular function arity
/// destructures the tuple into it's component types and calls your handler function with them. /// destructures the tuple into its component types and calls your handler function with them.
/// ///
/// In pseudo-code the process looks something like this: /// In pseudo-code the process looks something like this:
/// ```ignore /// ```ignore

View File

@ -10,9 +10,10 @@
//! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/> //! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/>
//! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml> //! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml>
use std::fmt::{self, Write};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::fmt::{self, Write};
use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header; use crate::http::header;
@ -36,7 +37,7 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
} }
/// The implied disposition of the content of the HTTP body. /// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispositionType { pub enum DispositionType {
/// Inline implies default processing. /// Inline implies default processing.
Inline, Inline,
@ -78,7 +79,7 @@ impl<'a> From<&'a str> for DispositionType {
/// assert!(param.is_filename()); /// assert!(param.is_filename());
/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); /// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum DispositionParam { pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
@ -301,7 +302,7 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path /// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present. /// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContentDisposition { pub struct ContentDisposition {
/// The disposition type /// The disposition type
pub disposition: DispositionType, pub disposition: DispositionType,

View File

@ -57,7 +57,7 @@ use crate::HttpMessage;
/// IfRange::Date(fetched.into()) /// IfRange::Date(fetched.into())
/// ); /// );
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum IfRange { pub enum IfRange {
/// The entity-tag the client has of the resource. /// The entity-tag the client has of the resource.
EntityTag(EntityTag), EntityTag(EntityTag),

View File

@ -224,10 +224,11 @@ macro_rules! common_header {
// List header, one or more items with "*" option // List header, one or more items with "*" option
($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
$(#[$attrs])* $(#[$attrs])*
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum $id { pub enum $id {
/// Any value is a match /// Any value is a match
Any, Any,
/// Only the listed items are a match /// Only the listed items are a match
Items(Vec<$item>), Items(Vec<$item>),
} }

View File

@ -53,7 +53,7 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa
/// builder.insert_header(Range::bytes(1, 100)); /// builder.insert_header(Range::bytes(1, 100));
/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)])); /// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)]));
/// ``` /// ```
#[derive(PartialEq, Clone, Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Range { pub enum Range {
/// Byte range. /// Byte range.
Bytes(Vec<ByteRangeSpec>), Bytes(Vec<ByteRangeSpec>),

View File

@ -2,7 +2,6 @@ use std::{convert::Infallible, net::SocketAddr};
use actix_utils::future::{err, ok, Ready}; use actix_utils::future::{err, ok, Ready};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use once_cell::sync::Lazy;
use crate::{ use crate::{
dev::{AppConfig, Payload, RequestHead}, dev::{AppConfig, Payload, RequestHead},
@ -13,12 +12,9 @@ use crate::{
FromRequest, HttpRequest, ResponseError, FromRequest, HttpRequest, ResponseError,
}; };
static X_FORWARDED_FOR: Lazy<HeaderName> = static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
Lazy::new(|| HeaderName::from_static("x-forwarded-for")); static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_HOST: Lazy<HeaderName> = static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
Lazy::new(|| HeaderName::from_static("x-forwarded-host"));
static X_FORWARDED_PROTO: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-proto"));
/// Trim whitespace then any quote marks. /// Trim whitespace then any quote marks.
fn unquote(val: &str) -> &str { fn unquote(val: &str) -> &str {
@ -117,21 +113,21 @@ impl ConnectionInfo {
} }
let scheme = scheme let scheme = scheme
.or_else(|| first_header_value(req, &*X_FORWARDED_PROTO)) .or_else(|| first_header_value(req, &X_FORWARDED_PROTO))
.or_else(|| req.uri.scheme().map(Scheme::as_str)) .or_else(|| req.uri.scheme().map(Scheme::as_str))
.or_else(|| Some("https").filter(|_| cfg.secure())) .or_else(|| Some("https").filter(|_| cfg.secure()))
.unwrap_or("http") .unwrap_or("http")
.to_owned(); .to_owned();
let host = host let host = host
.or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) .or_else(|| first_header_value(req, &X_FORWARDED_HOST))
.or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) .or_else(|| req.headers.get(&header::HOST)?.to_str().ok())
.or_else(|| req.uri.authority().map(Authority::as_str)) .or_else(|| req.uri.authority().map(Authority::as_str))
.unwrap_or_else(|| cfg.host()) .unwrap_or_else(|| cfg.host())
.to_owned(); .to_owned();
let realip_remote_addr = realip_remote_addr let realip_remote_addr = realip_remote_addr
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) .or_else(|| first_header_value(req, &X_FORWARDED_FOR))
.map(str::to_owned); .map(str::to_owned);
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());

View File

@ -132,6 +132,7 @@ macro_rules! codegen_reexport {
codegen_reexport!(main); codegen_reexport!(main);
codegen_reexport!(test); codegen_reexport!(test);
codegen_reexport!(route); codegen_reexport!(route);
codegen_reexport!(routes);
codegen_reexport!(head); codegen_reexport!(head);
codegen_reexport!(get); codegen_reexport!(get);
codegen_reexport!(post); codegen_reexport!(post);

View File

@ -220,37 +220,32 @@ static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
encoding.join(", ") encoding.join(", ")
}); });
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| { static SUPPORTED_ENCODINGS: &[Encoding] = &[
let mut encodings = vec![Encoding::identity()]; Encoding::identity(),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
{ {
encodings.push(Encoding::brotli()); Encoding::brotli()
} },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
{ {
encodings.push(Encoding::gzip()); Encoding::gzip()
encodings.push(Encoding::deflate()); },
} #[cfg(feature = "compress-gzip")]
{
Encoding::deflate()
},
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
{ {
encodings.push(Encoding::zstd()); Encoding::zstd()
} },
];
assert!(
!encodings.is_empty(),
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
);
encodings
});
// move cfg(feature) to prevents_double_compressing if more tests are added // move cfg(feature) to prevents_double_compressing if more tests are added
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashSet;
use super::*; use super::*;
use crate::{middleware::DefaultHeaders, test, web, App}; use crate::{middleware::DefaultHeaders, test, web, App};
@ -305,4 +300,28 @@ mod tests {
let bytes = test::read_body(res).await; let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes()); assert_eq!(gzip_decode(bytes), DATA.as_bytes());
} }
#[actix_rt::test]
async fn retains_previously_set_vary_header() {
let app = test::init_service({
App::new()
.wrap(Compress::default())
.default_service(web::to(move || {
HttpResponse::Ok()
.insert_header((header::VARY, "x-test"))
.finish()
}))
})
.await;
let req = test::TestRequest::default()
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
#[allow(clippy::mutable_key_type)]
let vary_headers = res.headers().get_all(header::VARY).collect::<HashSet<_>>();
assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));
}
} }

View File

@ -253,7 +253,7 @@ impl HttpRequest {
#[inline] #[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
if !self.extensions().contains::<ConnectionInfo>() { if !self.extensions().contains::<ConnectionInfo>() {
let info = ConnectionInfo::new(self.head(), &*self.app_config()); let info = ConnectionInfo::new(self.head(), self.app_config());
self.extensions_mut().insert(info); self.extensions_mut().insert(info);
} }

View File

@ -343,7 +343,7 @@ mod response_fut_impl {
// Future is only implemented for BoxBody payload type because it's the most useful for making // Future is only implemented for BoxBody payload type because it's the most useful for making
// simple handlers without async blocks. Making it generic over all MessageBody types requires a // simple handlers without async blocks. Making it generic over all MessageBody types requires a
// future impl on Response which would cause it's body field to be, undesirably, Option<B>. // future impl on Response which would cause its body field to be, undesirably, Option<B>.
// //
// This impl is not particularly efficient due to the Response construction and should probably // This impl is not particularly efficient due to the Response construction and should probably
// not be invoked if performance is important. Prefer an async fn/block in such cases. // not be invoked if performance is important. Prefer an async fn/block in such cases.

View File

@ -18,6 +18,9 @@ use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorB
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig;
#[cfg(any(feature = "openssl", feature = "rustls"))]
use actix_http::TlsAcceptorConfig;
use crate::{config::AppConfig, Error}; use crate::{config::AppConfig, Error};
struct Socket { struct Socket {
@ -30,6 +33,8 @@ struct Config {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
#[cfg(any(feature = "openssl", feature = "rustls"))]
tls_handshake_timeout: Option<Duration>,
} }
/// An HTTP Server. /// An HTTP Server.
@ -92,6 +97,8 @@ where
keep_alive: KeepAlive::default(), keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1), client_disconnect_timeout: Duration::from_secs(1),
#[cfg(any(feature = "rustls", feature = "openssl"))]
tls_handshake_timeout: None,
})), })),
backlog: 1024, backlog: 1024,
sockets: Vec::new(), sockets: Vec::new(),
@ -225,6 +232,24 @@ where
self self
} }
/// Set TLS handshake timeout.
///
/// Defines a timeout for TLS handshake. If the TLS handshake does not complete
/// within this time, the connection is closed.
///
/// By default handshake timeout is set to 3000 milliseconds.
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
pub fn tls_handshake_timeout(self, dur: Duration) -> Self {
self.config
.lock()
.unwrap()
.tls_handshake_timeout
.replace(dur);
self
}
#[doc(hidden)] #[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")] #[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")]
pub fn client_shutdown(self, dur: u64) -> Self { pub fn client_shutdown(self, dur: u64) -> Self {
@ -376,10 +401,17 @@ where
.into_factory() .into_factory()
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
// false positive lint (?)
#[allow(clippy::significant_drop_in_scrutinee)]
let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(),
};
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr)
})) }))
.openssl(acceptor.clone()) .openssl_with_config(acceptor.clone(), acceptor_config)
})?; })?;
Ok(self) Ok(self)
@ -434,10 +466,15 @@ where
.into_factory() .into_factory()
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(),
};
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr)
})) }))
.rustls(config.clone()) .rustls_with_config(config.clone(), acceptor_config)
})?; })?;
Ok(self) Ok(self)

View File

@ -95,6 +95,18 @@ impl ServiceRequest {
(&mut self.req, &mut self.payload) (&mut self.req, &mut self.payload)
} }
/// Returns immutable accessors to inner parts.
#[inline]
pub fn parts(&self) -> (&HttpRequest, &Payload) {
(&self.req, &self.payload)
}
/// Returns immutable accessor to inner [`HttpRequest`].
#[inline]
pub fn request(&self) -> &HttpRequest {
&self.req
}
/// Derives a type from this request using an [extractor](crate::FromRequest). /// Derives a type from this request using an [extractor](crate::FromRequest).
/// ///
/// Returns the `T` extractor's `Future` type which can be `await`ed. This is particularly handy /// Returns the `T` extractor's `Future` type which can be `await`ed. This is particularly handy

View File

@ -73,7 +73,7 @@ use crate::{
/// } /// }
/// } /// }
/// ``` /// ```
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Eq)]
pub enum Either<L, R> { pub enum Either<L, R> {
/// A value of type `L`. /// A value of type `L`.
Left(L), Left(L),

View File

@ -134,6 +134,7 @@ where
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct PathConfig { pub struct PathConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>, err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
} }
@ -183,6 +184,7 @@ mod tests {
assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err()); assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
} }
#[allow(clippy::let_unit_value)]
#[actix_rt::test] #[actix_rt::test]
async fn test_tuple_extract() { async fn test_tuple_extract() {
let resource = ResourceDef::new("/{key}/{value}/"); let resource = ResourceDef::new("/{key}/{value}/");

View File

@ -113,7 +113,7 @@ pub struct BytesExtractFut {
body_fut: HttpMessageBody, body_fut: HttpMessageBody,
} }
impl<'a> Future for BytesExtractFut { impl Future for BytesExtractFut {
type Output = Result<Bytes, Error>; type Output = Result<Bytes, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
@ -167,7 +167,7 @@ pub struct StringExtractFut {
encoding: &'static Encoding, encoding: &'static Encoding,
} }
impl<'a> Future for StringExtractFut { impl Future for StringExtractFut {
type Output = Result<String, Error>; type Output = Result<String, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {

View File

@ -169,6 +169,7 @@ impl<T: DeserializeOwned> FromRequest for Query<T> {
/// ``` /// ```
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct QueryConfig { pub struct QueryConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>, err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>,
} }

View File

@ -1,8 +1,8 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2022-xx-xx
### Changed ### Changed
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 3.0.0 - 2022-03-07 ## 3.0.0 - 2022-03-07

View File

@ -90,25 +90,25 @@ cookie = { version = "0.16", features = ["percent-encode"], optional = true }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.21", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3", features = ["openssl"] } actix-http = { version = "3", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] } actix-test = { version = "0.1", features = ["openssl", "rustls"] }
actix-tls = { version = "3", features = ["openssl", "rustls"] } actix-tls = { version = "3", features = ["openssl", "rustls"] }
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] } actix-web = { version = "4", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.4"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.8" rcgen = "0.9"
rustls-pemfile = "0.2" rustls-pemfile = "1"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.11" zstd = "0.11"

View File

@ -1 +1 @@
msrv = "1.56" msrv = "1.57"

View File

@ -98,7 +98,7 @@ rm -f $README_FILE.bak
echo "manifest, changelog, and readme updated" echo "manifest, changelog, and readme updated"
echo echo
echo "check other references:" echo "check other references:"
rg --glob='**/Cargo.toml' "\ rg --glob='**/{Cargo.toml,README.md}' "\
${PACKAGE_NAME} ?= ?\"[^\"]+\"\ ${PACKAGE_NAME} ?= ?\"[^\"]+\"\
|${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\ |${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\
|package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\ |package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\