Compare commits

...

25 Commits

Author SHA1 Message Date
Joel Wurtz 40b2c377cf
Merge 6a8b4f7ffe into 98d7d0b46b 2025-08-29 22:48:11 +01:00
Rob Ede 98d7d0b46b
chore(actix-files): prepare release 0.6.7 2025-08-29 22:31:48 +01:00
Rob Ede 4966a54e05
refactor(files): rename read_mode_threshold fn 2025-08-29 22:30:47 +01:00
Andrew Scott 00b0f8f700
feat(actix-files): opt-in filesize threshold for faster synchronous reads (#3706)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 21:52:34 +01:00
励志买套上海苏河湾大平层 3c2907da41
docs(middleware): complete middleware author's guide (#3680)
Add comprehensive documentation for middleware development in Actix Web, including:
- Detailed explanation of middleware concepts and execution flow
- Clear description of middleware traits and their responsibilities
- Guidelines for body type handling
- Best practices for middleware development
- Error handling recommendations
- Usage scenarios and anti-patterns

Co-authored-by: chenjjiaa <chenjjiaaa@gmail.com>
2025-08-29 20:12:05 +00:00
George Pollard 5041cd1c65
Make 'ws' feature of actix-http optional in actix-web (#3734)
* Make 'ws' feature of actix-http optional

* Update CHANGES.md

* Update actix-web-actors

* Update CHANGES.md

* nits

* nits

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-08-29 02:50:05 +00:00
Thales d3c46537b3
fix(http): Wake Payload when feeding error or EOF (#3749)
* fix(http): Add failing tests to demonstrate the payload problem

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

* fix(http): Wake Payload when feeding error or eof

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>

---------

Signed-off-by: Thales Fragoso <thales.fragoso@axiros.com>
2025-08-29 02:47:03 +00:00
Rob Ede 8996198f2c
chore: require h2 versions after MadeYouReset fix 2025-08-26 23:59:57 +01:00
Rob Ede 68624ec63b
chore: remove now-useless docs.rs flags 2025-08-26 23:51:22 +01:00
Rob Ede bcd0ffb016
chore: add multi-crate publish script 2025-08-26 09:25:22 +01:00
Rob Ede 9fb6c13a1a
ci: fix msrv job 2025-08-26 08:26:49 +01:00
Rob Ede 05cfef7f4b
ci: fix msrv job 2025-08-26 08:18:34 +01:00
Rob Ede 8f3eb32a32
chore: fix justfile for msrv 2025-08-26 08:00:19 +01:00
Rob Ede ddd16ec9db
chore(actix-http): prepare release 3.11.1 2025-08-26 07:28:27 +01:00
dependabot[bot] 9c47a247fb
build(deps): bump bitflags from 2.9.2 to 2.9.3 (#3745)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.9.2 to 2.9.3.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.9.2...2.9.3)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 12:31:30 +00:00
dependabot[bot] 2536823e3b
build(deps): bump regex-lite from 0.1.6 to 0.1.7 (#3743)
Bumps [regex-lite](https://github.com/rust-lang/regex) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-lite-0.1.6...regex-lite-0.1.7)

---
updated-dependencies:
- dependency-name: regex-lite
  dependency-version: 0.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:43:50 +00:00
dependabot[bot] e3f81d0643
build(deps): bump url from 2.5.4 to 2.5.7 (#3742)
Bumps [url](https://github.com/servo/rust-url) from 2.5.4 to 2.5.7.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/commits)

---
updated-dependencies:
- dependency-name: url
  dependency-version: 2.5.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:42:00 +00:00
dependabot[bot] a84aee836b
build(deps): bump tempfile from 3.20.0 to 3.21.0 (#3744)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/commits)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:40:55 +00:00
dependabot[bot] 1f08cb24c3
build(deps): bump regex from 1.11.1 to 1.11.2 (#3746)
Bumps [regex](https://github.com/rust-lang/regex) from 1.11.1 to 1.11.2.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.11.1...1.11.2)

---
updated-dependencies:
- dependency-name: regex
  dependency-version: 1.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:36:40 +00:00
dependabot[bot] 1b49047086
build(deps): bump codecov/codecov-action from 5.4.3 to 5.5.0 (#3741)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.3 to 5.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](18283e04ce...fdcc847654)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:33:15 +00:00
dependabot[bot] 9fe033a963
build(deps): bump taiki-e/install-action from 2.58.17 to 2.58.21 (#3740)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.58.17 to 2.58.21.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](ad95d4e02e...f63c33fd96)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.58.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:32:36 +00:00
dependabot[bot] 2ba69a1904
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.13.0 to 1.14.0 (#3739)
build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](fb51252c7b...ab6845274e)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 10:31:31 +00:00
Andrew Scott c6352005f7
fix: include content-length with bytes payload (#3695)
* fix: include content-length with bytes payload

* chore: json unit-test patch

* Update doc comment

---------

Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2025-08-24 09:42:11 +00:00
Rob Ede 6a8b4f7ffe
Merge branch 'master' into feat/awc-sni-host 2024-12-29 16:05:43 +00:00
Joel Wurtz 0915879267
feat(awc): allow to set a specific sni host on the request 2024-12-11 13:05:29 +01:00
46 changed files with 986 additions and 430 deletions

View File

@ -3,6 +3,6 @@ disallowed-names = [
"e", # no single letter error bindings
]
disallowed-methods = [
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default" },
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default" },
{ path = "std::cell::RefCell::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
{ path = "std::rc::Rc::default()", reason = "prefer explicit inner type default (remove allow-invalid when rust-lang/rust-clippy/#8581 is fixed)", allow-invalid = true },
]

View File

@ -9,4 +9,5 @@ words:
- rustls
- rustup
- serde
- uring
- zstd

View File

@ -44,12 +44,12 @@ jobs:
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -80,10 +80,10 @@ jobs:
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack

View File

@ -59,12 +59,12 @@ jobs:
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -92,7 +92,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
@ -108,12 +108,12 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just

View File

@ -18,13 +18,13 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just,cargo-llvm-cov,cargo-nextest
@ -32,7 +32,7 @@ jobs:
run: just test-coverage-codecov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
with:
files: codecov.json
fail_ci_if_error: true

View File

@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: rustfmt
@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
components: clippy
@ -55,7 +55,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: nightly
components: rust-docs
@ -72,12 +72,12 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Rust (${{ vars.RUST_VERSION_EXTERNAL_TYPES }})
uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0
uses: actions-rust-lang/setup-rust-toolchain@ab6845274e2ff01cd4462007e1a9d9df1ab49f42 # v1.14.0
with:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@ad95d4e02e061d4390c4b66ef5ed56c7fee3d2ce # v2.58.17
uses: taiki-e/install-action@f63c33fd96cc1e69a29bafd06541cf28588b43a4 # v2.58.21
with:
tool: just

502
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,9 @@
## Unreleased
## 0.6.7
- Add `{Files, NamedFile}::read_mode_threshold()` methods to allow faster synchronous reads of small files.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.6

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.6"
version = "0.6.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"]

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start -->
[![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.6)](https://docs.rs/actix-files/0.6.6)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.7)](https://docs.rs/actix-files/0.6.7)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6)
[![dependency status](https://deps.rs/crate/actix-files/0.6.7/status.svg)](https://deps.rs/crate/actix-files/0.6.7)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -14,6 +14,12 @@ use pin_project_lite::pin_project;
use super::named::File;
#[derive(Debug, Clone, Copy)]
pub(crate) enum ReadMode {
Sync,
Async,
}
pin_project! {
/// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)]
@ -24,6 +30,7 @@ pin_project! {
state: ChunkedReadFileState<Fut>,
counter: u64,
callback: F,
read_mode: ReadMode,
}
}
@ -57,6 +64,7 @@ pub(crate) fn new_chunked_read(
size: u64,
offset: u64,
file: File,
read_mode_threshold: u64,
) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile {
size,
@ -69,31 +77,50 @@ pub(crate) fn new_chunked_read(
},
counter: 0,
callback: chunked_read_file_callback,
read_mode: if size < read_mode_threshold {
ReadMode::Sync
} else {
ReadMode::Async
},
}
}
#[cfg(not(feature = "experimental-io-uring"))]
async fn chunked_read_file_callback(
fn chunked_read_file_callback_sync(
mut file: File,
offset: u64,
max_bytes: usize,
) -> Result<(File, Bytes), Error> {
) -> Result<(File, Bytes), io::Error> {
use io::{Read as _, Seek as _};
let res = actix_web::web::block(move || {
let mut buf = Vec::with_capacity(max_bytes);
let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?;
file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
}
}
#[cfg(not(feature = "experimental-io-uring"))]
#[inline]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
read_mode: ReadMode,
) -> Result<(File, Bytes), Error> {
let res = match read_mode {
ReadMode::Sync => chunked_read_file_callback_sync(file, offset, max_bytes)?,
ReadMode::Async => {
actix_web::web::block(move || chunked_read_file_callback_sync(file, offset, max_bytes))
.await??
}
})
.await??;
};
Ok(res)
}
@ -171,7 +198,7 @@ where
#[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize) -> Fut,
F: Fn(File, u64, usize, ReadMode) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>,
{
type Item = Result<Bytes, Error>;
@ -193,7 +220,7 @@ where
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes);
let fut = (this.callback)(file, offset, max_bytes, *this.read_mode);
this.state
.project_replace(ChunkedReadFileState::Future { fut });

View File

@ -49,6 +49,7 @@ pub struct Files {
use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
hidden_files: bool,
read_mode_threshold: u64,
}
impl fmt::Debug for Files {
@ -73,6 +74,7 @@ impl Clone for Files {
use_guards: self.use_guards.clone(),
guards: self.guards.clone(),
hidden_files: self.hidden_files,
read_mode_threshold: self.read_mode_threshold,
}
}
}
@ -119,6 +121,7 @@ impl Files {
use_guards: None,
guards: Vec::new(),
hidden_files: false,
read_mode_threshold: 0,
}
}
@ -204,6 +207,23 @@ impl Files {
self
}
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to use ETag or not.
///
/// Default is true.
@ -367,6 +387,7 @@ impl ServiceFactory<ServiceRequest> for Files {
file_flags: self.file_flags,
guards: self.use_guards.clone(),
hidden_files: self.hidden_files,
size_threshold: self.read_mode_threshold,
};
if let Some(ref default) = *self.default.borrow() {

View File

@ -80,6 +80,7 @@ pub struct NamedFile {
pub(crate) content_type: Mime,
pub(crate) content_disposition: ContentDisposition,
pub(crate) encoding: Option<ContentEncoding>,
pub(crate) read_mode_threshold: u64,
}
#[cfg(not(feature = "experimental-io-uring"))]
@ -200,6 +201,7 @@ impl NamedFile {
encoding,
status_code: StatusCode::OK,
flags: Flags::default(),
read_mode_threshold: 0,
})
}
@ -353,6 +355,23 @@ impl NamedFile {
self
}
/// Sets the size threshold that determines file read mode (sync/async).
///
/// When a file is smaller than the threshold (bytes), the reader will switch from synchronous
/// (blocking) file-reads to async reads to avoid blocking the main-thread when processing large
/// files.
///
/// Tweaking this value according to your expected usage may lead to signifiant performance
/// gains (or losses in other handlers, if `size` is too high).
///
/// When the `experimental-io-uring` crate feature is enabled, file reads are always async.
///
/// Default is 0, meaning all files are read asynchronously.
pub fn read_mode_threshold(mut self, size: u64) -> Self {
self.read_mode_threshold = size;
self
}
/// Specifies whether to return `ETag` header in response.
///
/// Default is true.
@ -440,7 +459,8 @@ impl NamedFile {
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
}
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
let reader =
chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
return res.streaming(reader);
}
@ -577,7 +597,7 @@ impl NamedFile {
.map_into_boxed_body();
}
let reader = chunked::new_chunked_read(length, offset, self.file);
let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
if offset != 0 || length != self.md.len() {
res.status(StatusCode::PARTIAL_CONTENT);

View File

@ -39,6 +39,7 @@ pub struct FilesServiceInner {
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
pub(crate) size_threshold: u64,
}
impl fmt::Debug for FilesServiceInner {
@ -70,7 +71,9 @@ impl FilesService {
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
let res = named_file
.read_mode_threshold(self.size_threshold)
.into_response(&req);
ServiceResponse::new(req, res)
}
@ -169,17 +172,7 @@ impl Service<ServiceRequest> for FilesService {
}
} else {
match NamedFile::open_async(&path).await {
Ok(mut named_file) => {
if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = this.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(err) => this.handle_err(err, req).await,
}
}

View File

@ -2,7 +2,13 @@
## Unreleased
- Malformed websocket frames are now gracefully rejected.
- Properly wake Payload receivers when feeding errors or EOF
## 3.11.1
- Prevent more hangs after client disconnects.
- More malformed WebSocket frames are now gracefully rejected.
- Using `TestRequest::set_payload()` now sets a Content-Length header.
## 3.11.0

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.11.0"
version = "3.11.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>", "Rob Ede <robjtede@icloud.com>"]
description = "HTTP types and services for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"]
@ -17,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"http2",
"ws",
@ -119,7 +118,7 @@ tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2
h2 = { version = "0.3.26", optional = true }
h2 = { version = "0.3.27", optional = true }
# websockets
base64 = { version = "0.22", optional = true }
@ -157,7 +156,7 @@ serde_json = "1.0"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.55" }
tls-rustls_023 = { package = "rustls", version = "0.23" }
tokio = { version = "1.38.2", features = ["net", "rt", "macros"] }
tokio = { version = "1.38.2", features = ["net", "rt", "macros", "sync"] }
[lints]
workspace = true

View File

@ -5,11 +5,11 @@
<!-- prettier-ignore-start -->
[![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.11.0)](https://docs.rs/actix-http/3.11.0)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.11.1)](https://docs.rs/actix-http/3.11.1)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.11.0/status.svg)](https://deps.rs/crate/actix-http/3.11.0)
[![dependency status](https://deps.rs/crate/actix-http/3.11.1/status.svg)](https://deps.rs/crate/actix-http/3.11.1)
[![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)

View File

@ -200,11 +200,13 @@ impl Inner {
#[inline]
fn set_error(&mut self, err: PayloadError) {
self.err = Some(err);
self.wake();
}
#[inline]
fn feed_eof(&mut self) {
self.eof = true;
self.wake();
}
#[inline]
@ -253,8 +255,13 @@ impl Inner {
#[cfg(test)]
mod tests {
use std::{task::Poll, time::Duration};
use actix_rt::time::timeout;
use actix_utils::future::poll_fn;
use futures_util::{FutureExt, StreamExt};
use static_assertions::{assert_impl_all, assert_not_impl_any};
use tokio::sync::oneshot;
use super::*;
@ -263,6 +270,67 @@ mod tests {
assert_impl_all!(Inner: Unpin, Send, Sync);
const WAKE_TIMEOUT: Duration = Duration::from_secs(2);
fn prepare_waking_test(
mut payload: Payload,
expected: Option<Result<(), ()>>,
) -> (oneshot::Receiver<()>, actix_rt::task::JoinHandle<()>) {
let (tx, rx) = oneshot::channel();
let handle = actix_rt::spawn(async move {
// Make sure to poll once to set the waker
poll_fn(|cx| {
assert!(payload.poll_next_unpin(cx).is_pending());
Poll::Ready(())
})
.await;
tx.send(()).unwrap();
// actix-rt is single-threaded, so this won't race with `rx.await`
let mut pend_once = false;
poll_fn(|_| {
if pend_once {
Poll::Ready(())
} else {
// Return pending without storing wakers, we already did on the previous
// `poll_fn`, now this task will only continue if the `sender` wakes us
pend_once = true;
Poll::Pending
}
})
.await;
let got = payload.next().now_or_never().unwrap();
match expected {
Some(Ok(_)) => assert!(got.unwrap().is_ok()),
Some(Err(_)) => assert!(got.unwrap().is_err()),
None => assert!(got.is_none()),
}
});
(rx, handle)
}
#[actix_rt::test]
async fn wake_on_error() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, Some(Err(())));
rx.await.unwrap();
sender.set_error(PayloadError::Incomplete(None));
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn wake_on_eof() {
let (mut sender, payload) = Payload::create(false);
let (rx, handle) = prepare_waking_test(payload, None);
rx.await.unwrap();
sender.feed_eof();
timeout(WAKE_TIMEOUT, handle).await.unwrap().unwrap();
}
#[actix_rt::test]
async fn test_unread_data() {
let (_, mut payload) = Payload::create(false);

View File

@ -11,7 +11,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version};
use http::{header, Method, Uri, Version};
use crate::{
header::{HeaderMap, TryIntoHeaderPair},
@ -98,9 +98,13 @@ impl TestRequest {
}
/// Set request payload.
///
/// This sets the `Content-Length` header with the size of `data`.
pub fn set_payload(&mut self, data: impl Into<Bytes>) -> &mut Self {
let mut payload = crate::h1::Payload::empty();
payload.unread_data(data.into());
let bytes = data.into();
self.insert_header((header::CONTENT_LENGTH, bytes.len()));
payload.unread_data(bytes);
parts(&mut self.0).payload = Some(payload.into());
self
}

View File

@ -11,7 +11,6 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[lib]

View File

@ -14,7 +14,6 @@ license.workspace = true
edition.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[package.metadata.cargo_check_external_types]

View File

@ -13,6 +13,7 @@ macro_rules! register {
register!(finish => "(.*)", "(.*)", "(.*)", "(.*)")
}};
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
#[expect(clippy::useless_concat)]
let arr = [
concat!("/authorizations"),
concat!("/authorizations/", $p1),

View File

@ -24,7 +24,7 @@ allowed_external_types = [
actix = { version = ">=0.12, <0.14", default-features = false }
actix-codec = "0.5"
actix-http = "3"
actix-web = { version = "4", default-features = false }
actix-web = { version = "4", default-features = false, features = ["ws"] }
bytes = "1"
bytestring = "1"

View File

@ -4,6 +4,7 @@
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now calls `actix_web::response::builder::HttpResponseBuilder::no_chunking()` if `Content-Length` is set by user.
- Add `ws` crate feature (on-by-default) which forwards to `actix-http` and guards some of its `ResponseError` impls.
## 4.11.0

View File

@ -17,7 +17,6 @@ edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"macros",
"openssl",
@ -68,6 +67,7 @@ default = [
"http2",
"unicode",
"compat",
"ws",
]
# Brotli algorithm content-encoding support
@ -86,9 +86,12 @@ cookies = ["dep:cookie"]
# Secure & signed cookies
secure-cookies = ["cookies", "cookie/secure"]
# HTTP/2 support (including h2c).
# HTTP/2 support (including h2c)
http2 = ["actix-http/http2"]
# WebSocket support
ws = ["actix-http/ws"]
# TLS via OpenSSL
openssl = ["__tls", "http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
@ -132,7 +135,7 @@ actix-service = "2"
actix-tls = { version = "3.4", default-features = false, optional = true }
actix-utils = "3"
actix-http = { version = "3.11", features = ["ws"] }
actix-http = "3.11"
actix-router = { version = "0.5.3", default-features = false, features = ["http"] }
actix-web-codegen = { version = "4.3", optional = true, default-features = false }

View File

@ -3,7 +3,6 @@
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- Cookie handling has been offloaded to the `cookie` crate:
- `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
- Some types now require lifetime parameters.

View File

@ -7,7 +7,6 @@ use std::{
io::{self, Write as _},
};
use actix_http::Response;
use bytes::BytesMut;
use crate::{
@ -126,20 +125,24 @@ impl ResponseError for actix_http::error::PayloadError {
}
}
impl ResponseError for actix_http::ws::ProtocolError {}
impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse<BoxBody> {
Response::from(self).map_into_boxed_body().into()
actix_http::Response::from(self)
.map_into_boxed_body()
.into()
}
}
#[cfg(feature = "ws")]
impl ResponseError for actix_http::ws::ProtocolError {}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -2,16 +2,80 @@
## What Is A Middleware?
Middleware in Actix Web is a powerful mechanism that allows you to add additional behavior to request/response processing. It enables you to:
- Pre-process incoming requests (e.g., path normalization, authentication)
- Post-process outgoing responses (e.g., logging, compression)
- Modify application state through ServiceRequest
- Access external services (e.g., sessions, caching)
Middleware is registered for each App, Scope, or Resource and executed in the reverse order of registration. This means the last registered middleware is the first to process the request.
## Middleware Traits
Actix Web's middleware system is built on two main traits:
1. `Transform<S, Req>`: The builder trait that creates the actual Service. It's responsible for:
- Creating new middleware instances
- Assembling the middleware chain
- Handling initialization errors
2. `Service<Req>`: The trait that represents the actual middleware functionality. It:
- Processes requests and responses
- Can modify both request and response
- Can short-circuit request processing
- Must be implemented for the middleware to work
## Understanding Body Types
When working with middleware, it's important to understand body types:
- Middleware can work with different body types for requests and responses
- The `MessageBody` trait is used to handle different body types
- You can use `EitherBody` when you need to handle multiple body types
- Be careful with body consumption - once a body is consumed, it cannot be read again
## Best Practices
1. Keep middleware focused and single-purpose
2. Handle errors appropriately and propagate them correctly
3. Be mindful of performance impact
4. Use appropriate body types and handle them correctly
5. Consider middleware ordering carefully
6. Document your middleware's behavior and requirements
7. Test your middleware thoroughly
## Error Propagation
Proper error handling is crucial in middleware:
1. Always propagate errors from the inner service
2. Use appropriate error types
3. Handle initialization errors
4. Consider using custom error types for specific middleware errors
5. Document error conditions and handling
## When To (Not) Use Middleware
Use middleware when you need to:
- Add cross-cutting concerns
- Modify requests/responses globally
- Add authentication/authorization
- Add logging or monitoring
- Handle compression or caching
Avoid middleware when:
- The functionality is specific to a single route
- The operation is better handled by a service
- The overhead would be too high
- The functionality can be implemented more simply
## Author's References
- `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428
- Actix Web Documentation: https://docs.rs/actix-web
- Service Trait Documentation: https://docs.rs/actix-service
- MessageBody Trait Documentation: https://docs.rs/actix-web/latest/actix_web/body/trait.MessageBody.html

View File

@ -616,7 +616,7 @@ mod tests {
}
));
let (req, mut pl) = TestRequest::default()
let (mut req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
@ -624,6 +624,7 @@ mod tests {
.set_payload(Bytes::from_static(&[0u8; 1000]))
.to_http_parts();
req.head_mut().headers_mut().remove(header::CONTENT_LENGTH);
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
.limit(100)
.await;

View File

@ -12,6 +12,7 @@
- Do not send `Host` header on HTTP/2 requests, as it is not required, and some web servers may reject it.
- Update `brotli` dependency to `7`.
- Minimum supported Rust version (MSRV) is now 1.75.
- Allow to set a specific SNI hostname on the request for TLS connections.
## 3.5.1

View File

@ -16,7 +16,6 @@ license = "MIT OR Apache-2.0"
edition = "2021"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = [
"cookies",
"openssl",
@ -109,7 +108,7 @@ cfg-if = "1"
derive_more = { version = "2", features = ["display", "error", "from"] }
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.26"
h2 = "0.3.27"
http = "0.2.7"
itoa = "1"
log = "0.4"

View File

@ -3,7 +3,6 @@ use std::{fmt, net::IpAddr, rc::Rc, time::Duration};
use actix_http::{
error::HttpError,
header::{self, HeaderMap, HeaderName, TryIntoHeaderPair},
Uri,
};
use actix_rt::net::{ActixStream, TcpStream};
use actix_service::{boxed, Service};
@ -11,7 +10,8 @@ use base64::prelude::*;
use crate::{
client::{
ClientConfig, ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection,
ClientConfig, ConnectInfo, Connector, ConnectorService, HostnameWithSni, TcpConnectError,
TcpConnection,
},
connect::DefaultConnector,
error::SendRequestError,
@ -46,8 +46,8 @@ impl ClientBuilder {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> ClientBuilder<
impl Service<
ConnectInfo<Uri>,
Response = TcpConnection<Uri, TcpStream>,
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, TcpStream>,
Error = TcpConnectError,
> + Clone,
(),
@ -69,16 +69,22 @@ impl ClientBuilder {
impl<S, Io, M> ClientBuilder<S, M>
where
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
+ Clone
S: Service<
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, Io>,
Error = TcpConnectError,
> + Clone
+ 'static,
Io: ActixStream + fmt::Debug + 'static,
{
/// Use custom connector service.
pub fn connector<S1, Io1>(self, connector: Connector<S1>) -> ClientBuilder<S1, M>
where
S1: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io1>, Error = TcpConnectError>
+ Clone
S1: Service<
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, Io1>,
Error = TcpConnectError,
> + Clone
+ 'static,
Io1: ActixStream + fmt::Debug + 'static,
{

View File

@ -16,10 +16,9 @@ use actix_rt::{
use actix_service::Service;
use actix_tls::connect::{
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection,
Connector as TcpConnector, Resolver,
Connector as TcpConnector, Host, Resolver,
};
use futures_core::{future::LocalBoxFuture, ready};
use http::Uri;
use pin_project_lite::pin_project;
use super::{
@ -27,9 +26,41 @@ use super::{
connection::{Connection, ConnectionIo},
error::ConnectError,
pool::ConnectionPool,
Connect,
Connect, ServerName,
};
pub enum HostnameWithSni {
ForTcp(String, u16, Option<ServerName>),
ForTls(String, u16, Option<ServerName>),
}
impl Host for HostnameWithSni {
fn hostname(&self) -> &str {
match self {
HostnameWithSni::ForTcp(hostname, _, _) => hostname,
HostnameWithSni::ForTls(hostname, _, sni) => sni.as_deref().unwrap_or(hostname),
}
}
fn port(&self) -> Option<u16> {
match self {
HostnameWithSni::ForTcp(_, port, _) => Some(*port),
HostnameWithSni::ForTls(_, port, _) => Some(*port),
}
}
}
impl HostnameWithSni {
pub fn to_tls(self) -> Self {
match self {
HostnameWithSni::ForTcp(hostname, port, sni) => {
HostnameWithSni::ForTls(hostname, port, sni)
}
HostnameWithSni::ForTls(_, _, _) => self,
}
}
}
enum OurTlsConnector {
#[allow(dead_code)] // only dead when no TLS feature is enabled
None,
@ -95,8 +126,8 @@ impl Connector<()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector<
impl Service<
ConnectInfo<Uri>,
Response = TcpConnection<Uri, TcpStream>,
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, TcpStream>,
Error = actix_tls::connect::ConnectError,
> + Clone,
> {
@ -214,8 +245,11 @@ impl<S> Connector<S> {
pub fn connector<S1, Io1>(self, connector: S1) -> Connector<S1>
where
Io1: ActixStream + fmt::Debug + 'static,
S1: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io1>, Error = TcpConnectError>
+ Clone,
S1: Service<
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, Io1>,
Error = TcpConnectError,
> + Clone,
{
Connector {
connector,
@ -235,8 +269,11 @@ where
// This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code.
IO: ActixStream + fmt::Debug + 'static,
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, IO>, Error = TcpConnectError>
+ Clone
S: Service<
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, IO>,
Error = TcpConnectError,
> + Clone
+ 'static,
{
/// Sets TCP connection timeout.
@ -454,7 +491,7 @@ where
use actix_utils::future::{ready, Ready};
#[allow(non_local_definitions)]
impl IntoConnectionIo for TcpConnection<Uri, Box<dyn ConnectionIo>> {
impl IntoConnectionIo for TcpConnection<HostnameWithSni, Box<dyn ConnectionIo>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let io = self.into_parts().0;
(io, Protocol::Http2)
@ -505,7 +542,7 @@ where
use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector};
#[allow(non_local_definitions)]
impl<IO: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncSslStream<IO>> {
impl<IO: ConnectionIo> IntoConnectionIo for TcpConnection<HostnameWithSni, AsyncSslStream<IO>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
@ -544,7 +581,7 @@ where
use actix_tls::connect::rustls_0_20::{reexports::AsyncTlsStream, TlsConnector};
#[allow(non_local_definitions)]
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> {
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<HostnameWithSni, AsyncTlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
@ -579,7 +616,7 @@ where
use actix_tls::connect::rustls_0_21::{reexports::AsyncTlsStream, TlsConnector};
#[allow(non_local_definitions)]
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> {
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<HostnameWithSni, AsyncTlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
@ -617,7 +654,7 @@ where
use actix_tls::connect::rustls_0_22::{reexports::AsyncTlsStream, TlsConnector};
#[allow(non_local_definitions)]
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> {
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<HostnameWithSni, AsyncTlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
@ -652,7 +689,7 @@ where
use actix_tls::connect::rustls_0_23::{reexports::AsyncTlsStream, TlsConnector};
#[allow(non_local_definitions)]
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> {
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<HostnameWithSni, AsyncTlsStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
@ -693,7 +730,7 @@ where
}
}
/// tcp service for map `TcpConnection<Uri, Io>` type to `(Io, Protocol)`
/// tcp service for map `TcpConnection<HostnameWithSni, Io>` type to `(Io, Protocol)`
#[derive(Clone)]
pub struct TcpConnectorService<S: Clone> {
service: S,
@ -701,7 +738,9 @@ pub struct TcpConnectorService<S: Clone> {
impl<S, Io> Service<Connect> for TcpConnectorService<S>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError> + Clone + 'static,
S: Service<Connect, Response = TcpConnection<HostnameWithSni, Io>, Error = ConnectError>
+ Clone
+ 'static,
{
type Response = (Io, Protocol);
type Error = ConnectError;
@ -726,7 +765,7 @@ pin_project! {
impl<Fut, Io> Future for TcpConnectorFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut: Future<Output = Result<TcpConnection<HostnameWithSni, Io>, ConnectError>>,
{
type Output = Result<(Io, Protocol), ConnectError>;
@ -772,9 +811,10 @@ struct TlsConnectorService<Tcp, Tls> {
))]
impl<Tcp, Tls, IO> Service<Connect> for TlsConnectorService<Tcp, Tls>
where
Tcp:
Service<Connect, Response = TcpConnection<Uri, IO>, Error = ConnectError> + Clone + 'static,
Tls: Service<TcpConnection<Uri, IO>, Error = std::io::Error> + Clone + 'static,
Tcp: Service<Connect, Response = TcpConnection<HostnameWithSni, IO>, Error = ConnectError>
+ Clone
+ 'static,
Tls: Service<TcpConnection<HostnameWithSni, IO>, Error = std::io::Error> + Clone + 'static,
Tls::Response: IntoConnectionIo,
IO: ConnectionIo,
{
@ -827,9 +867,14 @@ trait IntoConnectionIo {
impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2>
where
S: Service<TcpConnection<Uri, Io>, Response = Res, Error = std::io::Error, Future = Fut2>,
S: Service<
TcpConnection<HostnameWithSni, Io>,
Response = Res,
Error = std::io::Error,
Future = Fut2,
>,
S::Response: IntoConnectionIo,
Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut1: Future<Output = Result<TcpConnection<HostnameWithSni, Io>, ConnectError>>,
Fut2: Future<Output = Result<S::Response, S::Error>>,
Io: ConnectionIo,
{
@ -843,10 +888,11 @@ where
timeout,
} => {
let res = ready!(fut.poll(cx))?;
let (io, hostname_with_sni) = res.into_parts();
let fut = tls_service
.take()
.expect("TlsConnectorFuture polled after complete")
.call(res);
.call(TcpConnection::new(hostname_with_sni.to_tls(), io));
let timeout = sleep(*timeout);
self.set(TlsConnectorFuture::TlsConnect { fut, timeout });
self.poll(cx)
@ -880,8 +926,11 @@ impl<S: Clone> TcpConnectorInnerService<S> {
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
+ Clone
S: Service<
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, Io>,
Error = TcpConnectError,
> + Clone
+ 'static,
{
type Response = S::Response;
@ -891,7 +940,13 @@ where
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
let mut req = ConnectInfo::new(req.uri).set_addr(req.addr);
let mut req = ConnectInfo::new(HostnameWithSni::ForTcp(
req.hostname,
req.port,
req.sni_host,
))
.set_addr(req.addr)
.set_port(req.port);
if let Some(local_addr) = self.local_address {
req = req.set_local_addr(local_addr);
@ -916,9 +971,9 @@ pin_project! {
impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, TcpConnectError>>,
Fut: Future<Output = Result<TcpConnection<HostnameWithSni, Io>, TcpConnectError>>,
{
type Output = Result<TcpConnection<Uri, Io>, ConnectError>;
type Output = Result<TcpConnection<HostnameWithSni, Io>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
@ -978,16 +1033,17 @@ where
}
fn call(&self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => match self.tls_pool {
if req.tls {
match &self.tls_pool {
None => ConnectorServiceFuture::SslIsNotSupported,
Some(ref pool) => ConnectorServiceFuture::Tls {
Some(pool) => ConnectorServiceFuture::Tls {
fut: pool.call(req),
},
},
_ => ConnectorServiceFuture::Tcp {
}
} else {
ConnectorServiceFuture::Tcp {
fut: self.tcp_pool.call(req),
},
}
}
}
}

View File

@ -1,6 +1,6 @@
//! HTTP client.
use std::{rc::Rc, time::Duration};
use std::{ops::Deref, rc::Rc, time::Duration};
use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri};
use actix_rt::net::TcpStream;
@ -21,13 +21,33 @@ mod pool;
pub use self::{
connection::{Connection, ConnectionIo},
connector::{Connector, ConnectorService},
connector::{Connector, ConnectorService, HostnameWithSni},
error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError},
};
#[derive(Clone)]
#[derive(Clone, Hash, PartialEq, Eq)]
pub enum ServerName {
Owned(String),
Borrowed(Rc<String>),
}
impl Deref for ServerName {
type Target = str;
fn deref(&self) -> &str {
match self {
ServerName::Owned(ref s) => s,
ServerName::Borrowed(ref s) => s,
}
}
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct Connect {
pub uri: Uri,
pub hostname: String,
pub sni_host: Option<ServerName>,
pub port: u16,
pub tls: bool,
pub addr: Option<std::net::SocketAddr>,
}
@ -79,8 +99,8 @@ impl Client {
/// This function is equivalent of `ClientBuilder::new()`.
pub fn builder() -> ClientBuilder<
impl Service<
ConnectInfo<Uri>,
Response = TcpConnection<Uri, TcpStream>,
ConnectInfo<HostnameWithSni>,
Response = TcpConnection<HostnameWithSni, TcpStream>,
Error = TcpConnectError,
> + Clone,
> {

View File

@ -4,6 +4,7 @@ use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
future::Future,
hash::Hash,
io,
ops::Deref,
pin::Pin,
@ -127,7 +128,7 @@ where
Io: AsyncWrite + Unpin + 'static,
{
config: ConnectorConfig,
available: RefCell<HashMap<Key, VecDeque<PooledConnection<Io>>>>,
available: RefCell<HashMap<Connect, VecDeque<PooledConnection<Io>>>>,
permits: Arc<Semaphore>,
}
@ -168,12 +169,6 @@ where
let inner = self.inner.clone();
Box::pin(async move {
let key = if let Some(authority) = req.uri.authority() {
authority.clone().into()
} else {
return Err(ConnectError::Unresolved);
};
// acquire an owned permit and carry it with connection
let permit = Arc::clone(&inner.permits)
.acquire_owned()
@ -190,7 +185,7 @@ where
// check if there is idle connection for given key.
let mut map = inner.available.borrow_mut();
if let Some(conns) = map.get_mut(&key) {
if let Some(conns) = map.get_mut(&req) {
let now = Instant::now();
while let Some(mut c) = conns.pop_front() {
@ -231,7 +226,11 @@ where
// construct acquired. It's used to put Io type back to pool/ close the Io type.
// permit is carried with the whole lifecycle of Acquired.
let acquired = Acquired { key, inner, permit };
let acquired = Acquired {
req: req.clone(),
inner,
permit,
};
// match the connection and spawn new one if did not get anything.
match conn {
@ -343,8 +342,8 @@ pub struct Acquired<Io>
where
Io: AsyncWrite + Unpin + 'static,
{
/// authority key for identify connection.
key: Key,
/// hash key for identify connection.
req: Connect,
/// handle to connection pool.
inner: ConnectionPoolInner<Io>,
/// permit for limit concurrent in-flight connection for a Client object.
@ -359,12 +358,12 @@ impl<Io: ConnectionIo> Acquired<Io> {
/// Release IO back into pool.
pub(super) fn release(&self, conn: ConnectionInnerType<Io>, created: Instant) {
let Acquired { key, inner, .. } = self;
let Acquired { req, inner, .. } = self;
inner
.available
.borrow_mut()
.entry(key.clone())
.entry(req.clone())
.or_insert_with(VecDeque::new)
.push_back(PooledConnection {
conn,
@ -380,8 +379,6 @@ impl<Io: ConnectionIo> Acquired<Io> {
mod test {
use std::cell::Cell;
use http::Uri;
use super::*;
/// A stream type that always returns pending on async read.
@ -466,7 +463,10 @@ mod test {
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("http://localhost"),
hostname: "localhost".to_string(),
port: 80,
tls: false,
sni_host: None,
addr: None,
};
@ -506,7 +506,10 @@ mod test {
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("http://localhost"),
hostname: "localhost".to_string(),
port: 80,
tls: false,
sni_host: None,
addr: None,
};
@ -548,7 +551,10 @@ mod test {
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("http://localhost"),
hostname: "localhost".to_string(),
port: 80,
tls: false,
sni_host: None,
addr: None,
};
@ -587,7 +593,10 @@ mod test {
let pool = super::ConnectionPool::new(connector, config);
let req = Connect {
uri: Uri::from_static("https://crates.io"),
hostname: "crates.io".to_string(),
port: 443,
tls: true,
sni_host: None,
addr: None,
};
@ -600,7 +609,10 @@ mod test {
release(conn);
let req = Connect {
uri: Uri::from_static("https://google.com"),
hostname: "google.com".to_string(),
port: 443,
tls: true,
sni_host: None,
addr: None,
};
@ -624,7 +636,10 @@ mod test {
let pool = Rc::new(super::ConnectionPool::new(connector, config));
let req = Connect {
uri: Uri::from_static("https://crates.io"),
hostname: "crates.io".to_string(),
port: 443,
tls: true,
sni_host: None,
addr: None,
};
@ -633,7 +648,10 @@ mod test {
release(conn);
let req = Connect {
uri: Uri::from_static("https://google.com"),
hostname: "google.com".to_string(),
port: 443,
tls: true,
sni_host: None,
addr: None,
};
let conn = pool.call(req.clone()).await.unwrap();

View File

@ -13,7 +13,10 @@ use futures_core::{future::LocalBoxFuture, ready};
use crate::{
any_body::AnyBody,
client::{Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError},
client::{
Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError,
ServerName,
},
ClientResponse,
};
@ -32,13 +35,18 @@ pub type BoxedSocket = Box<dyn ConnectionIo>;
pub enum ConnectRequest {
/// Standard HTTP request.
///
/// Contains the request head, body type, and optional pre-resolved socket address.
Client(RequestHeadType, AnyBody, Option<net::SocketAddr>),
/// Contains the request head, body type, optional pre-resolved socket address and optional sni host.
Client(
RequestHeadType,
AnyBody,
Option<net::SocketAddr>,
Option<ServerName>,
),
/// Tunnel used by WebSocket connection requests.
///
/// Contains the request head and optional pre-resolved socket address.
Tunnel(RequestHead, Option<net::SocketAddr>),
/// Contains the request head, optional pre-resolved socket address and optional sni host.
Tunnel(RequestHead, Option<net::SocketAddr>, Option<ServerName>),
}
/// Combined HTTP response & WebSocket tunnel type returned from connection service.
@ -103,17 +111,41 @@ where
fn call(&self, req: ConnectRequest) -> Self::Future {
// connect to the host
let fut = match req {
ConnectRequest::Client(ref head, .., addr) => self.connector.call(ClientConnect {
uri: head.as_ref().uri.clone(),
addr,
}),
ConnectRequest::Tunnel(ref head, addr) => self.connector.call(ClientConnect {
uri: head.uri.clone(),
addr,
}),
let (head, addr, sni_host) = match req {
ConnectRequest::Client(ref head, .., addr, ref sni_host) => {
(head.as_ref(), addr, sni_host.clone())
}
ConnectRequest::Tunnel(ref head, addr, ref sni_host) => (head, addr, sni_host.clone()),
};
let authority = if let Some(authority) = head.uri.authority() {
authority
} else {
return ConnectRequestFuture::Error {
err: ConnectError::Unresolved,
};
};
let tls = match head.uri.scheme_str() {
Some("https") | Some("wss") => true,
_ => false,
};
let fut =
self.connector.call(ClientConnect {
hostname: authority.host().to_string(),
port: authority.port().map(|p| p.as_u16()).unwrap_or_else(|| {
if tls {
443
} else {
80
}
}),
tls,
sni_host,
addr,
});
ConnectRequestFuture::Connection {
fut,
req: Some(req),
@ -127,6 +159,9 @@ pin_project_lite::pin_project! {
where
Io: ConnectionIo
{
Error {
err: ConnectError
},
Connection {
#[pin]
fut: Fut,
@ -192,6 +227,10 @@ where
let framed = framed.into_map_io(|io| Box::new(io) as _);
Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed)))
}
ConnectRequestProj::Error { .. } => {
Poll::Ready(Err(SendRequestError::Connect(ConnectError::Unresolved)))
}
}
}
}

View File

@ -11,7 +11,7 @@ use futures_core::Stream;
use serde::Serialize;
use crate::{
client::ClientConfig,
client::{ClientConfig, ServerName},
sender::{RequestSender, SendClientRequest},
BoxError,
};
@ -26,6 +26,7 @@ pub struct FrozenClientRequest {
pub(crate) response_decompress: bool,
pub(crate) timeout: Option<Duration>,
pub(crate) config: ClientConfig,
pub(crate) sni_host: Option<ServerName>,
}
impl FrozenClientRequest {
@ -54,6 +55,7 @@ impl FrozenClientRequest {
self.response_decompress,
self.timeout,
&self.config,
self.sni_host.clone(),
body,
)
}
@ -65,6 +67,7 @@ impl FrozenClientRequest {
self.response_decompress,
self.timeout,
&self.config,
self.sni_host.clone(),
value,
)
}
@ -76,6 +79,7 @@ impl FrozenClientRequest {
self.response_decompress,
self.timeout,
&self.config,
self.sni_host.clone(),
value,
)
}
@ -91,6 +95,7 @@ impl FrozenClientRequest {
self.response_decompress,
self.timeout,
&self.config,
self.sni_host.clone(),
stream,
)
}
@ -102,6 +107,7 @@ impl FrozenClientRequest {
self.response_decompress,
self.timeout,
&self.config,
self.sni_host.clone(),
)
}
@ -156,6 +162,7 @@ impl FrozenSendBuilder {
self.req.response_decompress,
self.req.timeout,
&self.req.config,
self.req.sni_host.clone(),
body,
)
}
@ -171,6 +178,7 @@ impl FrozenSendBuilder {
self.req.response_decompress,
self.req.timeout,
&self.req.config,
self.req.sni_host.clone(),
value,
)
}
@ -186,6 +194,7 @@ impl FrozenSendBuilder {
self.req.response_decompress,
self.req.timeout,
&self.req.config,
self.req.sni_host.clone(),
value,
)
}
@ -205,6 +214,7 @@ impl FrozenSendBuilder {
self.req.response_decompress,
self.req.timeout,
&self.req.config,
self.req.sni_host.clone(),
stream,
)
}
@ -220,6 +230,7 @@ impl FrozenSendBuilder {
self.req.response_decompress,
self.req.timeout,
&self.req.config,
self.req.sni_host.clone(),
)
}
}

View File

@ -73,11 +73,13 @@ where
fn call(&self, req: ConnectRequest) -> Self::Future {
match req {
ConnectRequest::Tunnel(head, addr) => {
let fut = self.connector.call(ConnectRequest::Tunnel(head, addr));
ConnectRequest::Tunnel(head, addr, sni_host) => {
let fut = self
.connector
.call(ConnectRequest::Tunnel(head, addr, sni_host));
RedirectServiceFuture::Tunnel { fut }
}
ConnectRequest::Client(head, body, addr) => {
ConnectRequest::Client(head, body, addr, sni_host) => {
let connector = Rc::clone(&self.connector);
let max_redirect_times = self.max_redirect_times;
@ -96,7 +98,7 @@ where
_ => None,
};
let fut = connector.call(ConnectRequest::Client(head, body, addr));
let fut = connector.call(ConnectRequest::Client(head, body, addr, sni_host));
RedirectServiceFuture::Client {
fut,
@ -221,7 +223,8 @@ where
let fut = connector
.as_ref()
.unwrap()
.call(ConnectRequest::Client(head, body_new, addr));
// @TODO find a way to get sni host
.call(ConnectRequest::Client(head, body_new, addr, None));
self.set(RedirectServiceFuture::Client {
fut,

View File

@ -14,7 +14,7 @@ use serde::Serialize;
#[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar};
use crate::{
client::ClientConfig,
client::{ClientConfig, ServerName},
error::{FreezeRequestError, InvalidUrl},
frozen::FrozenClientRequest,
sender::{PrepForSendingError, RequestSender, SendClientRequest},
@ -48,6 +48,7 @@ pub struct ClientRequest {
response_decompress: bool,
timeout: Option<Duration>,
config: ClientConfig,
sni_host: Option<ServerName>,
#[cfg(feature = "cookies")]
cookies: Option<CookieJar>,
@ -69,6 +70,7 @@ impl ClientRequest {
cookies: None,
timeout: None,
response_decompress: true,
sni_host: None,
}
.method(method)
.uri(uri)
@ -306,6 +308,12 @@ impl ClientRequest {
Ok(self)
}
/// Set SNI (Server Name Indication) host for this request.
pub fn sni_host(mut self, host: impl Into<String>) -> Self {
self.sni_host = Some(ServerName::Owned(host.into()));
self
}
/// Freeze request builder and construct `FrozenClientRequest`,
/// which could be used for sending same request multiple times.
pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> {
@ -320,6 +328,10 @@ impl ClientRequest {
response_decompress: slf.response_decompress,
timeout: slf.timeout,
config: slf.config,
sni_host: slf.sni_host.map(|v| match v {
ServerName::Borrowed(r) => ServerName::Borrowed(r),
ServerName::Owned(o) => ServerName::Borrowed(Rc::new(o)),
}),
};
Ok(request)
@ -340,6 +352,7 @@ impl ClientRequest {
slf.response_decompress,
slf.timeout,
&slf.config,
slf.sni_host,
body,
)
}
@ -356,6 +369,7 @@ impl ClientRequest {
slf.response_decompress,
slf.timeout,
&slf.config,
slf.sni_host,
value,
)
}
@ -374,6 +388,7 @@ impl ClientRequest {
slf.response_decompress,
slf.timeout,
&slf.config,
slf.sni_host,
value,
)
}
@ -394,6 +409,7 @@ impl ClientRequest {
slf.response_decompress,
slf.timeout,
&slf.config,
slf.sni_host,
stream,
)
}
@ -410,6 +426,7 @@ impl ClientRequest {
slf.response_decompress,
slf.timeout,
&slf.config,
slf.sni_host,
)
}

View File

@ -23,7 +23,7 @@ use serde::Serialize;
use crate::{
any_body::AnyBody,
client::ClientConfig,
client::{ClientConfig, ServerName},
error::{FreezeRequestError, InvalidUrl, SendRequestError},
BoxError, ClientResponse, ConnectRequest, ConnectResponse,
};
@ -186,6 +186,7 @@ impl RequestSender {
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
sni_host: Option<ServerName>,
body: impl MessageBody + 'static,
) -> SendClientRequest {
let req = match self {
@ -193,11 +194,13 @@ impl RequestSender {
RequestHeadType::Owned(head),
AnyBody::from_message_body(body).into_boxed(),
addr,
sni_host,
),
RequestSender::Rc(head, extra_headers) => ConnectRequest::Client(
RequestHeadType::Rc(head, extra_headers),
AnyBody::from_message_body(body).into_boxed(),
addr,
sni_host,
),
};
@ -212,6 +215,7 @@ impl RequestSender {
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
sni_host: Option<ServerName>,
value: impl Serialize,
) -> SendClientRequest {
let body = match serde_json::to_string(&value) {
@ -223,7 +227,7 @@ impl RequestSender {
return err.into();
}
self.send_body(addr, response_decompress, timeout, config, body)
self.send_body(addr, response_decompress, timeout, config, sni_host, body)
}
pub(crate) fn send_form(
@ -232,6 +236,7 @@ impl RequestSender {
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
sni_host: Option<ServerName>,
value: impl Serialize,
) -> SendClientRequest {
let body = match serde_urlencoded::to_string(value) {
@ -246,7 +251,7 @@ impl RequestSender {
return err.into();
}
self.send_body(addr, response_decompress, timeout, config, body)
self.send_body(addr, response_decompress, timeout, config, sni_host, body)
}
pub(crate) fn send_stream<S, E>(
@ -255,6 +260,7 @@ impl RequestSender {
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
sni_host: Option<ServerName>,
stream: S,
) -> SendClientRequest
where
@ -266,6 +272,7 @@ impl RequestSender {
response_decompress,
timeout,
config,
sni_host,
BodyStream::new(stream),
)
}
@ -276,8 +283,9 @@ impl RequestSender {
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
sni_host: Option<ServerName>,
) -> SendClientRequest {
self.send_body(addr, response_decompress, timeout, config, ())
self.send_body(addr, response_decompress, timeout, config, sni_host, ())
}
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>

View File

@ -38,7 +38,7 @@ use base64::prelude::*;
#[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar};
use crate::{
client::ClientConfig,
client::{ClientConfig, ServerName},
connect::{BoxedSocket, ConnectRequest},
error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
http::{
@ -58,6 +58,7 @@ pub struct WebsocketsRequest {
max_size: usize,
server_mode: bool,
config: ClientConfig,
sni_host: Option<ServerName>,
#[cfg(feature = "cookies")]
cookies: Option<CookieJar>,
@ -96,6 +97,7 @@ impl WebsocketsRequest {
server_mode: false,
#[cfg(feature = "cookies")]
cookies: None,
sni_host: None,
}
}
@ -249,6 +251,12 @@ impl WebsocketsRequest {
self.header(AUTHORIZATION, format!("Bearer {}", token))
}
/// Set SNI (Server Name Indication) host for this request.
pub fn sni_host(mut self, host: impl Into<String>) -> Self {
self.sni_host = Some(ServerName::Owned(host.into()));
self
}
/// Complete request construction and connect to a WebSocket server.
pub async fn connect(
mut self,
@ -338,7 +346,7 @@ impl WebsocketsRequest {
let max_size = self.max_size;
let server_mode = self.server_mode;
let req = ConnectRequest::Tunnel(head, self.addr);
let req = ConnectRequest::Tunnel(head, self.addr, self.sni_host);
let fut = self.config.connector.call(req);

View File

@ -43,6 +43,8 @@ fn tls_config() -> ServerConfig {
}
mod danger {
use std::collections::HashSet;
use rustls::{
client::danger::{ServerCertVerified, ServerCertVerifier},
pki_types::UnixTime,
@ -50,8 +52,10 @@ mod danger {
use super::*;
#[derive(Debug)]
pub struct NoCertificateVerification;
#[derive(Debug, Default)]
pub struct NoCertificateVerification {
pub trusted_hosts: HashSet<String>,
}
impl ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
@ -62,7 +66,15 @@ mod danger {
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
if self.trusted_hosts.is_empty() {
return Ok(ServerCertVerified::assertion());
}
if self.trusted_hosts.contains(_server_name.to_str().as_ref()) {
return Ok(ServerCertVerified::assertion());
}
Err(rustls::Error::General("untrusted host".into()))
}
fn verify_tls12_signature(
@ -124,7 +136,7 @@ async fn test_connection_reuse_h2() {
// disable TLS verification
config
.dangerous()
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification));
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification::default()));
let client = awc::Client::builder()
.connector(awc::Connector::new().rustls_0_23(Arc::new(config)))
@ -144,3 +156,84 @@ async fn test_connection_reuse_h2() {
// one connection
assert_eq!(num.load(Ordering::Relaxed), 1);
}
#[actix_rt::test]
async fn test_connection_with_sni() {
let srv = test_server(move || {
HttpService::build()
.h2(map_config(
App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))),
|_| AppConfig::default(),
))
.rustls_0_23(tls_config())
.map_err(|_| ())
})
.await;
let mut config = ClientConfig::builder()
.with_root_certificates(webpki_roots_cert_store())
.with_no_client_auth();
let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
config.alpn_protocols = protos;
// disable TLS verification
config
.dangerous()
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {
trusted_hosts: ["localhost".to_owned()].iter().cloned().collect(),
}));
let client = awc::Client::builder()
.connector(awc::Connector::new().rustls_0_23(Arc::new(config)))
.finish();
// req : standard request
let request = client.get(srv.surl("/")).send();
let response = request.await.unwrap();
assert!(response.status().is_success());
// req : test specific host with address, return trusted host
let request = client.get(srv.surl("/")).sni_host("localhost").send();
let response = request.await.unwrap();
assert!(response.status().is_success());
// req : test bad host, return untrusted host
let request = client.get(srv.surl("/")).sni_host("bad.host").send();
let response = request.await;
assert!(response.is_err());
assert_eq!(
response.unwrap_err().to_string(),
"Failed to connect to host: unexpected error: untrusted host"
);
// req : test specific host with address, return untrusted host
let addr = srv.addr();
let request = client.get("https://example.com:443/").address(addr).send();
let response = request.await;
assert!(response.is_err());
assert_eq!(
response.unwrap_err().to_string(),
"Failed to connect to host: unexpected error: untrusted host"
);
// req : test specify sni_host with address and other host (authority)
let request = client
.get("https://example.com:443/")
.address(addr)
.sni_host("localhost")
.send();
let response = request.await.unwrap();
assert!(response.status().is_success());
// req : test ip address with sni host
let request = client
.get("https://127.0.0.1:443/")
.address(addr)
.sni_host("localhost")
.send();
let response = request.await.unwrap();
assert!(response.status().is_success());
}

View File

@ -13,6 +13,8 @@ fmt:
[private]
downgrade-for-msrv:
cargo {{ toolchain }} update -p=divan --precise=0.1.15 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon --precise=1.10.0 # next ver: 1.80.0
cargo {{ toolchain }} update -p=rayon-core --precise=1.12.1 # next ver: 1.80.0
cargo {{ toolchain }} update -p=half --precise=2.4.1 # next ver: 1.81.0
cargo {{ toolchain }} update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0
cargo {{ toolchain }} update -p=litemap --precise=0.7.4 # next ver: 1.81.0
@ -50,8 +52,7 @@ clippy:
cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }}
# Run Clippy over workspace using MSRV.
clippy-msrv:
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
clippy-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} clippy
# Test workspace code.
@ -62,8 +63,7 @@ test:
cargo {{ toolchain }} nextest run --no-tests=warn --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)"
# Test workspace using MSRV.
test-msrv:
@just toolchain={{ msrv_rustup }} downgrade-for-msrv
test-msrv: downgrade-for-msrv
@just toolchain={{ msrv_rustup }} test
# Test workspace docs.

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# developed on macOS and probably doesn't work on Linux yet due to minor
# differences in flags on sed

View File

@ -1,38 +0,0 @@
#!/bin/sh
# run tests matching what CI does for non-linux feature sets
set -x
EXIT=0
save_exit_code() {
eval $@
local CMD_EXIT=$?
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
}
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
save_exit_code cargo test --workspace --doc
if [ "$EXIT" = "0" ]; then
PASSED="All tests passed!"
if [ "$(command -v figlet)" ]; then
figlet "$PASSED"
else
echo "$PASSED"
fi
fi
exit $EXIT

25
scripts/publish Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -Euo pipefail
for dir in $@; do
cd "$dir"
cargo publish --dry-run
read -p "Look okay? "
read -p "Sure? "
cargo publish
if [ $? -ne 0 ]; then
echo
read -p "Was the above error caused by cyclic dev-deps? Choosing yes will publish without a git backreference. (y/N) " publish_no_dev_deps
if [[ "$publish_no_dev_deps" == "y" || "$publish_no_dev_deps" == "Y" ]]; then
cargo hack --no-dev-deps publish --allow-dirty
fi
fi
cd ..
done