mirror of
https://github.com/actix/actix-extras.git
synced 2025-04-21 17:46:49 +02:00
Compare commits
74 Commits
protobuf-v
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
c04cc19e73 | ||
|
6a13b3b182 | ||
|
d994912ac2 | ||
|
5f6f20cf37 | ||
|
5145924410 | ||
|
b20dec36ac | ||
|
f6e45d487b | ||
|
c53e198ea7 | ||
|
4d9984ee76 | ||
|
9a08090709 | ||
|
7d3348bb29 | ||
|
c0fa63af39 | ||
|
0b5e2b3647 | ||
|
b95595b9cd | ||
|
4b3f87e915 | ||
|
144c7f92b9 | ||
|
c71b9dd443 | ||
|
282d56e96b | ||
|
d514ad3af5 | ||
|
109e6a4793 | ||
|
bb0c7f21d9 | ||
|
3f7a479a76 | ||
|
fc4b656c3b | ||
|
0f35de7da1 | ||
|
8294fcc645 | ||
|
3de6b03711 | ||
|
64931189c7 | ||
|
265b213123 | ||
|
695369f02f | ||
|
87d9e51112 | ||
|
8c11d37dda | ||
|
d97b36652a | ||
|
98847b9279 | ||
|
cd1b77134e | ||
|
105932706d | ||
|
18f94fa8b5 | ||
|
66b82f0f30 | ||
|
d67abde5f3 | ||
|
3eafe7f5ce | ||
|
3b5f7ae68c | ||
|
036af488fd | ||
|
77406cbb71 | ||
|
2ede588693 | ||
|
21680e0ebe | ||
|
370f9d3033 | ||
|
8f4fb348b3 | ||
|
ff4b173716 | ||
|
49aacfce9f | ||
|
dd20ebb6cb | ||
|
a3211b73d3 | ||
|
a89d3a58bc | ||
|
3c640ec120 | ||
|
26ccf8b200 | ||
|
dd1421f1a0 | ||
|
4eb779be77 | ||
|
48646d1bd3 | ||
|
275675e1c2 | ||
|
50d2fee4e2 | ||
|
0c0d13be12 | ||
|
d10b71fe06 | ||
|
f2339971cd | ||
|
517e72f248 | ||
|
504e89403b | ||
|
31b1dc5aa8 | ||
|
d7daf441d1 | ||
|
2de4b1886c | ||
|
caa5dbc5b3 | ||
|
c259e715f8 | ||
|
d8a86751f0 | ||
|
cac93d2bc7 | ||
|
95f4e0f692 | ||
|
24f3985eab | ||
|
b0d2947a4a | ||
|
0802eff40d |
17
.github/workflows/ci-post-merge.yml
vendored
17
.github/workflows/ci-post-merge.yml
vendored
@ -31,12 +31,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install cargo-hack, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: cargo-hack,cargo-ci-cache-clean
|
||||
|
||||
@ -71,13 +71,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
choco install openssl --version=1.1.1.2100 -y --no-progress
|
||||
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
|
||||
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install cargo-hack and cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: cargo-hack,cargo-ci-cache-clean
|
||||
|
||||
|
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@ -44,19 +44,18 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust (${{ matrix.version.name }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install cargo-hack and cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
- name: Install cargo-hack and cargo-ci-cache-clean, just
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: cargo-hack,cargo-ci-cache-clean
|
||||
tool: cargo-hack,cargo-ci-cache-clean,just
|
||||
|
||||
# - name: workaround MSRV issues
|
||||
# if: matrix.version.name == 'msrv'
|
||||
# run: |
|
||||
# cargo update -p=time:0.3.20 --precise=0.3.16
|
||||
- name: workaround MSRV issues
|
||||
if: matrix.version.name == 'msrv'
|
||||
run: just downgrade-for-msrv
|
||||
|
||||
- name: check minimal
|
||||
run: cargo ci-min
|
||||
@ -92,20 +91,28 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.os == 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
choco install openssl --version=1.1.1.2100 -y --no-progress
|
||||
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
|
||||
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Rust (${{ matrix.version.name }})
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: ${{ matrix.version.version }}
|
||||
|
||||
- name: Install cargo-hack, cargo-ci-cache-clean
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
- name: Install cargo-hack, cargo-ci-cache-clean, just
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: cargo-hack,cargo-ci-cache-clean
|
||||
tool: cargo-hack,cargo-ci-cache-clean,just
|
||||
|
||||
# - name: workaround MSRV issues
|
||||
# if: matrix.version.name == 'msrv'
|
||||
# run: |
|
||||
# cargo update -p=time:0.3.20 --precise=0.3.16
|
||||
- name: workaround MSRV issues
|
||||
if: matrix.version.name == 'msrv'
|
||||
run: just downgrade-for-msrv
|
||||
|
||||
- name: check minimal
|
||||
run: cargo ci-min
|
||||
@ -130,12 +137,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install just
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: just
|
||||
|
||||
|
6
.github/workflows/coverage.yml
vendored
6
.github/workflows/coverage.yml
vendored
@ -25,13 +25,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: llvm-tools-preview
|
||||
|
||||
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||
uses: taiki-e/install-action@v2.41.7
|
||||
uses: taiki-e/install-action@v2.49.42
|
||||
with:
|
||||
tool: just,cargo-llvm-cov,cargo-nextest
|
||||
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
run: just test-coverage-codecov
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4.5.0
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
with:
|
||||
files: codecov.json
|
||||
fail_ci_if_error: true
|
||||
|
33
.github/workflows/lint.yml
vendored
33
.github/workflows/lint.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
@ -46,32 +46,3 @@ jobs:
|
||||
clippy_flags: >-
|
||||
--workspace --all-features --tests --examples --bins --
|
||||
-A unknown_lints -D clippy::todo -D clippy::dbg_macro
|
||||
|
||||
public-api-diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout ${{ github.base_ref }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: checkout ${{ github.head_ref }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust (nightly)
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
|
||||
with:
|
||||
toolchain: nightly
|
||||
|
||||
- name: Install cargo-public-api
|
||||
uses: taiki-e/cache-cargo-install-action@v2.0.1
|
||||
with:
|
||||
tool: cargo-public-api
|
||||
|
||||
- name: generate API diff
|
||||
run: |
|
||||
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
|
||||
|
||||
cargo public-api --manifest-path "$f" --all-features diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} >> /tmp/diff.txt
|
||||
done
|
||||
cat /tmp/diff.txt
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
||||
guide/build/
|
||||
/gh-pages
|
||||
|
3292
Cargo.lock
generated
Normal file
3292
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
README.md
20
README.md
@ -33,8 +33,7 @@ These crates are provided by the community.
|
||||
| Crate | | |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| [actix-web-lab] | [][actix-web-lab] [](https://deps.rs/crate/actix-web-lab) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. |
|
||||
| [actix-multipart-extract] | [][actix-multipart-extract] [](https://deps.rs/crate/actix-multipart-extract) | Better multipart form support for Actix Web. |
|
||||
| [actix-form-data] | [][actix-form-data] [](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams |
|
||||
| [actix-form-data] | [][actix-form-data] [](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams. |
|
||||
| [actix-governor] | [][actix-governor] [](https://deps.rs/crate/actix-governor) | Rate-limiting backed by governor. |
|
||||
| [actix-casbin] | [][actix-casbin] [](https://deps.rs/crate/actix-casbin) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
|
||||
| [actix-ip-filter] | [][actix-ip-filter] [](https://deps.rs/crate/actix-ip-filter) | IP address filter. Supports glob patterns. |
|
||||
@ -45,10 +44,13 @@ These crates are provided by the community.
|
||||
| [awmp] | [][awmp] [](https://deps.rs/crate/awmp) | An easy to use wrapper around multipart fields for Actix Web. |
|
||||
| [tracing-actix-web] | [][tracing-actix-web] [](https://deps.rs/crate/tracing-actix-web) | A middleware to collect telemetry data from applications built on top of the Actix Web framework. |
|
||||
| [actix-hash] | [][actix-hash] [](https://deps.rs/crate/actix-hash) | Hashing utilities for Actix Web. |
|
||||
| [actix-bincode] |  [](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web |
|
||||
| [sentinel-actix] |  [](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web |
|
||||
| [actix-bincode] |  [](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web. |
|
||||
| [sentinel-actix] |  [](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web. |
|
||||
| [actix-telepathy] |  [](https://deps.rs/crate/actix-telepathy) | Build distributed applications with `RemoteActors` and `RemoteMessages`. |
|
||||
| [apistos] |  [](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web |
|
||||
| [apistos] |  [](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web. |
|
||||
| [actix-web-validation] |  [](https://deps.rs/crate/actix-web-validation) | Request validation for Actix Web. |
|
||||
| [actix-jwt-cookies] |  [](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies?path=%2F) | Store your data in encrypted cookies and get it elegantly. |
|
||||
| [actix-ws-broadcaster] |  [](https://deps.rs/repo/github/Necoo33/actix-ws-broadcaster?path=%2F) | A broadcaster library for actix-ws that includes grouping and conditional broadcasting. |
|
||||
|
||||
To add a crate to this list, submit a pull request.
|
||||
|
||||
@ -80,5 +82,9 @@ To add a crate to this list, submit a pull request.
|
||||
[actix-hash]: https://crates.io/crates/actix-hash
|
||||
[actix-bincode]: https://crates.io/crates/actix-bincode
|
||||
[sentinel-actix]: https://crates.io/crates/sentinel-actix
|
||||
[actix-telepathy]: https://github.com/wenig/actix-telepathy
|
||||
[apistos]: https://github.com/netwo-io/apistos
|
||||
[actix-telepathy]: https://crates.io/crates/actix-telepathy
|
||||
[actix-web-validation]: https://crates.io/crates/actix-web-validation
|
||||
[actix-telepathy]: https://crates.io/crates/actix-telepathy
|
||||
[apistos]: https://crates.io/crates/apistos
|
||||
[actix-jwt-cookies]: https://crates.io/crates/actix-jwt-cookies
|
||||
[actix-ws-broadcaster]: https://crates.io/crates/actix-ws-broadcaster
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Implement `PartialEq` for `Cors` allowing for better testing.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- `Cors` is now marked `#[must_use]`.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-cors"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -24,7 +24,7 @@ draft-private-network-access = []
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
|
||||
derive_more = "0.99.7"
|
||||
derive_more = { version = "2", features = ["display", "error"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
log = "0.4"
|
||||
once_cell = "1"
|
||||
|
@ -3,11 +3,11 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-cors)
|
||||
[](https://docs.rs/actix-cors/0.7.0)
|
||||
[](https://docs.rs/actix-cors/0.7.1)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-cors/0.7.0)
|
||||
[](https://deps.rs/crate/actix-cors/0.7.1)
|
||||
[](https://crates.io/crates/actix-cors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
@ -608,6 +608,19 @@ where
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
impl PartialEq for Cors {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
// Because of the cors-function, checking if the content is equal implies that the errors are equal
|
||||
//
|
||||
// Proof by contradiction:
|
||||
// Lets assume that the inner values are equal, but the error values are not.
|
||||
// This means there had been an error, which has been fixed.
|
||||
// This cannot happen as the first call to set the invalid value means that further usages of the cors-function will reject other input.
|
||||
// => inner has to be in a different state
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::convert::Infallible;
|
||||
@ -679,4 +692,11 @@ mod test {
|
||||
|
||||
Cors::default().new_transform(srv).await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn impl_eq() {
|
||||
assert_eq!(Cors::default(), Cors::default());
|
||||
assert_ne!(Cors::default().send_wildcard(), Cors::default());
|
||||
assert_ne!(Cors::default(), Cors::permissive());
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,40 @@
|
||||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||
use derive_more::{Display, Error};
|
||||
use derive_more::derive::{Display, Error};
|
||||
|
||||
/// Errors that can occur when processing CORS guarded requests.
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum CorsError {
|
||||
/// Allowed origin argument must not be wildcard (`*`).
|
||||
#[display(fmt = "`allowed_origin` argument must not be wildcard (`*`)")]
|
||||
#[display("`allowed_origin` argument must not be wildcard (`*`)")]
|
||||
WildcardOrigin,
|
||||
|
||||
/// Request header `Origin` is required but was not provided.
|
||||
#[display(fmt = "Request header `Origin` is required but was not provided")]
|
||||
#[display("Request header `Origin` is required but was not provided")]
|
||||
MissingOrigin,
|
||||
|
||||
/// Request header `Access-Control-Request-Method` is required but is missing.
|
||||
#[display(fmt = "Request header `Access-Control-Request-Method` is required but is missing")]
|
||||
#[display("Request header `Access-Control-Request-Method` is required but is missing")]
|
||||
MissingRequestMethod,
|
||||
|
||||
/// Request header `Access-Control-Request-Method` has an invalid value.
|
||||
#[display(fmt = "Request header `Access-Control-Request-Method` has an invalid value")]
|
||||
#[display("Request header `Access-Control-Request-Method` has an invalid value")]
|
||||
BadRequestMethod,
|
||||
|
||||
/// Request header `Access-Control-Request-Headers` has an invalid value.
|
||||
#[display(fmt = "Request header `Access-Control-Request-Headers` has an invalid value")]
|
||||
#[display("Request header `Access-Control-Request-Headers` has an invalid value")]
|
||||
BadRequestHeaders,
|
||||
|
||||
/// Origin is not allowed to make this request.
|
||||
#[display(fmt = "Origin is not allowed to make this request")]
|
||||
#[display("Origin is not allowed to make this request")]
|
||||
OriginNotAllowed,
|
||||
|
||||
/// Request method is not allowed.
|
||||
#[display(fmt = "Requested method is not allowed")]
|
||||
#[display("Requested method is not allowed")]
|
||||
MethodNotAllowed,
|
||||
|
||||
/// One or more request headers are not allowed.
|
||||
#[display(fmt = "One or more request headers are not allowed")]
|
||||
#[display("One or more request headers are not allowed")]
|
||||
HeadersNotAllowed,
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,12 @@ impl Default for OriginFn {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for OriginFn {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.boxed_fn, &other.boxed_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for OriginFn {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("origin_fn")
|
||||
@ -40,7 +46,7 @@ pub(crate) fn header_value_try_into_method(hdr: &HeaderValue) -> Option<Method>
|
||||
.and_then(|meth| Method::try_from(meth).ok())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Inner {
|
||||
pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>,
|
||||
pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>,
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Update `actix-session` dependency to `0.10`.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Add `IdentityMiddlewareBuilder::{id_key, last_visit_unix_timestamp_key, login_unix_timestamp_key}()` methods for customizing keys used in session. Defaults remain the same as before.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-identity"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Luca Palmieri <rust@lpalmieri.com>",
|
||||
@ -19,11 +19,11 @@ all-features = true
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2"
|
||||
actix-session = "0.9"
|
||||
actix-session = "0.10"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||
|
||||
derive_more = "0.99.7"
|
||||
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||
futures-core = "0.3.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
@ -31,10 +31,10 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
[dev-dependencies]
|
||||
actix-http = "3"
|
||||
actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] }
|
||||
actix-session = { version = "0.9", features = ["redis-session", "cookie-session"] }
|
||||
actix-session = { version = "0.10", features = ["redis-session", "cookie-session"] }
|
||||
|
||||
env_logger = "0.11"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["cookies", "json"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[lints]
|
||||
|
@ -5,9 +5,9 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-identity)
|
||||
[](https://docs.rs/actix-identity/0.7.1)
|
||||
[](https://docs.rs/actix-identity/0.8.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-identity/0.7.1)
|
||||
[](https://deps.rs/crate/actix-identity/0.8.0)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@ -83,8 +83,10 @@ async fn login(request: HttpRequest) -> impl Responder {
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
async fn logout(user: Identity) -> impl Responder {
|
||||
user.logout();
|
||||
async fn logout(user: Option<Identity>) -> impl Responder {
|
||||
if let Some(user) = user {
|
||||
user.logout();
|
||||
}
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
```
|
||||
|
@ -13,10 +13,10 @@
|
||||
//! http -v --session=identity GET localhost:8080/
|
||||
//! ```
|
||||
|
||||
use std::io;
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use actix_identity::{Identity, IdentityMiddleware};
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::{
|
||||
cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
|
||||
HttpServer, Responder,
|
||||
@ -28,16 +28,25 @@ async fn main() -> io::Result<()> {
|
||||
|
||||
let secret_key = Key::generate();
|
||||
|
||||
let expiration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let session_mw =
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||
// disable secure cookie for local testing
|
||||
.cookie_secure(false)
|
||||
// Set a ttl for the cookie if the identity should live longer than the user session
|
||||
.session_lifecycle(
|
||||
PersistentSession::default().session_ttl(expiration.try_into().unwrap()),
|
||||
)
|
||||
.build();
|
||||
let identity_mw = IdentityMiddleware::builder()
|
||||
.visit_deadline(Some(expiration))
|
||||
.build();
|
||||
|
||||
App::new()
|
||||
// Install the identity framework first.
|
||||
.wrap(IdentityMiddleware::default())
|
||||
.wrap(identity_mw)
|
||||
// The identity system is built on top of sessions. You must install the session
|
||||
// middleware to leverage `actix-identity`. The session middleware must be mounted
|
||||
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
use actix_session::{SessionGetError, SessionInsertError};
|
||||
use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError};
|
||||
use derive_more::{Display, Error, From};
|
||||
use derive_more::derive::{Display, Error, From};
|
||||
|
||||
/// Error that can occur during login attempts.
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
pub struct LoginError(SessionInsertError);
|
||||
|
||||
impl ResponseError for LoginError {
|
||||
@ -17,7 +17,7 @@ impl ResponseError for LoginError {
|
||||
|
||||
/// Error encountered when working with a session that has expired.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "The given session has expired and is no longer valid")]
|
||||
#[display("The given session has expired and is no longer valid")]
|
||||
pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
|
||||
|
||||
/// The identity information has been lost.
|
||||
@ -25,7 +25,7 @@ pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
|
||||
/// Seeing this error in user code indicates a bug in actix-identity.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(
|
||||
fmt = "The identity information in the current session has disappeared after having been \
|
||||
"The identity information in the current session has disappeared after having been \
|
||||
successfully validated. This is likely to be a bug."
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
@ -33,7 +33,7 @@ pub struct LostIdentityError;
|
||||
|
||||
/// There is no identity information attached to the current session.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "There is no identity information attached to the current session")]
|
||||
#[display("There is no identity information attached to the current session")]
|
||||
#[non_exhaustive]
|
||||
pub struct MissingIdentityError;
|
||||
|
||||
@ -42,21 +42,21 @@ pub struct MissingIdentityError;
|
||||
#[non_exhaustive]
|
||||
pub enum GetIdentityError {
|
||||
/// The session has expired.
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
SessionExpiryError(SessionExpiryError),
|
||||
|
||||
/// No identity is found in a session.
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
MissingIdentityError(MissingIdentityError),
|
||||
|
||||
/// Failed to accessing the session store.
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
SessionGetError(SessionGetError),
|
||||
|
||||
/// Identity info was lost after being validated.
|
||||
///
|
||||
/// Seeing this error indicates a bug in actix-identity.
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
LostIdentityError(LostIdentityError),
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ impl IdentityExt for ServiceRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdentityExt for GuardContext<'a> {
|
||||
impl IdentityExt for GuardContext<'_> {
|
||||
fn get_identity(&self) -> Result<Identity, GetIdentityError> {
|
||||
Identity::extract(&self.req_data())
|
||||
}
|
||||
|
@ -74,8 +74,10 @@ async fn login(request: HttpRequest) -> impl Responder {
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
async fn logout(user: Identity) -> impl Responder {
|
||||
user.logout();
|
||||
async fn logout(user: Option<Identity>) -> impl Responder {
|
||||
if let Some(user) = user {
|
||||
user.logout();
|
||||
}
|
||||
HttpResponse::Ok()
|
||||
}
|
||||
```
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
|
||||
use actix_web::http::StatusCode;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::{fixtures::user_id, test_app::TestApp};
|
||||
|
||||
|
@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Update `redis` dependency to `0.29`.
|
||||
- Update `actix-session` dependency to `0.9`.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
- No significant changes since `0.5.0`.
|
||||
|
@ -26,13 +26,13 @@ actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||
|
||||
chrono = "0.4"
|
||||
derive_more = "0.99.7"
|
||||
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||
log = "0.4"
|
||||
redis = { version = "0.25", default-features = false, features = ["tokio-comp"] }
|
||||
redis = { version = "0.29", default-features = false, features = ["tokio-comp"] }
|
||||
time = "0.3"
|
||||
|
||||
# session
|
||||
actix-session = { version = "0.9", optional = true }
|
||||
actix-session = { version = "0.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "4"
|
||||
|
@ -1,4 +1,4 @@
|
||||
use derive_more::{Display, Error, From};
|
||||
use derive_more::derive::{Display, Error, From};
|
||||
|
||||
use crate::status::Status;
|
||||
|
||||
@ -6,20 +6,20 @@ use crate::status::Status;
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
pub enum Error {
|
||||
/// Redis client failed to connect or run a query.
|
||||
#[display(fmt = "Redis client failed to connect or run a query")]
|
||||
#[display("Redis client failed to connect or run a query")]
|
||||
Client(redis::RedisError),
|
||||
|
||||
/// Limit is exceeded for a key.
|
||||
#[display(fmt = "Limit is exceeded for a key")]
|
||||
#[display("Limit is exceeded for a key")]
|
||||
#[from(ignore)]
|
||||
LimitExceeded(#[error(not(source))] Status),
|
||||
|
||||
/// Time conversion failed.
|
||||
#[display(fmt = "Time conversion failed")]
|
||||
#[display("Time conversion failed")]
|
||||
Time(time::error::ComponentRange),
|
||||
|
||||
/// Generic error.
|
||||
#[display(fmt = "Generic error")]
|
||||
#[display("Generic error")]
|
||||
#[from(ignore)]
|
||||
Other(#[error(not(source))] String),
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ impl Status {
|
||||
/// Constructs status limit status from parts.
|
||||
#[must_use]
|
||||
pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
|
||||
let remaining = if count >= limit { 0 } else { limit - count };
|
||||
let remaining = limit.saturating_sub(count);
|
||||
|
||||
Status {
|
||||
limit,
|
||||
|
@ -19,7 +19,7 @@ all-features = true
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4", default-features = false }
|
||||
derive_more = "0.99.7"
|
||||
derive_more = { version = "2", features = ["display"] }
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
prost = { version = "0.13", default-features = false }
|
||||
|
||||
|
@ -22,7 +22,7 @@ use actix_web::{
|
||||
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder,
|
||||
ResponseError,
|
||||
};
|
||||
use derive_more::Display;
|
||||
use derive_more::derive::Display;
|
||||
use futures_util::{
|
||||
future::{FutureExt as _, LocalBoxFuture},
|
||||
stream::StreamExt as _,
|
||||
@ -32,26 +32,28 @@ use prost::{DecodeError as ProtoBufDecodeError, EncodeError as ProtoBufEncodeErr
|
||||
#[derive(Debug, Display)]
|
||||
pub enum ProtoBufPayloadError {
|
||||
/// Payload size is bigger than 256k
|
||||
#[display(fmt = "Payload size is bigger than 256k")]
|
||||
#[display("Payload size is bigger than 256k")]
|
||||
Overflow,
|
||||
|
||||
/// Content type error
|
||||
#[display(fmt = "Content type error")]
|
||||
#[display("Content type error")]
|
||||
ContentType,
|
||||
|
||||
/// Serialize error
|
||||
#[display(fmt = "ProtoBuf serialize error: {_0}")]
|
||||
#[display("ProtoBuf serialize error: {_0}")]
|
||||
Serialize(ProtoBufEncodeError),
|
||||
|
||||
/// Deserialize error
|
||||
#[display(fmt = "ProtoBuf deserialize error: {_0}")]
|
||||
#[display("ProtoBuf deserialize error: {_0}")]
|
||||
Deserialize(ProtoBufDecodeError),
|
||||
|
||||
/// Payload error
|
||||
#[display(fmt = "Error that occur during reading payload: {_0}")]
|
||||
#[display("Error that occur during reading payload: {_0}")]
|
||||
Payload(PayloadError),
|
||||
}
|
||||
|
||||
// TODO: impl error for ProtoBufPayloadError
|
||||
|
||||
impl ResponseError for ProtoBufPayloadError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
|
@ -2,10 +2,23 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Add `Session::contains_key` method.
|
||||
- Add `Session::update[_or]()` methods.
|
||||
- Update `redis` dependency to `0.29`.
|
||||
|
||||
## 0.10.1
|
||||
|
||||
- Expose `storage::generate_session_key()` without needing to enable a crate feature.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Add `redis-session-rustls` crate feature that enables `rustls`-secured Redis sessions.
|
||||
- Add `redis-pool` crate feature (off-by-default) which enables `RedisSessionStore::{new, builder}_pooled()` constructors.
|
||||
- Rename `redis-rs-session` crate feature to `redis-session`.
|
||||
- Rename `redis-rs-tls-session` crate feature to `redis-session-native-tls`.
|
||||
- Remove `redis-actor-session` crate feature (and, therefore, the `actix-redis` based storage backend).
|
||||
- Expose `storage::generate_session_key()`.
|
||||
- Update `redis` dependency to `0.26`.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-session"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Luca Palmieri <rust@lpalmieri.com>",
|
||||
@ -20,9 +20,10 @@ all-features = true
|
||||
[features]
|
||||
default = []
|
||||
cookie-session = []
|
||||
redis-session = ["dep:redis", "dep:rand"]
|
||||
redis-session = ["dep:redis"]
|
||||
redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"]
|
||||
redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"]
|
||||
redis-pool = ["dep:deadpool-redis"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2"
|
||||
@ -30,18 +31,19 @@ actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||
|
||||
anyhow = "1"
|
||||
derive_more = "0.99.7"
|
||||
rand = { version = "0.8", optional = true }
|
||||
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||
rand = "0.9"
|
||||
serde = { version = "1" }
|
||||
serde_json = { version = "1" }
|
||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||
|
||||
# redis-session
|
||||
redis = { version = "0.25", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
|
||||
redis = { version = "0.29", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
|
||||
deadpool-redis = { version = "0.20", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-session = { path = ".", features = ["cookie-session", "redis-session"] }
|
||||
actix-test = "0.1.0-beta.10"
|
||||
actix-test = "0.1"
|
||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing = "0.1.30"
|
||||
|
@ -5,9 +5,9 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-session)
|
||||
[](https://docs.rs/actix-session/0.9.0)
|
||||
[](https://docs.rs/actix-session/0.10.1)
|
||||

|
||||
[](https://deps.rs/crate/actix-session/0.9.0)
|
||||
[](https://deps.rs/crate/actix-session/0.10.1)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@ -94,34 +94,26 @@ By default, `actix-session` does not provide any storage backend to retrieve and
|
||||
|
||||
- a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature flag.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# ...
|
||||
actix-session = { version = "...", features = ["cookie-session"] }
|
||||
```console
|
||||
cargo add actix-session --features=cookie-session
|
||||
```
|
||||
|
||||
- a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using the `redis-session` feature flag.
|
||||
- a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the `redis-session` feature flag.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# ...
|
||||
actix-session = { version = "...", features = ["redis-session"] }
|
||||
```console
|
||||
cargo add actix-session --features=redis-session
|
||||
```
|
||||
|
||||
Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure connection (via the `native-tls` crate):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# ...
|
||||
actix-session = { version = "...", features = ["redis-session-native-tls"] }
|
||||
```console
|
||||
cargo add actix-session --features=redis-session-native-tls
|
||||
```
|
||||
|
||||
If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# ...
|
||||
actix-session = { version = "...", features = ["redis-session-rustls"] }
|
||||
```console
|
||||
cargo add actix-session --features=redis-session-rustls
|
||||
```
|
||||
|
||||
You can implement your own session storage backend using the [`SessionStore`] trait.
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Configuration options to tune the behaviour of [`SessionMiddleware`].
|
||||
|
||||
use actix_web::cookie::{time::Duration, Key, SameSite};
|
||||
use derive_more::From;
|
||||
use derive_more::derive::From;
|
||||
|
||||
use crate::{storage::SessionStore, SessionMiddleware};
|
||||
|
||||
|
@ -99,36 +99,28 @@
|
||||
//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
|
||||
//! flag.
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # ...
|
||||
//! actix-session = { version = "...", features = ["cookie-session"] }
|
||||
//! ```console
|
||||
//! cargo add actix-session --features=cookie-session
|
||||
//! ```
|
||||
//!
|
||||
//! - a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using
|
||||
//! the `redis-session` feature flag.
|
||||
//! - a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the
|
||||
//! `redis-session` feature flag.
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # ...
|
||||
//! actix-session = { version = "...", features = ["redis-session"] }
|
||||
//! ```console
|
||||
//! cargo add actix-session --features=redis-session
|
||||
//! ```
|
||||
//!
|
||||
//! Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure
|
||||
//! connection (via the `native-tls` crate):
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # ...
|
||||
//! actix-session = { version = "...", features = ["redis-session-native-tls"] }
|
||||
//! ```console
|
||||
//! cargo add actix-session --features=redis-session-native-tls
|
||||
//! ```
|
||||
//!
|
||||
//! If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # ...
|
||||
//! actix-session = { version = "...", features = ["redis-session-rustls"] }
|
||||
//! ```console
|
||||
//! cargo add actix-session --features=redis-session-rustls
|
||||
//! ```
|
||||
//!
|
||||
//! You can implement your own session storage backend using the [`SessionStore`] trait.
|
||||
@ -156,6 +148,7 @@ pub use self::{
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(missing_docs)]
|
||||
pub mod test_helpers {
|
||||
use actix_web::cookie::Key;
|
||||
|
||||
|
@ -14,7 +14,7 @@ use actix_web::{
|
||||
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use derive_more::{Display, From};
|
||||
use derive_more::derive::{Display, From};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// The primary interface to access and modify session state.
|
||||
@ -33,6 +33,9 @@ use serde::{de::DeserializeOwned, Serialize};
|
||||
/// session.insert("counter", 1)?;
|
||||
/// }
|
||||
///
|
||||
/// // or use the shorthand
|
||||
/// session.update_or("counter", 1, |count: i32| count + 1);
|
||||
///
|
||||
/// Ok("Welcome!")
|
||||
/// }
|
||||
/// # actix_web::web::to(index);
|
||||
@ -97,6 +100,11 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the session contains a value for the specified `key`.
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.0.borrow().state.contains_key(key)
|
||||
}
|
||||
|
||||
/// Get all raw key-value data from the session.
|
||||
///
|
||||
/// Note that values are JSON encoded.
|
||||
@ -114,7 +122,9 @@ impl Session {
|
||||
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
|
||||
/// only a reference to the value is taken.
|
||||
///
|
||||
/// It returns an error if it fails to serialize `value` to JSON.
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if JSON serialization of `value` fails.
|
||||
pub fn insert<T: Serialize>(
|
||||
&self,
|
||||
key: impl Into<String>,
|
||||
@ -132,9 +142,8 @@ impl Session {
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to serialize the provided `{}` type instance as JSON in order to \
|
||||
attach as session data to the `{}` key",
|
||||
attach as session data to the `{key}` key",
|
||||
std::any::type_name::<T>(),
|
||||
&key
|
||||
)
|
||||
})
|
||||
.map_err(SessionInsertError)?;
|
||||
@ -145,6 +154,83 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates a key-value pair into the session.
|
||||
///
|
||||
/// If the key exists then update it to the new value and place it back in. If the key does not
|
||||
/// exist it will not be updated.
|
||||
///
|
||||
/// Any serializable value can be used and will be encoded as JSON in the session data, hence
|
||||
/// why only a reference to the value is taken.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if JSON serialization of the value fails.
|
||||
pub fn update<T: Serialize + DeserializeOwned, F>(
|
||||
&self,
|
||||
key: impl Into<String>,
|
||||
updater: F,
|
||||
) -> Result<(), SessionUpdateError>
|
||||
where
|
||||
F: FnOnce(T) -> T,
|
||||
{
|
||||
let mut inner = self.0.borrow_mut();
|
||||
let key_str = key.into();
|
||||
|
||||
if let Some(val_str) = inner.state.get(&key_str) {
|
||||
let value = serde_json::from_str(val_str)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to deserialize the JSON-encoded session data attached to key \
|
||||
`{key_str}` as a `{}` type",
|
||||
std::any::type_name::<T>()
|
||||
)
|
||||
})
|
||||
.map_err(SessionUpdateError)?;
|
||||
|
||||
let val = serde_json::to_string(&updater(value))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to serialize the provided `{}` type instance as JSON in order to \
|
||||
attach as session data to the `{key_str}` key",
|
||||
std::any::type_name::<T>(),
|
||||
)
|
||||
})
|
||||
.map_err(SessionUpdateError)?;
|
||||
|
||||
inner.state.insert(key_str, val);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates a key-value pair into the session, or inserts a default value.
|
||||
///
|
||||
/// If the key exists then update it to the new value and place it back in. If the key does not
|
||||
/// exist the default value will be inserted instead.
|
||||
///
|
||||
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
|
||||
/// only a reference to the value is taken.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns error if JSON serialization of a value fails.
|
||||
pub fn update_or<T: Serialize + DeserializeOwned, F>(
|
||||
&self,
|
||||
key: &str,
|
||||
default_value: T,
|
||||
updater: F,
|
||||
) -> Result<(), SessionUpdateError>
|
||||
where
|
||||
F: FnOnce(T) -> T,
|
||||
{
|
||||
if self.contains_key(key) {
|
||||
self.update(key, updater)
|
||||
} else {
|
||||
self.insert(key, default_value)
|
||||
.map_err(|err| SessionUpdateError(err.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove value from the session.
|
||||
///
|
||||
/// If present, the JSON encoded value is returned.
|
||||
@ -288,7 +374,7 @@ impl FromRequest for Session {
|
||||
|
||||
/// Error returned by [`Session::get`].
|
||||
#[derive(Debug, Display, From)]
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
pub struct SessionGetError(anyhow::Error);
|
||||
|
||||
impl StdError for SessionGetError {
|
||||
@ -305,7 +391,7 @@ impl ResponseError for SessionGetError {
|
||||
|
||||
/// Error returned by [`Session::insert`].
|
||||
#[derive(Debug, Display, From)]
|
||||
#[display(fmt = "{_0}")]
|
||||
#[display("{_0}")]
|
||||
pub struct SessionInsertError(anyhow::Error);
|
||||
|
||||
impl StdError for SessionInsertError {
|
||||
@ -319,3 +405,20 @@ impl ResponseError for SessionInsertError {
|
||||
HttpResponse::new(self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by [`Session::update`].
|
||||
#[derive(Debug, Display, From)]
|
||||
#[display("{_0}")]
|
||||
pub struct SessionUpdateError(anyhow::Error);
|
||||
|
||||
impl StdError for SessionUpdateError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(self.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for SessionUpdateError {
|
||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||
HttpResponse::new(self.status_code())
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ impl SessionExt for ServiceResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SessionExt for GuardContext<'a> {
|
||||
impl SessionExt for GuardContext<'_> {
|
||||
fn get_session(&self) -> Session {
|
||||
Session::get_session(&mut self.req_data_mut())
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, future::Future};
|
||||
|
||||
use actix_web::cookie::time::Duration;
|
||||
use derive_more::Display;
|
||||
use derive_more::derive::Display;
|
||||
|
||||
use super::SessionKey;
|
||||
|
||||
@ -53,11 +53,11 @@ pub trait SessionStore {
|
||||
#[derive(Debug, Display)]
|
||||
pub enum LoadError {
|
||||
/// Failed to deserialize session state.
|
||||
#[display(fmt = "Failed to deserialize session state")]
|
||||
#[display("Failed to deserialize session state")]
|
||||
Deserialization(anyhow::Error),
|
||||
|
||||
/// Something went wrong when retrieving the session state.
|
||||
#[display(fmt = "Something went wrong when retrieving the session state")]
|
||||
#[display("Something went wrong when retrieving the session state")]
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
@ -74,11 +74,11 @@ impl std::error::Error for LoadError {
|
||||
#[derive(Debug, Display)]
|
||||
pub enum SaveError {
|
||||
/// Failed to serialize session state.
|
||||
#[display(fmt = "Failed to serialize session state")]
|
||||
#[display("Failed to serialize session state")]
|
||||
Serialization(anyhow::Error),
|
||||
|
||||
/// Something went wrong when persisting the session state.
|
||||
#[display(fmt = "Something went wrong when persisting the session state")]
|
||||
#[display("Something went wrong when persisting the session state")]
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
@ -95,11 +95,11 @@ impl std::error::Error for SaveError {
|
||||
/// Possible failures modes for [`SessionStore::update`].
|
||||
pub enum UpdateError {
|
||||
/// Failed to serialize session state.
|
||||
#[display(fmt = "Failed to serialize session state")]
|
||||
#[display("Failed to serialize session state")]
|
||||
Serialization(anyhow::Error),
|
||||
|
||||
/// Something went wrong when updating the session state.
|
||||
#[display(fmt = "Something went wrong when updating the session state.")]
|
||||
#[display("Something went wrong when updating the session state.")]
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,19 @@
|
||||
//! Pluggable storage backends for session state.
|
||||
|
||||
mod interface;
|
||||
mod session_key;
|
||||
|
||||
pub use self::{
|
||||
interface::{LoadError, SaveError, SessionStore, UpdateError},
|
||||
session_key::SessionKey,
|
||||
};
|
||||
|
||||
#[cfg(feature = "cookie-session")]
|
||||
mod cookie;
|
||||
|
||||
mod interface;
|
||||
#[cfg(feature = "redis-session")]
|
||||
mod redis_rs;
|
||||
|
||||
#[cfg(feature = "redis-session")]
|
||||
mod session_key;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "cookie-session")]
|
||||
pub use cookie::CookieSessionStore;
|
||||
pub use self::cookie::CookieSessionStore;
|
||||
#[cfg(feature = "redis-session")]
|
||||
pub use redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
|
||||
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
|
||||
pub use self::{
|
||||
interface::{LoadError, SaveError, SessionStore, UpdateError},
|
||||
session_key::SessionKey,
|
||||
utils::generate_session_key,
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use actix_web::cookie::time::Duration;
|
||||
use anyhow::Error;
|
||||
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
|
||||
use redis::{aio::ConnectionManager, AsyncCommands, Client, Cmd, FromRedisValue, Value};
|
||||
|
||||
use super::SessionKey;
|
||||
use crate::storage::{
|
||||
@ -44,7 +44,7 @@ use crate::storage::{
|
||||
/// ```
|
||||
///
|
||||
/// # TLS support
|
||||
/// Add the `redis-rs-tls-session` or `redis-rs-tls-session-rustls` feature flag to enable TLS support. You can then establish a TLS
|
||||
/// Add the `redis-session-native-tls` or `redis-session-rustls` feature flag to enable TLS support. You can then establish a TLS
|
||||
/// connection to Redis using the `rediss://` URL scheme:
|
||||
///
|
||||
/// ```no_run
|
||||
@ -56,14 +56,38 @@ use crate::storage::{
|
||||
/// # })
|
||||
/// ```
|
||||
///
|
||||
/// # Implementation notes
|
||||
/// `RedisSessionStore` leverages [`redis-rs`] as Redis client.
|
||||
/// # Pooled Redis Connections
|
||||
///
|
||||
/// [`redis-rs`]: https://github.com/mitsuhiko/redis-rs
|
||||
/// When the `redis-pool` crate feature is enabled, a pre-existing pool from [`deadpool_redis`] can
|
||||
/// be provided.
|
||||
///
|
||||
/// ```no_run
|
||||
/// use actix_session::storage::RedisSessionStore;
|
||||
/// use deadpool_redis::{Config, Runtime};
|
||||
///
|
||||
/// let redis_cfg = Config::from_url("redis://127.0.0.1:6379");
|
||||
/// let redis_pool = redis_cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
|
||||
///
|
||||
/// let store = RedisSessionStore::new_pooled(redis_pool);
|
||||
/// ```
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
/// `RedisSessionStore` leverages the [`redis`] crate as the underlying Redis client.
|
||||
#[derive(Clone)]
|
||||
pub struct RedisSessionStore {
|
||||
configuration: CacheConfiguration,
|
||||
client: ConnectionManager,
|
||||
client: RedisSessionConn,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum RedisSessionConn {
|
||||
/// Single connection.
|
||||
Single(ConnectionManager),
|
||||
|
||||
/// Connection pool.
|
||||
#[cfg(feature = "redis-pool")]
|
||||
Pool(deadpool_redis::Pool),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -80,34 +104,77 @@ impl Default for CacheConfiguration {
|
||||
}
|
||||
|
||||
impl RedisSessionStore {
|
||||
/// A fluent API to configure [`RedisSessionStore`].
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
|
||||
/// connection string for Redis.
|
||||
pub fn builder<S: Into<String>>(connection_string: S) -> RedisSessionStoreBuilder {
|
||||
/// Returns a fluent API builder to configure [`RedisSessionStore`].
|
||||
///
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||
/// - a connection string for Redis.
|
||||
pub fn builder(connection_string: impl Into<String>) -> RedisSessionStoreBuilder {
|
||||
RedisSessionStoreBuilder {
|
||||
configuration: CacheConfiguration::default(),
|
||||
connection_string: connection_string.into(),
|
||||
conn_builder: RedisSessionConnBuilder::Single(connection_string.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new instance of [`RedisSessionStore`] using the default configuration.
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
|
||||
/// connection string for Redis.
|
||||
pub async fn new<S: Into<String>>(
|
||||
connection_string: S,
|
||||
) -> Result<RedisSessionStore, anyhow::Error> {
|
||||
/// Returns a fluent API builder to configure [`RedisSessionStore`].
|
||||
///
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||
/// - a pool object for Redis.
|
||||
#[cfg(feature = "redis-pool")]
|
||||
pub fn builder_pooled(pool: impl Into<deadpool_redis::Pool>) -> RedisSessionStoreBuilder {
|
||||
RedisSessionStoreBuilder {
|
||||
configuration: CacheConfiguration::default(),
|
||||
conn_builder: RedisSessionConnBuilder::Pool(pool.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
|
||||
///
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||
/// - a connection string for Redis.
|
||||
pub async fn new(connection_string: impl Into<String>) -> Result<RedisSessionStore, Error> {
|
||||
Self::builder(connection_string).build().await
|
||||
}
|
||||
|
||||
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
|
||||
///
|
||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||
/// - a pool object for Redis.
|
||||
#[cfg(feature = "redis-pool")]
|
||||
pub async fn new_pooled(
|
||||
pool: impl Into<deadpool_redis::Pool>,
|
||||
) -> anyhow::Result<RedisSessionStore> {
|
||||
Self::builder_pooled(pool).build().await
|
||||
}
|
||||
}
|
||||
|
||||
/// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
|
||||
/// parameters.
|
||||
///
|
||||
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore
|
||||
#[must_use]
|
||||
pub struct RedisSessionStoreBuilder {
|
||||
connection_string: String,
|
||||
configuration: CacheConfiguration,
|
||||
conn_builder: RedisSessionConnBuilder,
|
||||
}
|
||||
|
||||
enum RedisSessionConnBuilder {
|
||||
/// Single connection string.
|
||||
Single(String),
|
||||
|
||||
/// Pre-built connection pool.
|
||||
#[cfg(feature = "redis-pool")]
|
||||
Pool(deadpool_redis::Pool),
|
||||
}
|
||||
|
||||
impl RedisSessionConnBuilder {
|
||||
async fn into_client(self) -> anyhow::Result<RedisSessionConn> {
|
||||
Ok(match self {
|
||||
RedisSessionConnBuilder::Single(conn_string) => {
|
||||
RedisSessionConn::Single(ConnectionManager::new(Client::open(conn_string)?).await?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "redis-pool")]
|
||||
RedisSessionConnBuilder::Pool(pool) => RedisSessionConn::Pool(pool),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RedisSessionStoreBuilder {
|
||||
@ -120,9 +187,10 @@ impl RedisSessionStoreBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Finalise the builder and return a [`RedisSessionStore`] instance.
|
||||
pub async fn build(self) -> Result<RedisSessionStore, anyhow::Error> {
|
||||
let client = ConnectionManager::new(redis::Client::open(self.connection_string)?).await?;
|
||||
/// Finalises builder and returns a [`RedisSessionStore`] instance.
|
||||
pub async fn build(self) -> anyhow::Result<RedisSessionStore> {
|
||||
let client = self.conn_builder.into_client().await?;
|
||||
|
||||
Ok(RedisSessionStore {
|
||||
configuration: self.configuration,
|
||||
client,
|
||||
@ -137,7 +205,6 @@ impl SessionStore for RedisSessionStore {
|
||||
let value: Option<String> = self
|
||||
.execute_command(redis::cmd("GET").arg(&[&cache_key]))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map_err(LoadError::Other)?;
|
||||
|
||||
match value {
|
||||
@ -172,7 +239,6 @@ impl SessionStore for RedisSessionStore {
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map_err(SaveError::Other)?;
|
||||
|
||||
Ok(session_key)
|
||||
@ -190,7 +256,7 @@ impl SessionStore for RedisSessionStore {
|
||||
|
||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||
|
||||
let v: redis::Value = self
|
||||
let v: Value = self
|
||||
.execute_command(redis::cmd("SET").arg(&[
|
||||
&cache_key,
|
||||
&body,
|
||||
@ -199,7 +265,6 @@ impl SessionStore for RedisSessionStore {
|
||||
&format!("{}", ttl.whole_seconds()),
|
||||
]))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map_err(UpdateError::Other)?;
|
||||
|
||||
match v {
|
||||
@ -215,7 +280,7 @@ impl SessionStore for RedisSessionStore {
|
||||
SaveError::Other(err) => UpdateError::Other(err),
|
||||
})
|
||||
}
|
||||
Value::Int(_) | Value::Okay | Value::Status(_) => Ok(session_key),
|
||||
Value::Int(_) | Value::Okay | Value::SimpleString(_) => Ok(session_key),
|
||||
val => Err(UpdateError::Other(anyhow::anyhow!(
|
||||
"Failed to update session state. {:?}",
|
||||
val
|
||||
@ -223,23 +288,33 @@ impl SessionStore for RedisSessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
|
||||
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> anyhow::Result<()> {
|
||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||
|
||||
self.client
|
||||
.clone()
|
||||
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
|
||||
.await?;
|
||||
match self.client {
|
||||
RedisSessionConn::Single(ref conn) => {
|
||||
conn.clone()
|
||||
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
|
||||
.await?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "redis-pool")]
|
||||
RedisSessionConn::Pool(ref pool) => {
|
||||
pool.get()
|
||||
.await?
|
||||
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
|
||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> {
|
||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||
|
||||
self.execute_command::<()>(redis::cmd("DEL").arg(&[&cache_key]))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map_err(UpdateError::Other)?;
|
||||
|
||||
Ok(())
|
||||
@ -261,24 +336,55 @@ impl RedisSessionStore {
|
||||
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
|
||||
/// a different more meaningful reason).
|
||||
#[allow(clippy::needless_pass_by_ref_mut)]
|
||||
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
|
||||
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<T> {
|
||||
let mut can_retry = true;
|
||||
|
||||
loop {
|
||||
match cmd.query_async(&mut self.client.clone()).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if can_retry && err.is_connection_dropped() {
|
||||
tracing::debug!(
|
||||
"Connection dropped while trying to talk to Redis. Retrying."
|
||||
);
|
||||
match self.client {
|
||||
RedisSessionConn::Single(ref conn) => {
|
||||
let mut conn = conn.clone();
|
||||
|
||||
// Retry at most once
|
||||
can_retry = false;
|
||||
loop {
|
||||
match cmd.query_async(&mut conn).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if can_retry && err.is_connection_dropped() {
|
||||
tracing::debug!(
|
||||
"Connection dropped while trying to talk to Redis. Retrying."
|
||||
);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
return Err(err);
|
||||
// Retry at most once
|
||||
can_retry = false;
|
||||
|
||||
continue;
|
||||
} else {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "redis-pool")]
|
||||
RedisSessionConn::Pool(ref pool) => {
|
||||
let mut conn = pool.get().await?;
|
||||
|
||||
loop {
|
||||
match cmd.query_async(&mut conn).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(err) => {
|
||||
if can_retry && err.is_connection_dropped() {
|
||||
tracing::debug!(
|
||||
"Connection dropped while trying to talk to Redis. Retrying."
|
||||
);
|
||||
|
||||
// Retry at most once
|
||||
can_retry = false;
|
||||
|
||||
continue;
|
||||
} else {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -291,14 +397,27 @@ mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::cookie::time;
|
||||
#[cfg(not(feature = "redis-session"))]
|
||||
use deadpool_redis::{Config, Runtime};
|
||||
|
||||
use super::*;
|
||||
use crate::test_helpers::acceptance_test_suite;
|
||||
|
||||
async fn redis_store() -> RedisSessionStore {
|
||||
RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||
.await
|
||||
.unwrap()
|
||||
#[cfg(feature = "redis-session")]
|
||||
{
|
||||
RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "redis-session"))]
|
||||
{
|
||||
let redis_pool = Config::from_url("redis://127.0.0.1:6379")
|
||||
.create_pool(Some(Runtime::Tokio1))
|
||||
.unwrap();
|
||||
RedisSessionStore::new(redis_pool.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
@ -318,12 +437,25 @@ mod tests {
|
||||
async fn loading_an_invalid_session_state_returns_deserialization_error() {
|
||||
let store = redis_store().await;
|
||||
let session_key = generate_session_key();
|
||||
store
|
||||
.client
|
||||
.clone()
|
||||
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match store.client {
|
||||
RedisSessionConn::Single(ref conn) => conn
|
||||
.clone()
|
||||
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||
.await
|
||||
.unwrap(),
|
||||
|
||||
#[cfg(feature = "redis-pool")]
|
||||
RedisSessionConn::Pool(ref pool) => {
|
||||
pool.get()
|
||||
.await
|
||||
.unwrap()
|
||||
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
store.load(&session_key).await.unwrap_err(),
|
||||
LoadError::Deserialization(_),
|
||||
|
@ -1,4 +1,4 @@
|
||||
use derive_more::{Display, From};
|
||||
use derive_more::derive::{Display, From};
|
||||
|
||||
/// A session key, the string stored in a client-side cookie to associate a user with its session
|
||||
/// state on the backend.
|
||||
@ -45,7 +45,7 @@ impl From<SessionKey> for String {
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, From)]
|
||||
#[display(fmt = "The provided string is not a valid session key")]
|
||||
#[display("The provided string is not a valid session key")]
|
||||
pub struct InvalidSessionKeyError(anyhow::Error);
|
||||
|
||||
impl std::error::Error for InvalidSessionKeyError {
|
||||
|
@ -1,17 +1,13 @@
|
||||
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
|
||||
use rand::distr::{Alphanumeric, SampleString as _};
|
||||
|
||||
use crate::storage::SessionKey;
|
||||
|
||||
/// Session key generation routine that follows [OWASP recommendations].
|
||||
///
|
||||
/// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
|
||||
pub(crate) fn generate_session_key() -> SessionKey {
|
||||
let value = std::iter::repeat(())
|
||||
.map(|()| OsRng.sample(Alphanumeric))
|
||||
.take(64)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// These unwraps will never panic because pre-conditions are always verified
|
||||
// (i.e. length and character set)
|
||||
String::from_utf8(value).unwrap().try_into().unwrap()
|
||||
pub fn generate_session_key() -> SessionKey {
|
||||
Alphanumeric
|
||||
.sample_string(&mut rand::rng(), 64)
|
||||
.try_into()
|
||||
.expect("generated string should be within size range for a session key")
|
||||
}
|
||||
|
@ -69,6 +69,16 @@ async fn session_entries() {
|
||||
map.contains_key("test_num");
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn session_contains_key() {
|
||||
let req = test::TestRequest::default().to_srv_request();
|
||||
let session = req.get_session();
|
||||
session.insert("test_str", "val").unwrap();
|
||||
session.insert("test_str", 1).unwrap();
|
||||
assert!(session.contains_key("test_str"));
|
||||
assert!(!session.contains_key("test_num"));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn insert_session_after_renew() {
|
||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||
@ -83,6 +93,35 @@ async fn insert_session_after_renew() {
|
||||
assert_eq!(session.status(), SessionStatus::Renewed);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn update_session() {
|
||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||
|
||||
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||
assert_eq!(session.status(), SessionStatus::Unchanged);
|
||||
|
||||
session.insert("test_val", 0).unwrap();
|
||||
assert_eq!(session.status(), SessionStatus::Changed);
|
||||
|
||||
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||
assert_eq!(session.get("test_val").unwrap(), Some(1));
|
||||
|
||||
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||
assert_eq!(session.get("test_val").unwrap(), Some(2));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn update_or_session() {
|
||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||
|
||||
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
|
||||
assert_eq!(session.status(), SessionStatus::Changed);
|
||||
assert_eq!(session.get("test_val").unwrap(), Some(1));
|
||||
|
||||
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
|
||||
assert_eq!(session.get("test_val").unwrap(), Some(2));
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn remove_session_after_renew() {
|
||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||
|
@ -2,6 +2,13 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Add `openssl` crate feature for TLS settings using OpenSSL.
|
||||
- Add `ApplySettings::try_apply_settings()`.
|
||||
- Implement TLS logic for `ApplySettings::try_apply_settings()`.
|
||||
- Add `Tls::get_ssl_acceptor_builder()` function to build `openssl::ssl::SslAcceptorBuilder`.
|
||||
- Deprecate `ApplySettings::apply_settings()`.
|
||||
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||
|
||||
## 0.7.1
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-settings"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
authors = [
|
||||
"Joey Ezechiels <joey.ezechiels@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -14,14 +14,17 @@ rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
openssl = ["dep:openssl", "actix-web/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3"
|
||||
actix-service = "2"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
derive_more = "0.99.7"
|
||||
once_cell = "1.13"
|
||||
derive_more = { version = "2", features = ["display", "error"] }
|
||||
once_cell = "1.21"
|
||||
openssl = { version = "0.10", features = ["v110"], optional = true }
|
||||
regex = "1.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
@ -5,9 +5,9 @@
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-settings)
|
||||
[](https://docs.rs/actix-settings/0.7.1)
|
||||
[](https://docs.rs/actix-settings/0.8.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-settings/0.7.1)
|
||||
[](https://deps.rs/crate/actix-settings/0.8.0)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@ -23,10 +23,6 @@ There is a way to extend the available settings. This can be used to combine the
|
||||
|
||||
Have a look at [the usage example][usage] to see how.
|
||||
|
||||
## WIP
|
||||
|
||||
Configuration options for TLS set up are not yet implemented.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
This crate was made possible by support from Accept B.V and [@jjpe].
|
||||
|
@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> {
|
||||
}
|
||||
})
|
||||
// apply the `Settings` to Actix Web's `HttpServer`
|
||||
.apply_settings(&settings)
|
||||
.try_apply_settings(&settings)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError};
|
||||
|
||||
use derive_more::{Display, Error};
|
||||
use derive_more::derive::{Display, Error};
|
||||
#[cfg(feature = "openssl")]
|
||||
use openssl::error::ErrorStack as OpenSSLError;
|
||||
use toml::de::Error as TomlError;
|
||||
|
||||
/// Errors that can be returned from methods in this crate.
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub enum Error {
|
||||
/// Environment variable does not exists or is invalid.
|
||||
#[display(fmt = "Env var error: {_0}")]
|
||||
#[display("Env var error: {_0}")]
|
||||
EnvVarError(VarError),
|
||||
|
||||
/// File already exists on disk.
|
||||
#[display(fmt = "File exists: {}", "_0.display()")]
|
||||
#[display("File exists: {}", _0.display())]
|
||||
FileExists(#[error(not(source))] PathBuf),
|
||||
|
||||
/// Invalid value.
|
||||
#[allow(missing_docs)]
|
||||
#[display(fmt = "Expected {expected}, got {got} (@ {file}:{line}:{column})")]
|
||||
#[display("Expected {expected}, got {got} (@ {file}:{line}:{column})")]
|
||||
InvalidValue {
|
||||
expected: &'static str,
|
||||
got: String,
|
||||
@ -26,23 +28,28 @@ pub enum Error {
|
||||
},
|
||||
|
||||
/// I/O error.
|
||||
#[display(fmt = "")]
|
||||
#[display("I/O error: {_0}")]
|
||||
IoError(io::Error),
|
||||
|
||||
/// OpenSSL Error.
|
||||
#[cfg(feature = "openssl")]
|
||||
#[display("OpenSSL error: {_0}")]
|
||||
OpenSSLError(OpenSSLError),
|
||||
|
||||
/// Value is not a boolean.
|
||||
#[display(fmt = "Failed to parse boolean: {_0}")]
|
||||
#[display("Failed to parse boolean: {_0}")]
|
||||
ParseBoolError(ParseBoolError),
|
||||
|
||||
/// Value is not an integer.
|
||||
#[display(fmt = "Failed to parse integer: {_0}")]
|
||||
#[display("Failed to parse integer: {_0}")]
|
||||
ParseIntError(ParseIntError),
|
||||
|
||||
/// Value is not an address.
|
||||
#[display(fmt = "Failed to parse address: {_0}")]
|
||||
#[display("Failed to parse address: {_0}")]
|
||||
ParseAddressError(#[error(not(source))] String),
|
||||
|
||||
/// Error deserializing as TOML.
|
||||
#[display(fmt = "TOML error: {_0}")]
|
||||
#[display("TOML error: {_0}")]
|
||||
TomlError(TomlError),
|
||||
}
|
||||
|
||||
@ -64,6 +71,13 @@ impl From<io::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
impl From<OpenSSLError> for Error {
|
||||
fn from(err: OpenSSLError) -> Self {
|
||||
Self::OpenSSLError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseBoolError> for Error {
|
||||
fn from(err: ParseBoolError) -> Self {
|
||||
Self::ParseBoolError(err)
|
||||
@ -101,6 +115,9 @@ impl From<Error> for io::Error {
|
||||
|
||||
Error::IoError(io_error) => io_error,
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
Error::OpenSSLError(ossl_error) => io::Error::new(io::ErrorKind::Other, ossl_error),
|
||||
|
||||
Error::ParseBoolError(_) => {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
|
||||
}
|
||||
|
@ -54,7 +54,7 @@
|
||||
//! }
|
||||
//! })
|
||||
//! // apply the `Settings` to Actix Web's `HttpServer`
|
||||
//! .apply_settings(&settings)
|
||||
//! .try_apply_settings(&settings)?
|
||||
//! .run()
|
||||
//! .await
|
||||
//! }
|
||||
@ -89,12 +89,14 @@ mod error;
|
||||
mod parse;
|
||||
mod settings;
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
pub use self::settings::Tls;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
parse::Parse,
|
||||
settings::{
|
||||
ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode,
|
||||
NumWorkers, Timeout, Tls,
|
||||
NumWorkers, Timeout,
|
||||
},
|
||||
};
|
||||
|
||||
@ -239,10 +241,28 @@ where
|
||||
}
|
||||
|
||||
/// Extension trait for applying parsed settings to the server object.
|
||||
pub trait ApplySettings<S> {
|
||||
/// Apply some settings object value to `self`.
|
||||
#[must_use]
|
||||
fn apply_settings(self, settings: &S) -> Self;
|
||||
pub trait ApplySettings<S>: Sized {
|
||||
/// Applies some settings object value to `self`.
|
||||
///
|
||||
/// The default implementation calls [`try_apply_settings()`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if settings are invalid or cannot be applied.
|
||||
///
|
||||
/// [`try_apply_settings()`]: ApplySettings::try_apply_settings().
|
||||
#[deprecated = "Prefer `try_apply_settings()`."]
|
||||
fn apply_settings(self, settings: &S) -> Self {
|
||||
self.try_apply_settings(settings)
|
||||
.expect("Could not apply settings")
|
||||
}
|
||||
|
||||
/// Applies some settings object value to `self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// May return error if settings are invalid or cannot be applied.
|
||||
fn try_apply_settings(self, settings: &S) -> AsResult<Self>;
|
||||
}
|
||||
|
||||
impl<F, I, S, B> ApplySettings<ActixSettings> for HttpServer<F, I, S, B>
|
||||
@ -256,17 +276,27 @@ where
|
||||
S::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
fn apply_settings(mut self, settings: &ActixSettings) -> Self {
|
||||
if settings.tls.enabled {
|
||||
// for Address { host, port } in &settings.actix.hosts {
|
||||
// self = self.bind(format!("{}:{}", host, port))
|
||||
// .unwrap(/*TODO*/);
|
||||
// }
|
||||
unimplemented!("[ApplySettings] TLS support has not been implemented yet.");
|
||||
} else {
|
||||
for Address { host, port } in &settings.hosts {
|
||||
self = self.bind(format!("{host}:{port}"))
|
||||
.unwrap(/*TODO*/);
|
||||
fn apply_settings(self, settings: &ActixSettings) -> Self {
|
||||
self.try_apply_settings(settings).unwrap()
|
||||
}
|
||||
|
||||
fn try_apply_settings(mut self, settings: &ActixSettings) -> AsResult<Self> {
|
||||
for Address { host, port } in &settings.hosts {
|
||||
#[cfg(feature = "openssl")]
|
||||
{
|
||||
if settings.tls.enabled {
|
||||
self = self.bind_openssl(
|
||||
format!("{}:{}", host, port),
|
||||
settings.tls.get_ssl_acceptor_builder()?,
|
||||
)?;
|
||||
} else {
|
||||
self = self.bind(format!("{host}:{port}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
{
|
||||
self = self.bind(format!("{host}:{port}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,7 +349,7 @@ where
|
||||
Timeout::Seconds(n) => self.shutdown_timeout(n as u64),
|
||||
};
|
||||
|
||||
self
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,7 +366,11 @@ where
|
||||
A: de::DeserializeOwned,
|
||||
{
|
||||
fn apply_settings(self, settings: &BasicSettings<A>) -> Self {
|
||||
self.apply_settings(&settings.actix)
|
||||
self.try_apply_settings(&settings.actix).unwrap()
|
||||
}
|
||||
|
||||
fn try_apply_settings(self, settings: &BasicSettings<A>) -> AsResult<Self> {
|
||||
self.try_apply_settings(&settings.actix)
|
||||
}
|
||||
}
|
||||
|
||||
@ -349,7 +383,8 @@ mod tests {
|
||||
#[test]
|
||||
fn apply_settings() {
|
||||
let settings = Settings::parse_toml("Server.toml").unwrap();
|
||||
let _ = HttpServer::new(App::new).apply_settings(&settings);
|
||||
let server = HttpServer::new(App::new).try_apply_settings(&settings);
|
||||
assert!(server.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -662,6 +697,7 @@ mod tests {
|
||||
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_tls_enabled() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
@ -670,6 +706,7 @@ mod tests {
|
||||
assert!(settings.actix.tls.enabled);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_with_env_var_tls_enabled() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
@ -683,6 +720,7 @@ mod tests {
|
||||
assert!(settings.actix.tls.enabled);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_tls_certificate() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
@ -701,6 +739,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_with_env_var_tls_certificate() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
@ -723,6 +762,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_tls_private_key() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
@ -741,6 +781,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
#[test]
|
||||
fn override_field_with_env_var_tls_private_key() {
|
||||
let mut settings = Settings::from_default_template();
|
||||
|
@ -43,7 +43,7 @@ impl<'de> de::Deserialize<'de> for Backlog {
|
||||
{
|
||||
struct BacklogVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for BacklogVisitor {
|
||||
impl de::Visitor<'_> for BacklogVisitor {
|
||||
type Value = Backlog;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -68,7 +68,7 @@ impl<'de> de::Deserialize<'de> for KeepAlive {
|
||||
{
|
||||
struct KeepAliveVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for KeepAliveVisitor {
|
||||
impl de::Visitor<'_> for KeepAliveVisitor {
|
||||
type Value = KeepAlive;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnectionRate {
|
||||
{
|
||||
struct MaxConnectionRateVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for MaxConnectionRateVisitor {
|
||||
impl de::Visitor<'_> for MaxConnectionRateVisitor {
|
||||
type Value = MaxConnectionRate;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnections {
|
||||
{
|
||||
struct MaxConnectionsVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for MaxConnectionsVisitor {
|
||||
impl de::Visitor<'_> for MaxConnectionsVisitor {
|
||||
type Value = MaxConnections;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -8,12 +8,15 @@ mod max_connections;
|
||||
mod mode;
|
||||
mod num_workers;
|
||||
mod timeout;
|
||||
#[cfg(feature = "openssl")]
|
||||
mod tls;
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
pub use self::tls::Tls;
|
||||
pub use self::{
|
||||
address::Address, backlog::Backlog, keep_alive::KeepAlive,
|
||||
max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode,
|
||||
num_workers::NumWorkers, timeout::Timeout, tls::Tls,
|
||||
num_workers::NumWorkers, timeout::Timeout,
|
||||
};
|
||||
|
||||
/// Settings types for Actix Web.
|
||||
@ -57,5 +60,6 @@ pub struct ActixSettings {
|
||||
pub shutdown_timeout: Timeout,
|
||||
|
||||
/// TLS (HTTPS) configuration.
|
||||
#[cfg(feature = "openssl")]
|
||||
pub tls: Tls,
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ impl<'de> de::Deserialize<'de> for NumWorkers {
|
||||
{
|
||||
struct NumWorkersVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for NumWorkersVisitor {
|
||||
impl de::Visitor<'_> for NumWorkersVisitor {
|
||||
type Value = NumWorkers;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -71,7 +71,7 @@ impl<'de> de::Deserialize<'de> for Timeout {
|
||||
{
|
||||
struct TimeoutVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for TimeoutVisitor {
|
||||
impl de::Visitor<'_> for TimeoutVisitor {
|
||||
type Value = Timeout;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
@ -1,13 +1,16 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::AsResult;
|
||||
|
||||
/// TLS (HTTPS) configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[doc(alias = "ssl", alias = "https")]
|
||||
pub struct Tls {
|
||||
/// Tru if accepting TLS connections should be enabled.
|
||||
/// True if accepting TLS connections should be enabled.
|
||||
pub enabled: bool,
|
||||
|
||||
/// Path to certificate `.pem` file.
|
||||
@ -16,3 +19,39 @@ pub struct Tls {
|
||||
/// Path to private key `.pem` file.
|
||||
pub private_key: PathBuf,
|
||||
}
|
||||
|
||||
impl Tls {
|
||||
/// Returns an [`SslAcceptorBuilder`] with the configured settings.
|
||||
///
|
||||
/// The result is often used with [`actix_web::HttpServer::bind_openssl()`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::io;
|
||||
/// use actix_settings::{ApplySettings as _, Settings};
|
||||
/// use actix_web::{get, web, App, HttpServer, Responder};
|
||||
///
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> io::Result<()> {
|
||||
/// let settings = Settings::from_default_template();
|
||||
///
|
||||
/// HttpServer::new(|| {
|
||||
/// App::new().route("/", web::to(|| async { "Hello, World!" }))
|
||||
/// })
|
||||
/// .try_apply_settings(&settings)?
|
||||
/// .bind(("127.0.0.1", 8080))?
|
||||
/// .bind_openssl(("127.0.0.1", 8443), settings.actix.tls.get_ssl_acceptor_builder()?)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_ssl_acceptor_builder(&self) -> AsResult<SslAcceptorBuilder> {
|
||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
|
||||
builder.set_certificate_chain_file(&self.certificate)?;
|
||||
builder.set_private_key_file(&self.private_key, SslFiletype::PEM)?;
|
||||
builder.check_private_key()?;
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,8 @@ struct Quoted<'a> {
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl<'a> Quoted<'a> {
|
||||
pub fn new(s: &'a str) -> Quoted<'_> {
|
||||
impl Quoted<'_> {
|
||||
pub fn new(s: &str) -> Quoted<'_> {
|
||||
Quoted {
|
||||
inner: s.split('"').peekable(),
|
||||
state: State::YieldStr,
|
||||
|
@ -2,10 +2,16 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use
|
||||
- Ensure TCP connection is properly shut down when session is dropped.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Add `AggregatedMessage[Stream]` types.
|
||||
- Add `MessageStream::max_frame_size()` setter method.
|
||||
- Add `Session::continuation()` method.
|
||||
- The `Session::text()` method now receives an `impl Into<ByteString>`, making broadcasting text messages more efficient.
|
||||
- Remove type parameters from `Session::{text, binary}()` methods, replacing with equivalent `impl Trait` parameters.
|
||||
- `Session::text()` now receives an `impl Into<ByteString>`, making broadcasting text messages more efficient.
|
||||
- Allow sending continuations via `Session::continuation()`
|
||||
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream.
|
||||
|
||||
## 0.2.5
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "actix-ws"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "WebSockets for Actix Web, without actors"
|
||||
categories = ["web-programming::websocket"]
|
||||
keywords = ["actix", "web", "websocket", "websockets", "http"]
|
||||
keywords = ["actix", "web", "websocket", "websockets", "streaming"]
|
||||
authors = [
|
||||
"asonix <asonix@asonix.dog>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -20,13 +20,12 @@ actix-http = { version = "3", default-features = false, features = ["ws"] }
|
||||
actix-web = { version = "4", default-features = false }
|
||||
bytestring = "1"
|
||||
futures-core = "0.3.17"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
tokio = { version = "1.24", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "4.8"
|
||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
pretty_env_logger = "0.5"
|
||||
tokio = { version = "1", features = ["sync", "rt", "macros"] }
|
||||
tokio = { version = "1.24", features = ["sync", "rt", "macros"] }
|
||||
tracing = "0.1.30"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
|
@ -1,23 +1,21 @@
|
||||
# Actix WS (Next Gen)
|
||||
# `actix-ws`
|
||||
|
||||
> WebSockets for Actix Web, without actors.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
[](https://crates.io/crates/actix-ws)
|
||||
[](https://docs.rs/actix-ws/0.2.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-ws/0.2.0)
|
||||
[](https://docs.rs/actix-ws/0.3.0)
|
||||

|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-ws/0.3.0)
|
||||
[](https://crates.io/crates/actix-ws)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-ws)
|
||||
- [Example Chat Project](https://github.com/actix/examples/tree/master/websockets/chat-actorless)
|
||||
- Minimum Supported Rust Version (MSRV): 1.75
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder};
|
||||
@ -27,7 +25,7 @@ async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result<impl Resp
|
||||
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
while let Some(Ok(msg)) = msg_stream.next().await {
|
||||
while let Some(Ok(msg)) = msg_stream.recv().await {
|
||||
match msg {
|
||||
Message::Ping(bytes) => {
|
||||
if session.pong(&bytes).await.is_err() {
|
||||
@ -60,6 +58,12 @@ async fn main() -> std::io::Result<()> {
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-ws)
|
||||
- [Example Chat Project](https://github.com/actix/examples/tree/master/websockets/chat-actorless)
|
||||
- Minimum Supported Rust Version (MSRV): 1.75
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
use actix_web::{
|
||||
middleware::Logger, web, web::Html, App, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use actix_ws::{Message, Session};
|
||||
use actix_ws::{AggregatedMessage, Session};
|
||||
use bytestring::ByteString;
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt as _};
|
||||
use tokio::sync::Mutex;
|
||||
@ -65,7 +65,10 @@ async fn ws(
|
||||
body: web::Payload,
|
||||
chat: web::Data<Chat>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let (response, mut session, mut stream) = actix_ws::handle(&req, body)?;
|
||||
let (response, mut session, stream) = actix_ws::handle(&req, body)?;
|
||||
|
||||
// increase the maximum allowed frame size to 128KiB and aggregate continuation frames
|
||||
let mut stream = stream.max_frame_size(128 * 1024).aggregate_continuations();
|
||||
|
||||
chat.insert(session.clone()).await;
|
||||
tracing::info!("Inserted session");
|
||||
@ -91,30 +94,29 @@ async fn ws(
|
||||
});
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
while let Some(Ok(msg)) = stream.next().await {
|
||||
while let Some(Ok(msg)) = stream.recv().await {
|
||||
match msg {
|
||||
Message::Ping(bytes) => {
|
||||
AggregatedMessage::Ping(bytes) => {
|
||||
if session.pong(&bytes).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Message::Text(msg) => {
|
||||
tracing::info!("Relaying msg: {msg}");
|
||||
chat.send(msg).await;
|
||||
|
||||
AggregatedMessage::Text(string) => {
|
||||
tracing::info!("Relaying text, {string}");
|
||||
chat.send(string).await;
|
||||
}
|
||||
Message::Close(reason) => {
|
||||
|
||||
AggregatedMessage::Close(reason) => {
|
||||
let _ = session.close(reason).await;
|
||||
tracing::info!("Got close, bailing");
|
||||
return;
|
||||
}
|
||||
Message::Continuation(_) => {
|
||||
let _ = session.close(None).await;
|
||||
tracing::info!("Got continuation, bailing");
|
||||
return;
|
||||
}
|
||||
Message::Pong(_) => {
|
||||
|
||||
AggregatedMessage::Pong(_) => {
|
||||
*alive.lock().await = Instant::now();
|
||||
}
|
||||
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
216
actix-ws/src/aggregated.rs
Normal file
216
actix-ws/src/aggregated.rs
Normal file
@ -0,0 +1,216 @@
|
||||
//! WebSocket stream for aggregating continuation frames.
|
||||
|
||||
use std::{
|
||||
future::poll_fn,
|
||||
io, mem,
|
||||
pin::Pin,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::ws::{CloseReason, Item, Message, ProtocolError};
|
||||
use actix_web::web::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::MessageStream;
|
||||
|
||||
pub(crate) enum ContinuationKind {
|
||||
Text,
|
||||
Binary,
|
||||
}
|
||||
|
||||
/// WebSocket message with any continuations aggregated together.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AggregatedMessage {
|
||||
/// Text message.
|
||||
Text(ByteString),
|
||||
|
||||
/// Binary message.
|
||||
Binary(Bytes),
|
||||
|
||||
/// Ping message.
|
||||
Ping(Bytes),
|
||||
|
||||
/// Pong message.
|
||||
Pong(Bytes),
|
||||
|
||||
/// Close message with optional reason.
|
||||
Close(Option<CloseReason>),
|
||||
}
|
||||
|
||||
/// Stream of messages from a WebSocket client, with continuations aggregated.
|
||||
pub struct AggregatedMessageStream {
|
||||
stream: MessageStream,
|
||||
current_size: usize,
|
||||
max_size: usize,
|
||||
continuations: Vec<Bytes>,
|
||||
continuation_kind: ContinuationKind,
|
||||
}
|
||||
|
||||
impl AggregatedMessageStream {
|
||||
#[must_use]
|
||||
pub(crate) fn new(stream: MessageStream) -> Self {
|
||||
AggregatedMessageStream {
|
||||
stream,
|
||||
current_size: 0,
|
||||
max_size: 1024 * 1024,
|
||||
continuations: Vec::new(),
|
||||
continuation_kind: ContinuationKind::Binary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum allowed size for aggregated continuations, in bytes.
|
||||
///
|
||||
/// By default, up to 1 MiB is allowed.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use actix_ws::AggregatedMessageStream;
|
||||
/// # async fn test(stream: AggregatedMessageStream) {
|
||||
/// // increase the allowed size from 1MB to 8MB
|
||||
/// let mut stream = stream.max_continuation_size(8 * 1024 * 1024);
|
||||
///
|
||||
/// while let Some(Ok(msg)) = stream.recv().await {
|
||||
/// // handle message
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn max_continuation_size(mut self, max_size: usize) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Waits for the next item from the aggregated message stream.
|
||||
///
|
||||
/// This is a convenience for calling the [`Stream`](Stream::poll_next()) implementation.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use actix_ws::AggregatedMessageStream;
|
||||
/// # async fn test(mut stream: AggregatedMessageStream) {
|
||||
/// while let Some(Ok(msg)) = stream.recv().await {
|
||||
/// // handle message
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub async fn recv(&mut self) -> Option<<Self as Stream>::Item> {
|
||||
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
|
||||
}
|
||||
}
|
||||
|
||||
fn size_error() -> Poll<Option<Result<AggregatedMessage, ProtocolError>>> {
|
||||
Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(
|
||||
"Exceeded maximum continuation size",
|
||||
)))))
|
||||
}
|
||||
|
||||
impl Stream for AggregatedMessageStream {
|
||||
type Item = Result<AggregatedMessage, ProtocolError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
let Some(msg) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) else {
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::Continuation(item) => match item {
|
||||
Item::FirstText(bytes) => {
|
||||
this.continuation_kind = ContinuationKind::Text;
|
||||
this.current_size += bytes.len();
|
||||
|
||||
if this.current_size > this.max_size {
|
||||
this.continuations.clear();
|
||||
return size_error();
|
||||
}
|
||||
|
||||
this.continuations.push(bytes);
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
Item::FirstBinary(bytes) => {
|
||||
this.continuation_kind = ContinuationKind::Binary;
|
||||
this.current_size += bytes.len();
|
||||
|
||||
if this.current_size > this.max_size {
|
||||
this.continuations.clear();
|
||||
return size_error();
|
||||
}
|
||||
|
||||
this.continuations.push(bytes);
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
Item::Continue(bytes) => {
|
||||
this.current_size += bytes.len();
|
||||
|
||||
if this.current_size > this.max_size {
|
||||
this.continuations.clear();
|
||||
return size_error();
|
||||
}
|
||||
|
||||
this.continuations.push(bytes);
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
Item::Last(bytes) => {
|
||||
this.current_size += bytes.len();
|
||||
|
||||
if this.current_size > this.max_size {
|
||||
// reset current_size, as this is the last message for
|
||||
// the current continuation
|
||||
this.current_size = 0;
|
||||
this.continuations.clear();
|
||||
|
||||
return size_error();
|
||||
}
|
||||
|
||||
this.continuations.push(bytes);
|
||||
let bytes = collect(&mut this.continuations);
|
||||
|
||||
this.current_size = 0;
|
||||
|
||||
match this.continuation_kind {
|
||||
ContinuationKind::Text => {
|
||||
Poll::Ready(Some(match ByteString::try_from(bytes) {
|
||||
Ok(bytestring) => Ok(AggregatedMessage::Text(bytestring)),
|
||||
Err(err) => Err(ProtocolError::Io(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
err.to_string(),
|
||||
))),
|
||||
}))
|
||||
}
|
||||
ContinuationKind::Binary => {
|
||||
Poll::Ready(Some(Ok(AggregatedMessage::Binary(bytes))))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Message::Text(text) => Poll::Ready(Some(Ok(AggregatedMessage::Text(text)))),
|
||||
Message::Binary(binary) => Poll::Ready(Some(Ok(AggregatedMessage::Binary(binary)))),
|
||||
Message::Ping(ping) => Poll::Ready(Some(Ok(AggregatedMessage::Ping(ping)))),
|
||||
Message::Pong(pong) => Poll::Ready(Some(Ok(AggregatedMessage::Pong(pong)))),
|
||||
Message::Close(close) => Poll::Ready(Some(Ok(AggregatedMessage::Close(close)))),
|
||||
|
||||
Message::Nop => unreachable!("MessageStream should not produce no-ops"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect(continuations: &mut Vec<Bytes>) -> Bytes {
|
||||
let continuations = mem::take(continuations);
|
||||
let total_len = continuations.iter().map(|b| b.len()).sum();
|
||||
|
||||
let mut buf = BytesMut::with_capacity(total_len);
|
||||
|
||||
for chunk in continuations {
|
||||
buf.extend(chunk);
|
||||
}
|
||||
|
||||
buf.freeze()
|
||||
}
|
@ -15,17 +15,20 @@ use actix_http::{
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use tokio::sync::mpsc::channel;
|
||||
|
||||
mod fut;
|
||||
mod aggregated;
|
||||
mod session;
|
||||
mod stream;
|
||||
|
||||
pub use self::{
|
||||
fut::{MessageStream, StreamingBody},
|
||||
aggregated::{AggregatedMessage, AggregatedMessageStream},
|
||||
session::{Closed, Session},
|
||||
stream::{MessageStream, StreamingBody},
|
||||
};
|
||||
|
||||
/// Begin handling websocket traffic
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::io;
|
||||
/// use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder};
|
||||
/// use actix_ws::Message;
|
||||
/// use futures_util::StreamExt as _;
|
||||
@ -41,6 +44,7 @@ pub use self::{
|
||||
/// return;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Message::Text(msg) => println!("Got text: {msg}"),
|
||||
/// _ => break,
|
||||
/// }
|
||||
@ -53,17 +57,15 @@ pub use self::{
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main(flavor = "current_thread")]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// async fn main() -> io::Result<()> {
|
||||
/// HttpServer::new(move || {
|
||||
/// App::new()
|
||||
/// .wrap(Logger::default())
|
||||
/// .route("/ws", web::get().to(ws))
|
||||
/// .wrap(Logger::default())
|
||||
/// })
|
||||
/// .bind("127.0.0.1:8080")?
|
||||
/// .bind(("127.0.0.1", 8080))?
|
||||
/// .run()
|
||||
/// .await?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// .await
|
||||
/// }
|
||||
/// ```
|
||||
pub fn handle(
|
||||
|
@ -15,25 +15,15 @@ use actix_web::{
|
||||
web::{Bytes, BytesMut},
|
||||
Error,
|
||||
};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::stream::Stream;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
|
||||
/// A response body for Websocket HTTP Requests
|
||||
use crate::AggregatedMessageStream;
|
||||
|
||||
/// Response body for a WebSocket.
|
||||
pub struct StreamingBody {
|
||||
session_rx: Receiver<Message>,
|
||||
|
||||
messages: VecDeque<Message>,
|
||||
buf: BytesMut,
|
||||
codec: Codec,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
/// A stream of Messages from a websocket client
|
||||
///
|
||||
/// Messages can be accessed via the stream's `.next()` method
|
||||
pub struct MessageStream {
|
||||
payload: Payload,
|
||||
|
||||
messages: VecDeque<Message>,
|
||||
buf: BytesMut,
|
||||
codec: Codec,
|
||||
@ -52,6 +42,16 @@ impl StreamingBody {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream of messages from a WebSocket client.
|
||||
pub struct MessageStream {
|
||||
payload: Payload,
|
||||
|
||||
messages: VecDeque<Message>,
|
||||
buf: BytesMut,
|
||||
codec: Codec,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl MessageStream {
|
||||
pub(super) fn new(payload: Payload) -> Self {
|
||||
MessageStream {
|
||||
@ -63,7 +63,40 @@ impl MessageStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the next item from the message stream
|
||||
/// Sets the maximum permitted size for received WebSocket frames, in bytes.
|
||||
///
|
||||
/// By default, up to 64KiB is allowed.
|
||||
///
|
||||
/// Any received frames larger than the permitted value will return
|
||||
/// `Err(ProtocolError::Overflow)` instead.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use actix_ws::MessageStream;
|
||||
/// # fn test(stream: MessageStream) {
|
||||
/// // increase permitted frame size from 64KB to 1MB
|
||||
/// let stream = stream.max_frame_size(1024 * 1024);
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn max_frame_size(mut self, max_size: usize) -> Self {
|
||||
self.codec = self.codec.max_size(max_size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a stream wrapper that collects continuation frames into their equivalent aggregated
|
||||
/// forms, i.e., binary or text.
|
||||
///
|
||||
/// By default, continuations will be aggregated up to 1MiB in size (customizable with
|
||||
/// [`AggregatedMessageStream::max_continuation_size()`]). The stream implementation returns an
|
||||
/// error if this size is exceeded.
|
||||
#[must_use]
|
||||
pub fn aggregate_continuations(self) -> AggregatedMessageStream {
|
||||
AggregatedMessageStream::new(self)
|
||||
}
|
||||
|
||||
/// Waits for the next item from the message stream
|
||||
///
|
||||
/// This is a convenience for calling the [`Stream`](Stream::poll_next()) implementation.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use actix_ws::MessageStream;
|
||||
@ -73,6 +106,7 @@ impl MessageStream {
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub async fn recv(&mut self) -> Option<Result<Message, ProtocolError>> {
|
||||
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
|
||||
}
|
||||
@ -111,6 +145,10 @@ impl Stream for StreamingBody {
|
||||
return Poll::Ready(Some(Ok(mem::take(&mut this.buf).freeze())));
|
||||
}
|
||||
|
||||
if this.closing {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@ -135,11 +173,8 @@ impl Stream for MessageStream {
|
||||
Poll::Ready(Some(Ok(bytes))) => {
|
||||
this.buf.extend_from_slice(&bytes);
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
e.to_string(),
|
||||
)))));
|
||||
Poll::Ready(Some(Err(err))) => {
|
||||
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(err)))));
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
this.closing = true;
|
||||
@ -154,12 +189,11 @@ impl Stream for MessageStream {
|
||||
while let Some(frame) = this.codec.decode(&mut this.buf)? {
|
||||
let message = match frame {
|
||||
Frame::Text(bytes) => {
|
||||
let s = std::str::from_utf8(&bytes)
|
||||
.map_err(|e| {
|
||||
ProtocolError::Io(io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
ByteString::try_from(bytes)
|
||||
.map(Message::Text)
|
||||
.map_err(|err| {
|
||||
ProtocolError::Io(io::Error::new(io::ErrorKind::InvalidData, err))
|
||||
})?
|
||||
.to_string();
|
||||
Message::Text(s.into())
|
||||
}
|
||||
Frame::Binary(bytes) => Message::Binary(bytes),
|
||||
Frame::Ping(bytes) => Message::Ping(bytes),
|
36
justfile
36
justfile
@ -19,24 +19,44 @@ msrv_rustup := "+" + msrv
|
||||
clippy:
|
||||
cargo {{ toolchain }} clippy --workspace --all-targets --all-features
|
||||
|
||||
# Format workspace.
|
||||
# Format project.
|
||||
[group("lint")]
|
||||
fmt: update-readmes
|
||||
cargo +nightly fmt
|
||||
fd --hidden --extension=yml --extension=md --exec-batch npx -y prettier --write
|
||||
fd --type=file --hidden --extension=yml --extension=md --exec-batch npx -y prettier --write
|
||||
|
||||
# Check project.
|
||||
[group("lint")]
|
||||
check:
|
||||
cargo +nightly fmt -- --check
|
||||
fd --type=file --hidden --extension=yml --extension=md --exec-batch npx -y prettier --check
|
||||
|
||||
# Update READMEs from crate root documentation.
|
||||
[group("lint")]
|
||||
update-readmes:
|
||||
cd ./actix-cors && cargo rdme --force
|
||||
cd ./actix-session && cargo rdme --force
|
||||
cd ./actix-identity && cargo rdme --force
|
||||
fd README.md --exec-batch -- npx -y prettier --write
|
||||
cd ./actix-session && cargo rdme --force
|
||||
fd README.md --exec-batch npx -y prettier --write
|
||||
|
||||
# Test workspace code.
|
||||
[group("test")]
|
||||
test:
|
||||
cargo {{ toolchain }} nextest run --workspace --all-features
|
||||
cargo {{ toolchain }} test --doc --workspace --all-features
|
||||
|
||||
# Downgrade dev-dependencies necessary to run MSRV checks/tests.
|
||||
[private]
|
||||
downgrade-for-msrv:
|
||||
cargo update -p=native-tls --precise=0.2.13
|
||||
cargo update -p=litemap --precise=0.7.4
|
||||
cargo update -p=zerofrom --precise=0.1.5
|
||||
|
||||
# Test workspace using MSRV.
|
||||
[group("test")]
|
||||
test-msrv:
|
||||
@just downgrade-for-msrv
|
||||
@just toolchain={{ msrv_rustup }} test
|
||||
|
||||
# Test workspace code and docs.
|
||||
[group("test")]
|
||||
@ -65,15 +85,17 @@ test-docs:
|
||||
# Document crates in workspace.
|
||||
[group("docs")]
|
||||
doc *args: && doc-set-workspace-crates
|
||||
rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
|
||||
RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace --all-features {{ args }}
|
||||
|
||||
[private]
|
||||
[group("docs")]
|
||||
[private]
|
||||
doc-set-workspace-crates:
|
||||
#!/usr/bin/env bash
|
||||
(
|
||||
echo "window.ALL_CRATES ="
|
||||
cargo metadata --format-version=1 | jq '[.packages[] | select(.source == null) | .name]'
|
||||
echo "window.ALL_CRATES = "
|
||||
cargo metadata --format-version=1 \
|
||||
| jq '[.packages[] | select(.source == null) | .targets | map(select(.doc) | .name)] | flatten'
|
||||
echo ";"
|
||||
) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user