mirror of https://github.com/fafhrd91/actix-web
Compare commits
28 Commits
ac1cbb4910
...
f44cf088f6
Author | SHA1 | Date |
---|---|---|
|
f44cf088f6 | |
|
98d7d0b46b | |
|
4966a54e05 | |
|
00b0f8f700 | |
|
3c2907da41 | |
|
5041cd1c65 | |
|
d3c46537b3 | |
|
8996198f2c | |
|
68624ec63b | |
|
bcd0ffb016 | |
|
9fb6c13a1a | |
|
05cfef7f4b | |
|
8f3eb32a32 | |
|
ddd16ec9db | |
|
9c47a247fb | |
|
2536823e3b | |
|
e3f81d0643 | |
|
a84aee836b | |
|
1f08cb24c3 | |
|
1b49047086 | |
|
9fe033a963 | |
|
2ba69a1904 | |
|
c6352005f7 | |
|
aeb42486af | |
|
30f7df5853 | |
|
3dfeda391a | |
|
610dd616ef | |
|
0915879267 |
|
@ -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 },
|
||||
]
|
||||
|
|
|
@ -9,4 +9,5 @@ words:
|
|||
- rustls
|
||||
- rustup
|
||||
- serde
|
||||
- uring
|
||||
- zstd
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.6)
|
||||
[](https://docs.rs/actix-files/0.6.7)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.6)
|
||||
[](https://deps.rs/crate/actix-files/0.6.7)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.11.0)
|
||||
[](https://docs.rs/actix-http/3.11.1)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.11.0)
|
||||
[](https://deps.rs/crate/actix-http/3.11.1)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
@ -1182,7 +1182,7 @@ where
|
|||
let state_is_none = inner_p.state.is_none();
|
||||
|
||||
// read half is closed; we do not process any responses
|
||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) {
|
||||
trace!("read half closed; start shutdown");
|
||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||
}
|
||||
|
@ -1216,6 +1216,9 @@ where
|
|||
inner_p.shutdown_timer,
|
||||
);
|
||||
|
||||
if inner_p.flags.contains(Flags::SHUTDOWN) {
|
||||
cx.waker().wake_by_ref();
|
||||
}
|
||||
Poll::Pending
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- `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
|
||||
|
||||
- Add `Logger::log_level()` method.
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -318,12 +318,33 @@ impl HttpResponseBuilder {
|
|||
/// Set a streaming body and build the `HttpResponse`.
|
||||
///
|
||||
/// `HttpResponseBuilder` can not be used after this call.
|
||||
///
|
||||
/// If `Content-Type` is not set, then it is automatically set to `application/octet-stream`.
|
||||
///
|
||||
/// If `Content-Length` is set, then [`no_chunking()`](Self::no_chunking) is automatically called.
|
||||
#[inline]
|
||||
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
// Set mime type to application/octet-stream if it is not set
|
||||
if let Some(parts) = self.inner() {
|
||||
if !parts.headers.contains_key(header::CONTENT_TYPE) {
|
||||
self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parts) = self.inner() {
|
||||
if let Some(length) = parts.headers.get(header::CONTENT_LENGTH) {
|
||||
if let Ok(length) = length.to_str() {
|
||||
if let Ok(length) = length.parse::<u64>() {
|
||||
self.no_chunking(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.body(BodyStream::new(stream))
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
http::header::{self, HeaderValue},
|
||||
HttpResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
|
||||
struct FixedSizeStream {
|
||||
data: Vec<u8>,
|
||||
yielded: bool,
|
||||
}
|
||||
|
||||
impl FixedSizeStream {
|
||||
fn new(size: usize) -> Self {
|
||||
Self {
|
||||
data: vec![0u8; size],
|
||||
yielded: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for FixedSizeStream {
|
||||
type Item = Result<Bytes, std::io::Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.yielded {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
self.yielded = true;
|
||||
let data = std::mem::take(&mut self.data);
|
||||
Poll::Ready(Some(Ok(Bytes::from(data))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_streaming_response_with_content_length() {
|
||||
let stream = FixedSizeStream::new(100);
|
||||
|
||||
let resp = HttpResponse::Ok()
|
||||
.append_header((header::CONTENT_LENGTH, "100"))
|
||||
.streaming(stream);
|
||||
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_LENGTH),
|
||||
Some(&HeaderValue::from_static("100")),
|
||||
"Content-Length should be preserved when explicitly set"
|
||||
);
|
||||
|
||||
let has_chunked = resp
|
||||
.headers()
|
||||
.get(header::TRANSFER_ENCODING)
|
||||
.map(|v| v.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.contains("chunked");
|
||||
|
||||
assert!(
|
||||
!has_chunked,
|
||||
"chunked should not be used when Content-Length is provided"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("application/octet-stream")),
|
||||
"Content-Type should default to application/octet-stream"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_streaming_response_default_content_type() {
|
||||
let stream = FixedSizeStream::new(50);
|
||||
|
||||
let resp = HttpResponse::Ok().streaming(stream);
|
||||
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("application/octet-stream")),
|
||||
"Content-Type should default to application/octet-stream"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_streaming_response_user_defined_content_type() {
|
||||
let stream = FixedSizeStream::new(25);
|
||||
|
||||
let resp = HttpResponse::Ok()
|
||||
.insert_header((header::CONTENT_TYPE, "text/plain"))
|
||||
.streaming(stream);
|
||||
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
"User-defined Content-Type should be preserved"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_streaming_response_empty_stream() {
|
||||
let stream = FixedSizeStream::new(0);
|
||||
|
||||
let resp = HttpResponse::Ok()
|
||||
.append_header((header::CONTENT_LENGTH, "0"))
|
||||
.streaming(stream);
|
||||
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_LENGTH),
|
||||
Some(&HeaderValue::from_static("0")),
|
||||
"Content-Length 0 should be preserved for empty streams"
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -3,29 +3,33 @@ use std::{net::IpAddr, time::Duration};
|
|||
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
|
||||
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
|
||||
|
||||
/// Connector configuration
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ConnectorConfig {
|
||||
/// Connect configuration
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
pub struct ConnectConfig {
|
||||
pub(crate) timeout: Duration,
|
||||
pub(crate) handshake_timeout: Duration,
|
||||
pub(crate) conn_lifetime: Duration,
|
||||
pub(crate) conn_keep_alive: Duration,
|
||||
pub(crate) disconnect_timeout: Option<Duration>,
|
||||
pub(crate) limit: usize,
|
||||
pub(crate) conn_window_size: u32,
|
||||
pub(crate) stream_window_size: u32,
|
||||
pub(crate) local_address: Option<IpAddr>,
|
||||
}
|
||||
|
||||
impl Default for ConnectorConfig {
|
||||
/// Connector configuration
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectorConfig {
|
||||
pub(crate) default_connect_config: ConnectConfig,
|
||||
pub(crate) disconnect_timeout: Option<Duration>,
|
||||
pub(crate) limit: usize,
|
||||
}
|
||||
|
||||
impl Default for ConnectConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(5),
|
||||
handshake_timeout: Duration::from_secs(5),
|
||||
conn_lifetime: Duration::from_secs(75),
|
||||
conn_keep_alive: Duration::from_secs(15),
|
||||
disconnect_timeout: Some(Duration::from_millis(3000)),
|
||||
limit: 100,
|
||||
conn_window_size: DEFAULT_H2_CONN_WINDOW,
|
||||
stream_window_size: DEFAULT_H2_STREAM_WINDOW,
|
||||
local_address: None,
|
||||
|
@ -33,10 +37,88 @@ impl Default for ConnectorConfig {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for ConnectorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_connect_config: ConnectConfig::default(),
|
||||
disconnect_timeout: Some(Duration::from_millis(3000)),
|
||||
limit: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorConfig {
|
||||
pub(crate) fn no_disconnect_timeout(&self) -> Self {
|
||||
pub fn no_disconnect_timeout(&self) -> Self {
|
||||
let mut res = self.clone();
|
||||
res.disconnect_timeout = None;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectConfig {
|
||||
/// Sets TCP connection timeout.
|
||||
///
|
||||
/// This is the max time allowed to connect to remote host, including DNS name resolution.
|
||||
///
|
||||
/// By default, the timeout is 5 seconds.
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets TLS handshake timeout.
|
||||
///
|
||||
/// This is the max time allowed to perform the TLS handshake with remote host after TCP
|
||||
/// connection is established.
|
||||
///
|
||||
/// By default, the timeout is 5 seconds.
|
||||
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.handshake_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the initial window size (in bytes) for HTTP/2 stream-level flow control for received
|
||||
/// data.
|
||||
///
|
||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
||||
pub fn initial_window_size(mut self, size: u32) -> Self {
|
||||
self.stream_window_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the initial window size (in bytes) for HTTP/2 connection-level flow control for
|
||||
/// received data.
|
||||
///
|
||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
||||
pub fn initial_connection_window_size(mut self, size: u32) -> Self {
|
||||
self.conn_window_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set keep-alive period for opened connection.
|
||||
///
|
||||
/// Keep-alive period is the period between connection usage. If
|
||||
/// the delay between repeated usages of the same connection
|
||||
/// exceeds this period, the connection is closed.
|
||||
/// Default keep-alive period is 15 seconds.
|
||||
pub fn conn_keep_alive(mut self, dur: Duration) -> Self {
|
||||
self.conn_keep_alive = dur;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set max lifetime period for connection.
|
||||
///
|
||||
/// Connection lifetime is max lifetime of any opened connection
|
||||
/// until it is closed regardless of keep-alive period.
|
||||
/// Default lifetime period is 75 seconds.
|
||||
pub fn conn_lifetime(mut self, dur: Duration) -> Self {
|
||||
self.conn_lifetime = dur;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set local IP Address the connector would use for establishing connection.
|
||||
pub fn local_address(mut self, addr: IpAddr) -> Self {
|
||||
self.local_address = Some(addr);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -245,7 +282,7 @@ where
|
|||
///
|
||||
/// By default, the timeout is 5 seconds.
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.config.timeout = timeout;
|
||||
self.config.default_connect_config.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -256,7 +293,7 @@ where
|
|||
///
|
||||
/// By default, the timeout is 5 seconds.
|
||||
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.config.handshake_timeout = timeout;
|
||||
self.config.default_connect_config.handshake_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -350,7 +387,7 @@ where
|
|||
///
|
||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
||||
pub fn initial_window_size(mut self, size: u32) -> Self {
|
||||
self.config.stream_window_size = size;
|
||||
self.config.default_connect_config.stream_window_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -359,7 +396,7 @@ where
|
|||
///
|
||||
/// The default value is 65,535 and is good for APIs, but not for big objects.
|
||||
pub fn initial_connection_window_size(mut self, size: u32) -> Self {
|
||||
self.config.conn_window_size = size;
|
||||
self.config.default_connect_config.conn_window_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -385,7 +422,7 @@ where
|
|||
/// exceeds this period, the connection is closed.
|
||||
/// Default keep-alive period is 15 seconds.
|
||||
pub fn conn_keep_alive(mut self, dur: Duration) -> Self {
|
||||
self.config.conn_keep_alive = dur;
|
||||
self.config.default_connect_config.conn_keep_alive = dur;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -395,7 +432,7 @@ where
|
|||
/// until it is closed regardless of keep-alive period.
|
||||
/// Default lifetime period is 75 seconds.
|
||||
pub fn conn_lifetime(mut self, dur: Duration) -> Self {
|
||||
self.config.conn_lifetime = dur;
|
||||
self.config.default_connect_config.conn_lifetime = dur;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -414,7 +451,7 @@ where
|
|||
|
||||
/// Set local IP Address the connector would use for establishing connection.
|
||||
pub fn local_address(mut self, addr: IpAddr) -> Self {
|
||||
self.config.local_address = Some(addr);
|
||||
self.config.default_connect_config.local_address = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -422,8 +459,8 @@ where
|
|||
///
|
||||
/// The `Connector` builder always concludes by calling `finish()` last in its combinator chain.
|
||||
pub fn finish(self) -> ConnectorService<S, IO> {
|
||||
let local_address = self.config.local_address;
|
||||
let timeout = self.config.timeout;
|
||||
let local_address = self.config.default_connect_config.local_address;
|
||||
let timeout = self.config.default_connect_config.timeout;
|
||||
|
||||
let tcp_service_inner =
|
||||
TcpConnectorInnerService::new(self.connector, timeout, local_address);
|
||||
|
@ -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)
|
||||
|
@ -486,7 +523,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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
|
||||
|
@ -521,7 +558,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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
|
||||
|
@ -561,7 +598,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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
|
||||
|
@ -596,7 +633,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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
|
||||
|
@ -634,7 +671,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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
|
||||
|
@ -669,7 +706,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
let handshake_timeout = self.config.default_connect_config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -789,9 +829,13 @@ where
|
|||
}
|
||||
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
let timeout = req
|
||||
.config
|
||||
.clone()
|
||||
.map(|c| c.handshake_timeout)
|
||||
.unwrap_or(self.timeout);
|
||||
let fut = self.tcp_service.call(req);
|
||||
let tls_service = self.tls_service.clone();
|
||||
let timeout = self.timeout;
|
||||
|
||||
TlsConnectorFuture::TcpConnect {
|
||||
fut,
|
||||
|
@ -827,9 +871,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 +892,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 +930,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 +944,14 @@ 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 timeout = req.config.map(|c| c.timeout).unwrap_or(self.timeout);
|
||||
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);
|
||||
|
@ -899,7 +959,7 @@ where
|
|||
|
||||
TcpConnectorInnerFuture {
|
||||
fut: self.service.call(req),
|
||||
timeout: sleep(self.timeout),
|
||||
timeout: sleep(timeout),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -916,9 +976,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 +1038,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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ use http::{
|
|||
use log::trace;
|
||||
|
||||
use super::{
|
||||
config::ConnectorConfig,
|
||||
connection::{ConnectionIo, H2Connection},
|
||||
error::SendRequestError,
|
||||
};
|
||||
|
@ -186,12 +185,13 @@ where
|
|||
|
||||
pub(crate) fn handshake<Io: ConnectionIo>(
|
||||
io: Io,
|
||||
config: &ConnectorConfig,
|
||||
stream_window_size: u32,
|
||||
conn_window_size: u32,
|
||||
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> {
|
||||
let mut builder = Builder::new();
|
||||
builder
|
||||
.initial_window_size(config.stream_window_size)
|
||||
.initial_connection_window_size(config.conn_window_size)
|
||||
.initial_window_size(stream_window_size)
|
||||
.initial_connection_window_size(conn_window_size)
|
||||
.enable_push(false);
|
||||
builder.handshake(io)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -20,15 +20,37 @@ mod h2proto;
|
|||
mod pool;
|
||||
|
||||
pub use self::{
|
||||
config::ConnectConfig,
|
||||
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>,
|
||||
pub config: Option<Rc<ConnectConfig>>,
|
||||
}
|
||||
|
||||
/// An asynchronous HTTP and WebSocket client.
|
||||
|
@ -79,8 +101,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,
|
||||
> {
|
||||
|
|
|
@ -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,11 +185,15 @@ 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() {
|
||||
let config = &inner.config;
|
||||
let config = req
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.as_ref())
|
||||
.unwrap_or(&inner.config.default_connect_config);
|
||||
let idle_dur = now - c.used;
|
||||
let age = now - c.created;
|
||||
let conn_ineligible =
|
||||
|
@ -229,9 +228,24 @@ where
|
|||
conn
|
||||
};
|
||||
|
||||
let stream_window_size = req
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.stream_window_size)
|
||||
.unwrap_or(inner.config.default_connect_config.stream_window_size);
|
||||
let conn_window_size = req
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.conn_window_size)
|
||||
.unwrap_or(inner.config.default_connect_config.conn_window_size);
|
||||
|
||||
// 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 {
|
||||
|
@ -245,8 +259,8 @@ where
|
|||
if proto == Protocol::Http1 {
|
||||
Ok(ConnectionType::from_h1(io, Instant::now(), acquired))
|
||||
} else {
|
||||
let config = &acquired.inner.config;
|
||||
let (sender, connection) = handshake(io, config).await?;
|
||||
let (sender, connection) =
|
||||
handshake(io, stream_window_size, conn_window_size).await?;
|
||||
let inner = H2ConnectionInner::new(sender, connection);
|
||||
Ok(ConnectionType::from_h2(inner, Instant::now(), acquired))
|
||||
}
|
||||
|
@ -343,8 +357,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 +373,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,9 +394,8 @@ impl<Io: ConnectionIo> Acquired<Io> {
|
|||
mod test {
|
||||
use std::cell::Cell;
|
||||
|
||||
use http::Uri;
|
||||
|
||||
use super::*;
|
||||
use crate::client::ConnectConfig;
|
||||
|
||||
/// A stream type that always returns pending on async read.
|
||||
///
|
||||
|
@ -466,8 +479,12 @@ 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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -499,15 +516,22 @@ mod test {
|
|||
let connector = TestPoolConnector { generated };
|
||||
|
||||
let config = ConnectorConfig {
|
||||
conn_keep_alive: Duration::from_secs(1),
|
||||
default_connect_config: ConnectConfig {
|
||||
conn_keep_alive: Duration::from_secs(1),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -541,15 +565,22 @@ mod test {
|
|||
let connector = TestPoolConnector { generated };
|
||||
|
||||
let config = ConnectorConfig {
|
||||
conn_lifetime: Duration::from_secs(1),
|
||||
default_connect_config: ConnectConfig {
|
||||
conn_lifetime: Duration::from_secs(1),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -587,8 +618,12 @@ 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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -600,8 +635,12 @@ 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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -624,8 +663,12 @@ 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,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
|
@ -633,8 +676,12 @@ 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,
|
||||
config: None,
|
||||
};
|
||||
let conn = pool.call(req.clone()).await.unwrap();
|
||||
assert_eq!(2, generated_clone.get());
|
||||
|
|
|
@ -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, ConnectConfig, ConnectError, Connection, ConnectionIo,
|
||||
SendRequestError, ServerName,
|
||||
},
|
||||
ClientResponse,
|
||||
};
|
||||
|
||||
|
@ -32,13 +35,24 @@ 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>,
|
||||
Option<Rc<ConnectConfig>>,
|
||||
),
|
||||
|
||||
/// 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>,
|
||||
Option<Rc<ConnectConfig>>,
|
||||
),
|
||||
}
|
||||
|
||||
/// Combined HTTP response & WebSocket tunnel type returned from connection service.
|
||||
|
@ -103,17 +117,44 @@ 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, config) = match req {
|
||||
ConnectRequest::Client(ref head, .., addr, ref sni_host, ref config) => {
|
||||
(head.as_ref(), addr, sni_host.clone(), config.clone())
|
||||
}
|
||||
ConnectRequest::Tunnel(ref head, addr, ref sni_host, ref config) => {
|
||||
(head, addr, sni_host.clone(), config.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,
|
||||
config,
|
||||
});
|
||||
|
||||
ConnectRequestFuture::Connection {
|
||||
fut,
|
||||
req: Some(req),
|
||||
|
@ -127,6 +168,9 @@ pin_project_lite::pin_project! {
|
|||
where
|
||||
Io: ConnectionIo
|
||||
{
|
||||
Error {
|
||||
err: ConnectError
|
||||
},
|
||||
Connection {
|
||||
#[pin]
|
||||
fut: Fut,
|
||||
|
@ -192,6 +236,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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use futures_core::Stream;
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
client::ClientConfig,
|
||||
client::{ClientConfig, ConnectConfig, ServerName},
|
||||
sender::{RequestSender, SendClientRequest},
|
||||
BoxError,
|
||||
};
|
||||
|
@ -26,6 +26,8 @@ pub struct FrozenClientRequest {
|
|||
pub(crate) response_decompress: bool,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
pub(crate) config: ClientConfig,
|
||||
pub(crate) sni_host: Option<ServerName>,
|
||||
pub(crate) connect_config: Option<Rc<ConnectConfig>>,
|
||||
}
|
||||
|
||||
impl FrozenClientRequest {
|
||||
|
@ -54,6 +56,8 @@ impl FrozenClientRequest {
|
|||
self.response_decompress,
|
||||
self.timeout,
|
||||
&self.config,
|
||||
self.sni_host.clone(),
|
||||
self.connect_config.clone(),
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
@ -65,6 +69,8 @@ impl FrozenClientRequest {
|
|||
self.response_decompress,
|
||||
self.timeout,
|
||||
&self.config,
|
||||
self.sni_host.clone(),
|
||||
self.connect_config.clone(),
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -76,6 +82,8 @@ impl FrozenClientRequest {
|
|||
self.response_decompress,
|
||||
self.timeout,
|
||||
&self.config,
|
||||
self.sni_host.clone(),
|
||||
self.connect_config.clone(),
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -91,6 +99,8 @@ impl FrozenClientRequest {
|
|||
self.response_decompress,
|
||||
self.timeout,
|
||||
&self.config,
|
||||
self.sni_host.clone(),
|
||||
self.connect_config.clone(),
|
||||
stream,
|
||||
)
|
||||
}
|
||||
|
@ -102,6 +112,8 @@ impl FrozenClientRequest {
|
|||
self.response_decompress,
|
||||
self.timeout,
|
||||
&self.config,
|
||||
self.sni_host.clone(),
|
||||
self.connect_config.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -156,6 +168,8 @@ impl FrozenSendBuilder {
|
|||
self.req.response_decompress,
|
||||
self.req.timeout,
|
||||
&self.req.config,
|
||||
self.req.sni_host.clone(),
|
||||
self.req.connect_config,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
@ -171,6 +185,8 @@ impl FrozenSendBuilder {
|
|||
self.req.response_decompress,
|
||||
self.req.timeout,
|
||||
&self.req.config,
|
||||
self.req.sni_host.clone(),
|
||||
self.req.connect_config,
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -186,6 +202,8 @@ impl FrozenSendBuilder {
|
|||
self.req.response_decompress,
|
||||
self.req.timeout,
|
||||
&self.req.config,
|
||||
self.req.sni_host.clone(),
|
||||
self.req.connect_config,
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -205,6 +223,8 @@ impl FrozenSendBuilder {
|
|||
self.req.response_decompress,
|
||||
self.req.timeout,
|
||||
&self.req.config,
|
||||
self.req.sni_host.clone(),
|
||||
self.req.connect_config,
|
||||
stream,
|
||||
)
|
||||
}
|
||||
|
@ -220,6 +240,8 @@ impl FrozenSendBuilder {
|
|||
self.req.response_decompress,
|
||||
self.req.timeout,
|
||||
&self.req.config,
|
||||
self.req.sni_host.clone(),
|
||||
self.req.connect_config,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, config) => {
|
||||
let fut = self
|
||||
.connector
|
||||
.call(ConnectRequest::Tunnel(head, addr, sni_host, config));
|
||||
RedirectServiceFuture::Tunnel { fut }
|
||||
}
|
||||
ConnectRequest::Client(head, body, addr) => {
|
||||
ConnectRequest::Client(head, body, addr, sni_host, config) => {
|
||||
let connector = Rc::clone(&self.connector);
|
||||
let max_redirect_times = self.max_redirect_times;
|
||||
|
||||
|
@ -96,7 +98,8 @@ where
|
|||
_ => None,
|
||||
};
|
||||
|
||||
let fut = connector.call(ConnectRequest::Client(head, body, addr));
|
||||
let fut =
|
||||
connector.call(ConnectRequest::Client(head, body, addr, sni_host, config));
|
||||
|
||||
RedirectServiceFuture::Client {
|
||||
fut,
|
||||
|
@ -221,7 +224,8 @@ where
|
|||
let fut = connector
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.call(ConnectRequest::Client(head, body_new, addr));
|
||||
// @TODO find a way to get sni host and config
|
||||
.call(ConnectRequest::Client(head, body_new, addr, None, None));
|
||||
|
||||
self.set(RedirectServiceFuture::Client {
|
||||
fut,
|
||||
|
|
|
@ -14,7 +14,7 @@ use serde::Serialize;
|
|||
#[cfg(feature = "cookies")]
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
use crate::{
|
||||
client::ClientConfig,
|
||||
client::{ClientConfig, ConnectConfig, ServerName},
|
||||
error::{FreezeRequestError, InvalidUrl},
|
||||
frozen::FrozenClientRequest,
|
||||
sender::{PrepForSendingError, RequestSender, SendClientRequest},
|
||||
|
@ -48,6 +48,8 @@ pub struct ClientRequest {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connect_config: Option<ConnectConfig>,
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: Option<CookieJar>,
|
||||
|
@ -69,6 +71,8 @@ impl ClientRequest {
|
|||
cookies: None,
|
||||
timeout: None,
|
||||
response_decompress: true,
|
||||
sni_host: None,
|
||||
connect_config: None,
|
||||
}
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
|
@ -279,6 +283,15 @@ impl ClientRequest {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set specific connector configuration for this request.
|
||||
///
|
||||
/// Not all config may be applied to the request, it depends on the connector and also
|
||||
/// if there is already a connection established.
|
||||
pub fn connect_config(mut self, config: ConnectConfig) -> Self {
|
||||
self.connect_config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request timeout. Overrides client wide timeout setting.
|
||||
///
|
||||
/// Request timeout is the total time before a response must be received.
|
||||
|
@ -306,6 +319,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 +339,11 @@ 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)),
|
||||
}),
|
||||
connect_config: slf.connect_config.map(Rc::new),
|
||||
};
|
||||
|
||||
Ok(request)
|
||||
|
@ -340,6 +364,8 @@ impl ClientRequest {
|
|||
slf.response_decompress,
|
||||
slf.timeout,
|
||||
&slf.config,
|
||||
slf.sni_host,
|
||||
slf.connect_config.map(Rc::new),
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
@ -356,6 +382,8 @@ impl ClientRequest {
|
|||
slf.response_decompress,
|
||||
slf.timeout,
|
||||
&slf.config,
|
||||
slf.sni_host,
|
||||
slf.connect_config.map(Rc::new),
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -374,6 +402,8 @@ impl ClientRequest {
|
|||
slf.response_decompress,
|
||||
slf.timeout,
|
||||
&slf.config,
|
||||
slf.sni_host,
|
||||
slf.connect_config.map(Rc::new),
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
@ -394,6 +424,8 @@ impl ClientRequest {
|
|||
slf.response_decompress,
|
||||
slf.timeout,
|
||||
&slf.config,
|
||||
slf.sni_host,
|
||||
slf.connect_config.map(Rc::new),
|
||||
stream,
|
||||
)
|
||||
}
|
||||
|
@ -410,6 +442,8 @@ impl ClientRequest {
|
|||
slf.response_decompress,
|
||||
slf.timeout,
|
||||
&slf.config,
|
||||
slf.sni_host,
|
||||
slf.connect_config.map(Rc::new),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ use serde::Serialize;
|
|||
|
||||
use crate::{
|
||||
any_body::AnyBody,
|
||||
client::ClientConfig,
|
||||
client::{ClientConfig, ConnectConfig, ServerName},
|
||||
error::{FreezeRequestError, InvalidUrl, SendRequestError},
|
||||
BoxError, ClientResponse, ConnectRequest, ConnectResponse,
|
||||
};
|
||||
|
@ -186,6 +186,8 @@ impl RequestSender {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connect_config: Option<Rc<ConnectConfig>>,
|
||||
body: impl MessageBody + 'static,
|
||||
) -> SendClientRequest {
|
||||
let req = match self {
|
||||
|
@ -193,11 +195,15 @@ impl RequestSender {
|
|||
RequestHeadType::Owned(head),
|
||||
AnyBody::from_message_body(body).into_boxed(),
|
||||
addr,
|
||||
sni_host,
|
||||
connect_config,
|
||||
),
|
||||
RequestSender::Rc(head, extra_headers) => ConnectRequest::Client(
|
||||
RequestHeadType::Rc(head, extra_headers),
|
||||
AnyBody::from_message_body(body).into_boxed(),
|
||||
addr,
|
||||
sni_host,
|
||||
connect_config,
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -212,6 +218,8 @@ impl RequestSender {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connector_config: Option<Rc<ConnectConfig>>,
|
||||
value: impl Serialize,
|
||||
) -> SendClientRequest {
|
||||
let body = match serde_json::to_string(&value) {
|
||||
|
@ -223,7 +231,15 @@ impl RequestSender {
|
|||
return err.into();
|
||||
}
|
||||
|
||||
self.send_body(addr, response_decompress, timeout, config, body)
|
||||
self.send_body(
|
||||
addr,
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
sni_host,
|
||||
connector_config,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn send_form(
|
||||
|
@ -232,6 +248,8 @@ impl RequestSender {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connector_config: Option<Rc<ConnectConfig>>,
|
||||
value: impl Serialize,
|
||||
) -> SendClientRequest {
|
||||
let body = match serde_urlencoded::to_string(value) {
|
||||
|
@ -246,7 +264,15 @@ impl RequestSender {
|
|||
return err.into();
|
||||
}
|
||||
|
||||
self.send_body(addr, response_decompress, timeout, config, body)
|
||||
self.send_body(
|
||||
addr,
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
sni_host,
|
||||
connector_config,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn send_stream<S, E>(
|
||||
|
@ -255,6 +281,8 @@ impl RequestSender {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connector_config: Option<Rc<ConnectConfig>>,
|
||||
stream: S,
|
||||
) -> SendClientRequest
|
||||
where
|
||||
|
@ -266,6 +294,8 @@ impl RequestSender {
|
|||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
sni_host,
|
||||
connector_config,
|
||||
BodyStream::new(stream),
|
||||
)
|
||||
}
|
||||
|
@ -276,8 +306,18 @@ impl RequestSender {
|
|||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connector_config: Option<Rc<ConnectConfig>>,
|
||||
) -> SendClientRequest {
|
||||
self.send_body(addr, response_decompress, timeout, config, ())
|
||||
self.send_body(
|
||||
addr,
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
sni_host,
|
||||
connector_config,
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
//! }
|
||||
//! ```
|
||||
|
||||
use std::{fmt, net::SocketAddr, str};
|
||||
use std::{fmt, net::SocketAddr, rc::Rc, str};
|
||||
|
||||
use actix_codec::Framed;
|
||||
pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
|
||||
|
@ -38,7 +38,7 @@ use base64::prelude::*;
|
|||
#[cfg(feature = "cookies")]
|
||||
use crate::cookie::{Cookie, CookieJar};
|
||||
use crate::{
|
||||
client::ClientConfig,
|
||||
client::{ClientConfig, ConnectConfig, ServerName},
|
||||
connect::{BoxedSocket, ConnectRequest},
|
||||
error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
|
||||
http::{
|
||||
|
@ -58,6 +58,8 @@ pub struct WebsocketsRequest {
|
|||
max_size: usize,
|
||||
server_mode: bool,
|
||||
config: ClientConfig,
|
||||
sni_host: Option<ServerName>,
|
||||
connect_config: Option<ConnectConfig>,
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: Option<CookieJar>,
|
||||
|
@ -96,6 +98,8 @@ impl WebsocketsRequest {
|
|||
server_mode: false,
|
||||
#[cfg(feature = "cookies")]
|
||||
cookies: None,
|
||||
sni_host: None,
|
||||
connect_config: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,6 +112,15 @@ impl WebsocketsRequest {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set specific connector configuration for this request.
|
||||
///
|
||||
/// Not all config may be applied to the request, it depends on the connector and also
|
||||
/// if there is already a connection established.
|
||||
pub fn connector_config(mut self, config: ConnectConfig) -> Self {
|
||||
self.connect_config = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set supported WebSocket protocols
|
||||
pub fn protocols<U, V>(mut self, protos: U) -> Self
|
||||
where
|
||||
|
@ -249,6 +262,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 +357,12 @@ 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,
|
||||
self.connect_config.map(Rc::new),
|
||||
);
|
||||
|
||||
let fut = self.config.connector.call(req);
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
8
justfile
8
justfile
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue