1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-04-22 18:04:52 +02:00

Compare commits

..

No commits in common. "master" and "identity-v0.7.1" have entirely different histories.

90 changed files with 1264 additions and 51632 deletions

View File

@ -1,10 +1,10 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
- package-ecosystem: cargo - package-ecosystem: cargo
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

View File

@ -31,12 +31,12 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install cargo-hack, cargo-ci-cache-clean - name: Install cargo-hack and cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@v2.27.10
with: with:
tool: cargo-hack,cargo-ci-cache-clean tool: cargo-hack,cargo-ci-cache-clean
@ -71,22 +71,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - 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) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install cargo-hack and cargo-ci-cache-clean - name: Install cargo-hack and cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@v2.27.10
with: with:
tool: cargo-hack,cargo-ci-cache-clean tool: cargo-hack,cargo-ci-cache-clean
@ -101,7 +92,7 @@ jobs:
- name: tests - name: tests
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation -- --nocapture run: cargo ci-test --exclude=actix-redis --exclude=actix-session --exclude=actix-limitation -- --nocapture
- name: CI cache clean - name: CI cache clean
run: cargo-ci-cache-clean run: cargo-ci-cache-clean

View File

@ -44,18 +44,19 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (${{ matrix.version.name }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install cargo-hack and cargo-ci-cache-clean, just - name: Install cargo-hack and cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@v2.27.10
with: with:
tool: cargo-hack,cargo-ci-cache-clean,just tool: cargo-hack,cargo-ci-cache-clean
- name: workaround MSRV issues # - name: workaround MSRV issues
if: matrix.version.name == 'msrv' # if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv # run: |
# cargo update -p=time:0.3.20 --precise=0.3.16
- name: check minimal - name: check minimal
run: cargo ci-min run: cargo ci-min
@ -91,28 +92,20 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - 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 }}) - name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version.version }}
- name: Install cargo-hack, cargo-ci-cache-clean, just - name: Install cargo-hack and cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@v2.27.10
with: with:
tool: cargo-hack,cargo-ci-cache-clean,just tool: cargo-hack,cargo-ci-cache-clean
- name: workaround MSRV issues # - name: workaround MSRV issues
if: matrix.version.name == 'msrv' # if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv # run: |
# cargo update -p=time:0.3.20 --precise=0.3.16
- name: check minimal - name: check minimal
run: cargo ci-min run: cargo ci-min
@ -125,29 +118,22 @@ jobs:
- name: tests - name: tests
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation run: cargo ci-test --exclude=actix-redis --exclude=actix-session --exclude=actix-limitation
- name: CI cache clean - name: CI cache clean
run: cargo-ci-cache-clean run: cargo-ci-cache-clean
doc_tests: doc_tests:
name: Documentation Tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly toolchain: nightly
- name: Install just - name: doc tests
uses: taiki-e/install-action@v2.49.42 timeout-minutes: 40
with: run: cargo ci-doctest -- --nocapture
tool: just
- name: Test docs
run: just test-docs
- name: Build docs
run: just doc

View File

@ -25,21 +25,20 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly
components: llvm-tools-preview components: llvm-tools-preview
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@v2.27.10
with: with:
tool: just,cargo-llvm-cov,cargo-nextest tool: cargo-llvm-cov
- name: Generate code coverage - name: Generate code coverage
run: just test-coverage-codecov run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v5.4.0 uses: codecov/codecov-action@v4.0.2
with: with:
files: codecov.json files: codecov.json
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
components: clippy components: clippy
@ -46,3 +46,32 @@ jobs:
clippy_flags: >- clippy_flags: >-
--workspace --all-features --tests --examples --bins -- --workspace --all-features --tests --examples --bins --
-A unknown_lints -D clippy::todo -D clippy::dbg_macro -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.8.0
with:
toolchain: nightly
- name: Install cargo-public-api
uses: taiki-e/cache-cargo-install-action@v1.3.0
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

36
.github/workflows/upload-doc.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Upload Documentation
on:
push:
branches: [master]
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly
- name: Build Docs
run: cargo doc --no-deps --workspace --all-features
- name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_cors/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
folder: target/doc
branch: gh-pages

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
/target /target
**/*.rs.bk **/*.rs.bk
Cargo.lock
guide/build/ guide/build/
/gh-pages /gh-pages

3292
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ members = [
"actix-identity", "actix-identity",
"actix-limitation", "actix-limitation",
"actix-protobuf", "actix-protobuf",
"actix-redis",
"actix-session", "actix-session",
"actix-settings", "actix-settings",
"actix-web-httpauth", "actix-web-httpauth",
@ -12,22 +13,17 @@ members = [
] ]
[workspace.package] [workspace.package]
repository = "https://github.com/actix/actix-extras"
homepage = "https://actix.rs" homepage = "https://actix.rs"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
[workspace.lints.rust]
rust-2018-idioms = { level = "deny" }
nonstandard-style = { level = "deny" }
future-incompatible = { level = "deny" }
[patch.crates-io] [patch.crates-io]
actix-cors = { path = "./actix-cors" } actix-cors = { path = "./actix-cors" }
actix-identity = { path = "./actix-identity" } actix-identity = { path = "./actix-identity" }
actix-limitation = { path = "./actix-limitation" } actix-limitation = { path = "./actix-limitation" }
actix-protobuf = { path = "./actix-protobuf" } actix-protobuf = { path = "./actix-protobuf" }
actix-redis = { path = "./actix-redis" }
actix-session = { path = "./actix-session" } actix-session = { path = "./actix-session" }
actix-settings = { path = "./actix-settings" } actix-settings = { path = "./actix-settings" }
actix-web-httpauth = { path = "./actix-web-httpauth" } actix-web-httpauth = { path = "./actix-web-httpauth" }

View File

@ -19,6 +19,7 @@
| [actix-identity] | [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity) [![dependency status](https://deps.rs/crate/actix-identity/latest/status.svg)](https://deps.rs/crate/actix-identity) | Identity management. | | [actix-identity] | [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity) [![dependency status](https://deps.rs/crate/actix-identity/latest/status.svg)](https://deps.rs/crate/actix-identity) | Identity management. |
| [actix-limitation] | [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation) [![dependency status](https://deps.rs/crate/actix-limitation/latest/status.svg)](https://deps.rs/crate/actix-limitation) | Rate-limiting using a fixed window counter for arbitrary keys, backed by Redis. | | [actix-limitation] | [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation) [![dependency status](https://deps.rs/crate/actix-limitation/latest/status.svg)](https://deps.rs/crate/actix-limitation) | Rate-limiting using a fixed window counter for arbitrary keys, backed by Redis. |
| [actix-protobuf] | [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf) [![dependency status](https://deps.rs/crate/actix-protobuf/latest/status.svg)](https://deps.rs/crate/actix-protobuf) | Protobuf payload extractor. | | [actix-protobuf] | [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf) [![dependency status](https://deps.rs/crate/actix-protobuf/latest/status.svg)](https://deps.rs/crate/actix-protobuf) | Protobuf payload extractor. |
| [actix-redis] | [![crates.io](https://img.shields.io/crates/v/actix-redis?label=latest)](https://crates.io/crates/actix-redis) [![dependency status](https://deps.rs/crate/actix-redis/latest/status.svg)](https://deps.rs/crate/actix-redis) | Actor-based Redis client. |
| [actix-session] | [![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session) [![dependency status](https://deps.rs/crate/actix-session/latest/status.svg)](https://deps.rs/crate/actix-session) | Session management. | | [actix-session] | [![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session) [![dependency status](https://deps.rs/crate/actix-session/latest/status.svg)](https://deps.rs/crate/actix-session) | Session management. |
| [actix-settings] | [![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings) [![dependency status](https://deps.rs/crate/actix-settings/latest/status.svg)](https://deps.rs/crate/actix-settings) | Easily manage Actix Web's settings from a TOML file and environment variables. | | [actix-settings] | [![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings) [![dependency status](https://deps.rs/crate/actix-settings/latest/status.svg)](https://deps.rs/crate/actix-settings) | Easily manage Actix Web's settings from a TOML file and environment variables. |
| [actix-web-httpauth] | [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth) [![dependency status](https://deps.rs/crate/actix-web-httpauth/latest/status.svg)](https://deps.rs/crate/actix-web-httpauth) | HTTP authentication schemes. | | [actix-web-httpauth] | [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth) [![dependency status](https://deps.rs/crate/actix-web-httpauth/latest/status.svg)](https://deps.rs/crate/actix-web-httpauth) | HTTP authentication schemes. |
@ -33,7 +34,8 @@ These crates are provided by the community.
| Crate | | | | Crate | | |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| [actix-web-lab] | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)][actix-web-lab] [![dependency status](https://deps.rs/crate/actix-web-lab/latest/status.svg)](https://deps.rs/crate/actix-web-lab) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. | | [actix-web-lab] | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)][actix-web-lab] [![dependency status](https://deps.rs/crate/actix-web-lab/latest/status.svg)](https://deps.rs/crate/actix-web-lab) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. |
| [actix-form-data] | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)][actix-form-data] [![dependency status](https://deps.rs/crate/actix-form-data/latest/status.svg)](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams. | | [actix-multipart-extract] | [![crates.io](https://img.shields.io/crates/v/actix-multipart-extract?label=latest)][actix-multipart-extract] [![dependency status](https://deps.rs/crate/actix-multipart-extract/latest/status.svg)](https://deps.rs/crate/actix-multipart-extract) | Better multipart form support for Actix Web. |
| [actix-form-data] | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)][actix-form-data] [![dependency status](https://deps.rs/crate/actix-form-data/latest/status.svg)](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams |
| [actix-governor] | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)][actix-governor] [![dependency status](https://deps.rs/crate/actix-governor/latest/status.svg)](https://deps.rs/crate/actix-governor) | Rate-limiting backed by governor. | | [actix-governor] | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)][actix-governor] [![dependency status](https://deps.rs/crate/actix-governor/latest/status.svg)](https://deps.rs/crate/actix-governor) | Rate-limiting backed by governor. |
| [actix-casbin] | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)][actix-casbin] [![dependency status](https://deps.rs/crate/actix-casbin/latest/status.svg)](https://deps.rs/crate/actix-casbin) | Authorization library that supports access control models like ACL, RBAC & ABAC. | | [actix-casbin] | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)][actix-casbin] [![dependency status](https://deps.rs/crate/actix-casbin/latest/status.svg)](https://deps.rs/crate/actix-casbin) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
| [actix-ip-filter] | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)][actix-ip-filter] [![dependency status](https://deps.rs/crate/actix-ip-filter/latest/status.svg)](https://deps.rs/crate/actix-ip-filter) | IP address filter. Supports glob patterns. | | [actix-ip-filter] | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)][actix-ip-filter] [![dependency status](https://deps.rs/crate/actix-ip-filter/latest/status.svg)](https://deps.rs/crate/actix-ip-filter) | IP address filter. Supports glob patterns. |
@ -44,13 +46,10 @@ These crates are provided by the community.
| [awmp] | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)][awmp] [![dependency status](https://deps.rs/crate/awmp/latest/status.svg)](https://deps.rs/crate/awmp) | An easy to use wrapper around multipart fields for Actix Web. | | [awmp] | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)][awmp] [![dependency status](https://deps.rs/crate/awmp/latest/status.svg)](https://deps.rs/crate/awmp) | An easy to use wrapper around multipart fields for Actix Web. |
| [tracing-actix-web] | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)][tracing-actix-web] [![dependency status](https://deps.rs/crate/tracing-actix-web/latest/status.svg)](https://deps.rs/crate/tracing-actix-web) | A middleware to collect telemetry data from applications built on top of the Actix Web framework. | | [tracing-actix-web] | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)][tracing-actix-web] [![dependency status](https://deps.rs/crate/tracing-actix-web/latest/status.svg)](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] | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)][actix-hash] [![dependency status](https://deps.rs/crate/actix-hash/latest/status.svg)](https://deps.rs/crate/actix-hash) | Hashing utilities for Actix Web. | | [actix-hash] | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)][actix-hash] [![dependency status](https://deps.rs/crate/actix-hash/latest/status.svg)](https://deps.rs/crate/actix-hash) | Hashing utilities for Actix Web. |
| [actix-bincode] | ![crates.io](https://img.shields.io/crates/v/actix-bincode?label=latest) [![dependency status](https://deps.rs/crate/actix-bincode/latest/status.svg)](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web. | | [actix-bincode] | ![crates.io](https://img.shields.io/crates/v/actix-bincode?label=latest) [![dependency status](https://deps.rs/crate/actix-bincode/latest/status.svg)](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web |
| [sentinel-actix] | ![crates.io](https://img.shields.io/crates/v/sentinel-actix?label=latest) [![dependency status](https://deps.rs/crate/sentinel-actix/latest/status.svg)](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web. | | [sentinel-actix] | ![crates.io](https://img.shields.io/crates/v/sentinel-actix?label=latest) [![dependency status](https://deps.rs/crate/sentinel-actix/latest/status.svg)](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web |
| [actix-telepathy] | ![crates.io](https://img.shields.io/crates/v/actix-telepathy?label=latest) [![dependency status](https://deps.rs/crate/actix-telepathy/latest/status.svg)](https://deps.rs/crate/actix-telepathy) | Build distributed applications with `RemoteActors` and `RemoteMessages`. | | [actix-telepathy] | ![crates.io](https://img.shields.io/crates/v/actix-telepathy?label=latest) [![dependency status](https://deps.rs/crate/actix-telepathy/latest/status.svg)](https://deps.rs/crate/actix-telepathy) | Build distributed applications with `RemoteActors` and `RemoteMessages`. |
| [apistos] | ![crates.io](https://img.shields.io/crates/v/apistos?label=latest) [![dependency status](https://deps.rs/crate/apistos/latest/status.svg)](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web. | | [apistos] | ![crates.io](https://img.shields.io/crates/v/apistos?label=latest) [![dependency status](https://deps.rs/crate/apistos/latest/status.svg)](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web |
| [actix-web-validation] | ![crates.io](https://img.shields.io/crates/v/actix-web-validation?label=latest) [![dependency status](https://deps.rs/crate/actix-web-validation/latest/status.svg)](https://deps.rs/crate/actix-web-validation) | Request validation for Actix Web. |
| [actix-jwt-cookies] | ![crates.io](https://img.shields.io/crates/v/actix-jwt-cookies?label=latest) [![dependency status](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies/status.svg)](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies?path=%2F) | Store your data in encrypted cookies and get it elegantly. |
| [actix-ws-broadcaster] | ![crates.io](https://img.shields.io/crates/v/actix-ws-broadcaster?label=latest) [![dependency status](https://deps.rs/repo/github/Necoo33/actix-ws-broadcaster/status.svg?path=%2F)](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. To add a crate to this list, submit a pull request.
@ -63,6 +62,7 @@ To add a crate to this list, submit a pull request.
[actix-identity]: ./actix-identity [actix-identity]: ./actix-identity
[actix-limitation]: ./actix-limitation [actix-limitation]: ./actix-limitation
[actix-protobuf]: ./actix-protobuf [actix-protobuf]: ./actix-protobuf
[actix-redis]: ./actix-redis
[actix-session]: ./actix-session [actix-session]: ./actix-session
[actix-settings]: ./actix-settings [actix-settings]: ./actix-settings
[actix-web-httpauth]: ./actix-web-httpauth [actix-web-httpauth]: ./actix-web-httpauth
@ -82,9 +82,5 @@ To add a crate to this list, submit a pull request.
[actix-hash]: https://crates.io/crates/actix-hash [actix-hash]: https://crates.io/crates/actix-hash
[actix-bincode]: https://crates.io/crates/actix-bincode [actix-bincode]: https://crates.io/crates/actix-bincode
[sentinel-actix]: https://crates.io/crates/sentinel-actix [sentinel-actix]: https://crates.io/crates/sentinel-actix
[actix-telepathy]: https://crates.io/crates/actix-telepathy [actix-telepathy]: https://github.com/wenig/actix-telepathy
[actix-web-validation]: https://crates.io/crates/actix-web-validation [apistos]: https://github.com/netwo-io/apistos
[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

View File

@ -2,10 +2,6 @@
## Unreleased ## Unreleased
## 0.7.1
- Implement `PartialEq` for `Cors` allowing for better testing.
## 0.7.0 ## 0.7.0
- `Cors` is now marked `#[must_use]`. - `Cors` is now marked `#[must_use]`.

View File

@ -1,14 +1,14 @@
[package] [package]
name = "actix-cors" name = "actix-cors"
version = "0.7.1" version = "0.7.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web" description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web"
keywords = ["actix", "cors", "web", "security", "crossorigin"] keywords = ["actix", "cors", "web", "security", "crossorigin"]
repository.workspace = true homepage = "https://actix.rs"
homepage.workspace = true repository = "https://github.com/actix/actix-extras/tree/master/actix-cors"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -24,7 +24,7 @@ draft-private-network-access = []
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display", "error"] } derive_more = "0.99.7"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
log = "0.4" log = "0.4"
once_cell = "1" once_cell = "1"
@ -34,6 +34,3 @@ smallvec = "1"
actix-web = { version = "4", default-features = false, features = ["macros"] } actix-web = { version = "4", default-features = false, features = ["macros"] }
env_logger = "0.11" env_logger = "0.11"
regex = "1.4" regex = "1.4"
[lints]
workspace = true

View File

@ -3,11 +3,11 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors) [![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors)
[![Documentation](https://docs.rs/actix-cors/badge.svg?version=0.7.1)](https://docs.rs/actix-cors/0.7.1) [![Documentation](https://docs.rs/actix-cors/badge.svg?version=0.7.0)](https://docs.rs/actix-cors/0.7.0)
![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-cors.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-cors.svg)
<br /> <br />
[![Dependency Status](https://deps.rs/crate/actix-cors/0.7.1/status.svg)](https://deps.rs/crate/actix-cors/0.7.1) [![Dependency Status](https://deps.rs/crate/actix-cors/0.7.0/status.svg)](https://deps.rs/crate/actix-cors/0.7.0)
[![Download](https://img.shields.io/crates/d/actix-cors.svg)](https://crates.io/crates/actix-cors) [![Download](https://img.shields.io/crates/d/actix-cors.svg)](https://crates.io/crates/actix-cors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -214,7 +214,7 @@ impl Cors {
/// See [`Cors::allowed_methods`] for more info on allowed methods. /// See [`Cors::allowed_methods`] for more info on allowed methods.
pub fn allow_any_method(mut self) -> Cors { pub fn allow_any_method(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
ALL_METHODS_SET.clone_into(&mut cors.allowed_methods); cors.allowed_methods = ALL_METHODS_SET.clone();
} }
self self
@ -608,19 +608,6 @@ where
.unwrap() .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)] #[cfg(test)]
mod test { mod test {
use std::convert::Infallible; use std::convert::Infallible;
@ -692,11 +679,4 @@ mod test {
Cors::default().new_transform(srv).await.unwrap(); 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());
}
} }

View File

@ -1,40 +1,40 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use derive_more::derive::{Display, Error}; use derive_more::{Display, Error};
/// Errors that can occur when processing CORS guarded requests. /// Errors that can occur when processing CORS guarded requests.
#[derive(Debug, Clone, Display, Error)] #[derive(Debug, Clone, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum CorsError { pub enum CorsError {
/// Allowed origin argument must not be wildcard (`*`). /// Allowed origin argument must not be wildcard (`*`).
#[display("`allowed_origin` argument must not be wildcard (`*`)")] #[display(fmt = "`allowed_origin` argument must not be wildcard (`*`)")]
WildcardOrigin, WildcardOrigin,
/// Request header `Origin` is required but was not provided. /// Request header `Origin` is required but was not provided.
#[display("Request header `Origin` is required but was not provided")] #[display(fmt = "Request header `Origin` is required but was not provided")]
MissingOrigin, MissingOrigin,
/// Request header `Access-Control-Request-Method` is required but is missing. /// Request header `Access-Control-Request-Method` is required but is missing.
#[display("Request header `Access-Control-Request-Method` is required but is missing")] #[display(fmt = "Request header `Access-Control-Request-Method` is required but is missing")]
MissingRequestMethod, MissingRequestMethod,
/// Request header `Access-Control-Request-Method` has an invalid value. /// Request header `Access-Control-Request-Method` has an invalid value.
#[display("Request header `Access-Control-Request-Method` has an invalid value")] #[display(fmt = "Request header `Access-Control-Request-Method` has an invalid value")]
BadRequestMethod, BadRequestMethod,
/// Request header `Access-Control-Request-Headers` has an invalid value. /// Request header `Access-Control-Request-Headers` has an invalid value.
#[display("Request header `Access-Control-Request-Headers` has an invalid value")] #[display(fmt = "Request header `Access-Control-Request-Headers` has an invalid value")]
BadRequestHeaders, BadRequestHeaders,
/// Origin is not allowed to make this request. /// Origin is not allowed to make this request.
#[display("Origin is not allowed to make this request")] #[display(fmt = "Origin is not allowed to make this request")]
OriginNotAllowed, OriginNotAllowed,
/// Request method is not allowed. /// Request method is not allowed.
#[display("Requested method is not allowed")] #[display(fmt = "Requested method is not allowed")]
MethodNotAllowed, MethodNotAllowed,
/// One or more request headers are not allowed. /// One or more request headers are not allowed.
#[display("One or more request headers are not allowed")] #[display(fmt = "One or more request headers are not allowed")]
HeadersNotAllowed, HeadersNotAllowed,
} }

View File

@ -27,12 +27,6 @@ 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 { impl fmt::Debug for OriginFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("origin_fn") f.write_str("origin_fn")
@ -46,7 +40,7 @@ pub(crate) fn header_value_try_into_method(hdr: &HeaderValue) -> Option<Method>
.and_then(|meth| Method::try_from(meth).ok()) .and_then(|meth| Method::try_from(meth).ok())
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone)]
pub(crate) struct Inner { pub(crate) struct Inner {
pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>, pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>,
pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>, pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>,

View File

@ -49,6 +49,7 @@
//! [Private Network Access]: https://wicg.github.io/private-network-access //! [Private Network Access]: https://wicg.github.io/private-network-access
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)] #![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]

View File

@ -2,10 +2,6 @@
## Unreleased ## Unreleased
## 0.8.0
- Update `actix-session` dependency to `0.10`.
## 0.7.1 ## 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. - 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.

View File

@ -1,14 +1,14 @@
[package] [package]
name = "actix-identity" name = "actix-identity"
version = "0.8.0" version = "0.7.1"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Luca Palmieri <rust@lpalmieri.com>", "Luca Palmieri <rust@lpalmieri.com>",
] ]
description = "Identity management for Actix Web" description = "Identity management for Actix Web"
keywords = ["actix", "auth", "identity", "web", "security"] keywords = ["actix", "auth", "identity", "web", "security"]
repository.workspace = true homepage = "https://actix.rs"
homepage.workspace = true repository = "https://github.com/actix/actix-extras.git"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -19,11 +19,11 @@ all-features = true
[dependencies] [dependencies]
actix-service = "2" actix-service = "2"
actix-session = "0.10" actix-session = "0.9"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = "0.99.7"
futures-core = "0.3.17" futures-core = "0.3.17"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@ -31,11 +31,8 @@ tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies] [dev-dependencies]
actix-http = "3" actix-http = "3"
actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] } actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] }
actix-session = { version = "0.10", features = ["redis-session", "cookie-session"] } actix-session = { version = "0.9", features = ["redis-rs-session", "cookie-session"] }
env_logger = "0.11" env_logger = "0.11"
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json"] } reqwest = { version = "0.11", default-features = false, features = ["cookies", "json"] }
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
[lints]
workspace = true

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity) [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity)
[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.8.0)](https://docs.rs/actix-identity/0.8.0) [![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.7.1)](https://docs.rs/actix-identity/0.7.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity)
[![Dependency Status](https://deps.rs/crate/actix-identity/0.8.0/status.svg)](https://deps.rs/crate/actix-identity/0.8.0) [![Dependency Status](https://deps.rs/crate/actix-identity/0.7.1/status.svg)](https://deps.rs/crate/actix-identity/0.7.1)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@ -83,10 +83,8 @@ async fn login(request: HttpRequest) -> impl Responder {
} }
#[post("/logout")] #[post("/logout")]
async fn logout(user: Option<Identity>) -> impl Responder { async fn logout(user: Identity) -> impl Responder {
if let Some(user) = user { user.logout();
user.logout();
}
HttpResponse::Ok() HttpResponse::Ok()
} }
``` ```

View File

@ -13,10 +13,10 @@
//! http -v --session=identity GET localhost:8080/ //! http -v --session=identity GET localhost:8080/
//! ``` //! ```
use std::{io, time::Duration}; use std::io;
use actix_identity::{Identity, IdentityMiddleware}; use actix_identity::{Identity, IdentityMiddleware};
use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware}; use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::{ use actix_web::{
cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse, cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
HttpServer, Responder, HttpServer, Responder,
@ -28,25 +28,16 @@ async fn main() -> io::Result<()> {
let secret_key = Key::generate(); let secret_key = Key::generate();
let expiration = Duration::from_secs(24 * 60 * 60);
HttpServer::new(move || { HttpServer::new(move || {
let session_mw = let session_mw =
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
// disable secure cookie for local testing // disable secure cookie for local testing
.cookie_secure(false) .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(); .build();
let identity_mw = IdentityMiddleware::builder()
.visit_deadline(Some(expiration))
.build();
App::new() App::new()
// Install the identity framework first. // Install the identity framework first.
.wrap(identity_mw) .wrap(IdentityMiddleware::default())
// The identity system is built on top of sessions. You must install the session // 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 // middleware to leverage `actix-identity`. The session middleware must be mounted
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE

View File

@ -2,11 +2,11 @@
use actix_session::{SessionGetError, SessionInsertError}; use actix_session::{SessionGetError, SessionInsertError};
use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError}; use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError};
use derive_more::derive::{Display, Error, From}; use derive_more::{Display, Error, From};
/// Error that can occur during login attempts. /// Error that can occur during login attempts.
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, Error, From)]
#[display("{_0}")] #[display(fmt = "{_0}")]
pub struct LoginError(SessionInsertError); pub struct LoginError(SessionInsertError);
impl ResponseError for LoginError { impl ResponseError for LoginError {
@ -17,7 +17,7 @@ impl ResponseError for LoginError {
/// Error encountered when working with a session that has expired. /// Error encountered when working with a session that has expired.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display("The given session has expired and is no longer valid")] #[display(fmt = "The given session has expired and is no longer valid")]
pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange); pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
/// The identity information has been lost. /// 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. /// Seeing this error in user code indicates a bug in actix-identity.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display( #[display(
"The identity information in the current session has disappeared after having been \ fmt = "The identity information in the current session has disappeared after having been \
successfully validated. This is likely to be a bug." successfully validated. This is likely to be a bug."
)] )]
#[non_exhaustive] #[non_exhaustive]
@ -33,7 +33,7 @@ pub struct LostIdentityError;
/// There is no identity information attached to the current session. /// There is no identity information attached to the current session.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display("There is no identity information attached to the current session")] #[display(fmt = "There is no identity information attached to the current session")]
#[non_exhaustive] #[non_exhaustive]
pub struct MissingIdentityError; pub struct MissingIdentityError;
@ -42,21 +42,21 @@ pub struct MissingIdentityError;
#[non_exhaustive] #[non_exhaustive]
pub enum GetIdentityError { pub enum GetIdentityError {
/// The session has expired. /// The session has expired.
#[display("{_0}")] #[display(fmt = "{_0}")]
SessionExpiryError(SessionExpiryError), SessionExpiryError(SessionExpiryError),
/// No identity is found in a session. /// No identity is found in a session.
#[display("{_0}")] #[display(fmt = "{_0}")]
MissingIdentityError(MissingIdentityError), MissingIdentityError(MissingIdentityError),
/// Failed to accessing the session store. /// Failed to accessing the session store.
#[display("{_0}")] #[display(fmt = "{_0}")]
SessionGetError(SessionGetError), SessionGetError(SessionGetError),
/// Identity info was lost after being validated. /// Identity info was lost after being validated.
/// ///
/// Seeing this error indicates a bug in actix-identity. /// Seeing this error indicates a bug in actix-identity.
#[display("{_0}")] #[display(fmt = "{_0}")]
LostIdentityError(LostIdentityError), LostIdentityError(LostIdentityError),
} }

View File

@ -20,7 +20,7 @@ impl IdentityExt for ServiceRequest {
} }
} }
impl IdentityExt for GuardContext<'_> { impl<'a> IdentityExt for GuardContext<'a> {
fn get_identity(&self) -> Result<Identity, GetIdentityError> { fn get_identity(&self) -> Result<Identity, GetIdentityError> {
Identity::extract(&self.req_data()) Identity::extract(&self.req_data())
} }

View File

@ -74,10 +74,8 @@ async fn login(request: HttpRequest) -> impl Responder {
} }
#[post("/logout")] #[post("/logout")]
async fn logout(user: Option<Identity>) -> impl Responder { async fn logout(user: Identity) -> impl Responder {
if let Some(user) = user { user.logout();
user.logout();
}
HttpResponse::Ok() HttpResponse::Ok()
} }
``` ```
@ -95,7 +93,8 @@ In particular, you can automatically log out users who:
*/ */
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(missing_docs)] #![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware}; use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
use reqwest::StatusCode; use actix_web::http::StatusCode;
use crate::{fixtures::user_id, test_app::TestApp}; use crate::{fixtures::user_id, test_app::TestApp};

View File

@ -2,9 +2,6 @@
## Unreleased ## Unreleased
- Update `redis` dependency to `0.29`.
- Update `actix-session` dependency to `0.9`.
## 0.5.1 ## 0.5.1
- No significant changes since `0.5.0`. - No significant changes since `0.5.0`.

View File

@ -8,7 +8,7 @@ authors = [
description = "Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web" description = "Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web"
keywords = ["actix-web", "rate-api", "rate-limit", "limitation"] keywords = ["actix-web", "rate-api", "rate-limit", "limitation"]
categories = ["asynchronous", "web-programming"] categories = ["asynchronous", "web-programming"]
repository = "https://github.com/actix/actix-extras" repository = "https://github.com/actix/actix-extras.git"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -26,18 +26,15 @@ actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies"] }
chrono = "0.4" chrono = "0.4"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = "0.99.7"
log = "0.4" log = "0.4"
redis = { version = "0.29", default-features = false, features = ["tokio-comp"] } redis = { version = "0.24", default-features = false, features = ["tokio-comp"] }
time = "0.3" time = "0.3"
# session # session
actix-session = { version = "0.10", optional = true } actix-session = { version = "0.9", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = "4" actix-web = "4"
static_assertions = "1" static_assertions = "1"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
[lints]
workspace = true

View File

@ -1,4 +1,4 @@
use derive_more::derive::{Display, Error, From}; use derive_more::{Display, Error, From};
use crate::status::Status; use crate::status::Status;
@ -6,20 +6,20 @@ use crate::status::Status;
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, Error, From)]
pub enum Error { pub enum Error {
/// Redis client failed to connect or run a query. /// Redis client failed to connect or run a query.
#[display("Redis client failed to connect or run a query")] #[display(fmt = "Redis client failed to connect or run a query")]
Client(redis::RedisError), Client(redis::RedisError),
/// Limit is exceeded for a key. /// Limit is exceeded for a key.
#[display("Limit is exceeded for a key")] #[display(fmt = "Limit is exceeded for a key")]
#[from(ignore)] #[from(ignore)]
LimitExceeded(#[error(not(source))] Status), LimitExceeded(#[error(not(source))] Status),
/// Time conversion failed. /// Time conversion failed.
#[display("Time conversion failed")] #[display(fmt = "Time conversion failed")]
Time(time::error::ComponentRange), Time(time::error::ComponentRange),
/// Generic error. /// Generic error.
#[display("Generic error")] #[display(fmt = "Generic error")]
#[from(ignore)] #[from(ignore)]
Other(#[error(not(source))] String), Other(#[error(not(source))] String),
} }

View File

@ -45,7 +45,8 @@
//! ``` //! ```
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -137,7 +138,7 @@ impl Limiter {
let key = key.into(); let key = key.into();
let expires = self.period.as_secs(); let expires = self.period.as_secs();
let mut connection = self.client.get_multiplexed_tokio_connection().await?; let mut connection = self.client.get_tokio_connection().await?;
// The seed of this approach is outlined Atul R in a blog post about rate limiting using // The seed of this approach is outlined Atul R in a blog post about rate limiting using
// NodeJS and Redis. For more details, see https://blog.atulr.com/rate-limiter // NodeJS and Redis. For more details, see https://blog.atulr.com/rate-limiter

View File

@ -16,7 +16,7 @@ impl Status {
/// Constructs status limit status from parts. /// Constructs status limit status from parts.
#[must_use] #[must_use]
pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self { pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
let remaining = limit.saturating_sub(count); let remaining = if count >= limit { 0 } else { limit - count };
Status { Status {
limit, limit,

View File

@ -2,9 +2,6 @@
## Unreleased ## Unreleased
## 0.11.0
- Updated `prost` dependency to `0.13`.
- Minimum supported Rust version (MSRV) is now 1.75. - Minimum supported Rust version (MSRV) is now 1.75.
## 0.10.0 ## 0.10.0

View File

@ -1,14 +1,14 @@
[package] [package]
name = "actix-protobuf" name = "actix-protobuf"
version = "0.11.0" version = "0.10.0"
authors = [ authors = [
"kingxsp <jin.hb.zh@outlook.com>", "kingxsp <jin.hb.zh@outlook.com>",
"Yuki Okushi <huyuumi.dev@gmail.com>", "Yuki Okushi <huyuumi.dev@gmail.com>",
] ]
description = "Protobuf payload extractor for Actix Web" description = "Protobuf payload extractor for Actix Web"
keywords = ["actix", "web", "protobuf", "protocol", "rpc"] keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
repository.workspace = true homepage = "https://actix.rs"
homepage.workspace = true repository = "https://github.com/actix/actix-extras.git"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -19,13 +19,10 @@ all-features = true
[dependencies] [dependencies]
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display"] } derive_more = "0.99.7"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
prost = { version = "0.13", default-features = false } prost = { version = "0.12", default-features = false }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["macros"] } actix-web = { version = "4", default-features = false, features = ["macros"] }
prost = { version = "0.13", default-features = false, features = ["prost-derive"] } prost = { version = "0.12", default-features = false, features = ["prost-derive"] }
[lints]
workspace = true

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf) [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf)
[![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.11.0)](https://docs.rs/actix-protobuf/0.11.0) [![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.10.0)](https://docs.rs/actix-protobuf/0.10.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-protobuf) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-protobuf)
[![Dependency Status](https://deps.rs/crate/actix-protobuf/0.11.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.11.0) [![Dependency Status](https://deps.rs/crate/actix-protobuf/0.10.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.10.0)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -1,6 +1,8 @@
//! Protobuf payload extractor for Actix Web. //! Protobuf payload extractor for Actix Web.
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -22,7 +24,7 @@ use actix_web::{
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder, Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder,
ResponseError, ResponseError,
}; };
use derive_more::derive::Display; use derive_more::Display;
use futures_util::{ use futures_util::{
future::{FutureExt as _, LocalBoxFuture}, future::{FutureExt as _, LocalBoxFuture},
stream::StreamExt as _, stream::StreamExt as _,
@ -32,28 +34,26 @@ use prost::{DecodeError as ProtoBufDecodeError, EncodeError as ProtoBufEncodeErr
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum ProtoBufPayloadError { pub enum ProtoBufPayloadError {
/// Payload size is bigger than 256k /// Payload size is bigger than 256k
#[display("Payload size is bigger than 256k")] #[display(fmt = "Payload size is bigger than 256k")]
Overflow, Overflow,
/// Content type error /// Content type error
#[display("Content type error")] #[display(fmt = "Content type error")]
ContentType, ContentType,
/// Serialize error /// Serialize error
#[display("ProtoBuf serialize error: {_0}")] #[display(fmt = "ProtoBuf serialize error: {_0}")]
Serialize(ProtoBufEncodeError), Serialize(ProtoBufEncodeError),
/// Deserialize error /// Deserialize error
#[display("ProtoBuf deserialize error: {_0}")] #[display(fmt = "ProtoBuf deserialize error: {_0}")]
Deserialize(ProtoBufDecodeError), Deserialize(ProtoBufDecodeError),
/// Payload error /// Payload error
#[display("Error that occur during reading payload: {_0}")] #[display(fmt = "Error that occur during reading payload: {_0}")]
Payload(PayloadError), Payload(PayloadError),
} }
// TODO: impl error for ProtoBufPayloadError
impl ResponseError for ProtoBufPayloadError { impl ResponseError for ProtoBufPayloadError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
match *self { match *self {

154
actix-redis/CHANGES.md Normal file
View File

@ -0,0 +1,154 @@
# Changes
## Unreleased
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.13.0
- Update `redis-async` dependency to `0.16`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.12.0
- Update `actix` dependency to `0.13`.
- Update `redis-async` dependency to `0.13`.
- Update `tokio-util` dependency to `0.7`.
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.11.0
### Removed
- `RedisSession` has been removed. Check out `RedisActorSessionStore` in `actix-session` for a session store backed by Redis using `actix-redis`. [#212]
### Changed
- Update `redis-async` dependency to `0.12`. [#212]
[#212]: https://github.com/actix/actix-extras/pull/212
## 0.10.0
- Update `actix-web` dependency to `4`.
## 0.10.0-beta.6
- Update `actix-web` dependency to `4.0.0-rc.1`.
## 0.10.0-beta.5
- Update `actix-web` dependency to `4.0.0.beta-18`. [#218]
- Minimum supported Rust version (MSRV) is now 1.54.
[#218]: https://github.com/actix/actix-extras/pull/218
## 0.10.0-beta.4
- A session will be created in Redis if and only if there is some data inside the session state. This reduces the performance impact of `RedisSession` on routes that do not leverage sessions. [#207]
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#207]: https://github.com/actix/actix-extras/pull/207
[#209]: https://github.com/actix/actix-extras/pull/209
## 0.10.0-beta.3
- Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52.
[#203]: https://github.com/actix/actix-extras/pull/203
## 0.10.0-beta.2
- No notable changes.
## 0.10.0-beta.1
- Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0.
## 0.9.2
- Implement `std::error::Error` for `Error` [#135]
- Allow the removal of `Max-Age` for session-only cookies. [#161]
[#135]: https://github.com/actix/actix-extras/pull/135
[#161]: https://github.com/actix/actix-extras/pull/161
## 0.9.1
- Enforce minimum redis-async version of 0.6.3 to workaround breaking patch change.
## 0.9.0
- Update `actix-web` dependency to 3.0.0.
- Minimize `futures` dependency.
## 0.9.0-alpha.2
- Add `cookie_http_only` functionality to RedisSession builder, setting this to false allows JavaScript to access cookies. Defaults to true.
- Change type of parameter of ttl method to u32.
- Update `actix` to 0.10.0-alpha.3
- Update `tokio-util` to 0.3
- Minimum supported Rust version(MSRV) is now 1.40.0.
## 0.9.0-alpha.1
- Update `actix` to 0.10.0-alpha.2
- Update `actix-session` to 0.4.0-alpha.1
- Update `actix-web` to 3.0.0-alpha.1
- Update `time` to 0.2.9
## 0.8.1
- Move `env_logger` dependency to dev-dependencies and update to 0.7
- Update `actix_web` to 2.0.0 from 2.0.0-rc
- Move repository to actix-extras
## 0.8.0 - 2019-12-20
- Release
## 0.8.0-alpha.1 - 2019-12-16
- Migrate to actix 0.9
## 0.7.0 - 2019-09-25
- added cache_keygen functionality to RedisSession builder, enabling support for customizable cache key creation
## 0.6.1 - 2019-07-19
- remove ClonableService usage
- added comprehensive tests for session workflow
## 0.6.0 - 2019-07-08
- actix-web 1.0.0 compatibility
- Upgraded logic that evaluates session state, including new SessionStatus field, and introduced `session.renew()` and `session.purge()` functionality. Use `renew()` to cycle the session key at successful login. `renew()` keeps a session's state while replacing the old cookie and session key with new ones. Use `purge()` at logout to invalidate the session cookie and remove the session's redis cache entry.
## 0.5.1 - 2018-08-02
- Use cookie 0.11
## 0.5.0 - 2018-07-21
- Session cookie configuration
- Actix/Actix-web 0.7 compatibility
## 0.4.0 - 2018-05-08
- Actix web 0.6 compatibility
## 0.3.0 - 2018-04-10
- Actix web 0.5 compatibility
## 0.2.0 - 2018-02-28
- Use resolver actor from actix
- Use actix web 0.5
## 0.1.0 - 2018-01-23
- First release

48
actix-redis/Cargo.toml Normal file
View File

@ -0,0 +1,48 @@
[package]
name = "actix-redis"
version = "0.13.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actor-based Redis client"
keywords = ["actix", "redis", "async"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-extras.git"
categories = ["network-programming", "asynchronous"]
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[lib]
name = "actix_redis"
path = "src/lib.rs"
[features]
default = ["web"]
# actix-web integration
web = ["actix-web"]
[dependencies]
actix = { version = "0.13", default-features = false }
actix-rt = { version = "2.1", default-features = false }
actix-service = "2"
actix-tls = { version = "3", default-features = false, features = ["connect"] }
log = "0.4.6"
backoff = "0.4.0"
derive_more = "0.99.7"
futures-core = "0.3.17"
redis-async = "0.16"
time = "0.3"
tokio = { version = "1.18.4", features = ["sync"] }
tokio-util = "0.7"
actix-web = { version = "4", default-features = false, optional = true }
[dev-dependencies]
actix-test = "0.1.0-beta.12"
actix-web = { version = "4", default-features = false, features = ["macros"] }
env_logger = "0.11"
serde = { version = "1.0.101", features = ["derive"] }

1
actix-redis/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-redis/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

18
actix-redis/README.md Normal file
View File

@ -0,0 +1,18 @@
# actix-redis
> Actor-based Redis client.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-redis?label=latest)](https://crates.io/crates/actix-redis)
[![Documentation](https://docs.rs/actix-redis/badge.svg?version=0.13.0)](https://docs.rs/actix-redis/0.13.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-redis)
[![Dependency Status](https://deps.rs/crate/actix-redis/0.13.0/status.svg)](https://deps.rs/crate/actix-redis/0.13.0)
<!-- prettier-ignore-end -->
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-redis)
- [Example Project](https://github.com/actix/examples/tree/master/auth/redis-session)
- Minimum Supported Rust Version (MSRV): 1.57

32
actix-redis/src/lib.rs Normal file
View File

@ -0,0 +1,32 @@
//! Redis integration for `actix`.
#![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use derive_more::{Display, Error, From};
pub use redis_async::{error::Error as RespError, resp::RespValue, resp_array};
mod redis;
pub use self::redis::{Command, RedisActor};
/// General purpose `actix-redis` error.
#[derive(Debug, Display, Error, From)]
pub enum Error {
#[display(fmt = "Redis error: {_0}")]
Redis(redis_async::error::Error),
/// Receiving message during reconnecting.
#[display(fmt = "Redis: Not connected")]
NotConnected,
/// Cancel all waiters when connection is dropped.
#[display(fmt = "Redis: Disconnected")]
Disconnected,
}
#[cfg(feature = "web")]
impl actix_web::ResponseError for Error {}

143
actix-redis/src/redis.rs Normal file
View File

@ -0,0 +1,143 @@
use std::{collections::VecDeque, io};
use actix::prelude::*;
use actix_rt::net::TcpStream;
use actix_service::boxed::{self, BoxService};
use actix_tls::connect::{ConnectError, ConnectInfo, Connection, ConnectorService};
use backoff::{backoff::Backoff, ExponentialBackoff};
use log::{error, info, warn};
use redis_async::{
error::Error as RespError,
resp::{RespCodec, RespValue},
};
use tokio::{
io::{split, WriteHalf},
sync::oneshot,
};
use tokio_util::codec::FramedRead;
use crate::Error;
/// Command for sending data to Redis.
#[derive(Debug)]
pub struct Command(pub RespValue);
impl Message for Command {
type Result = Result<RespValue, Error>;
}
/// Redis communication actor.
pub struct RedisActor {
addr: String,
connector: BoxService<ConnectInfo<String>, Connection<String, TcpStream>, ConnectError>,
backoff: ExponentialBackoff,
cell: Option<actix::io::FramedWrite<RespValue, WriteHalf<TcpStream>, RespCodec>>,
queue: VecDeque<oneshot::Sender<Result<RespValue, Error>>>,
}
impl RedisActor {
/// Start new `Supervisor` with `RedisActor`.
pub fn start<S: Into<String>>(addr: S) -> Addr<RedisActor> {
let addr = addr.into();
let backoff = ExponentialBackoff {
max_elapsed_time: None,
..Default::default()
};
Supervisor::start(|_| RedisActor {
addr,
connector: boxed::service(ConnectorService::default()),
cell: None,
backoff,
queue: VecDeque::new(),
})
}
}
impl Actor for RedisActor {
type Context = Context<Self>;
fn started(&mut self, ctx: &mut Context<Self>) {
let req = ConnectInfo::new(self.addr.to_owned());
self.connector
.call(req)
.into_actor(self)
.map(|res, act, ctx| match res {
Ok(conn) => {
let stream = conn.into_parts().0;
info!("Connected to redis server: {}", act.addr);
let (r, w) = split(stream);
// configure write side of the connection
let framed = actix::io::FramedWrite::new(w, RespCodec, ctx);
act.cell = Some(framed);
// read side of the connection
ctx.add_stream(FramedRead::new(r, RespCodec));
act.backoff.reset();
}
Err(err) => {
error!("Can not connect to redis server: {}", err);
// re-connect with backoff time.
// we stop current context, supervisor will restart it.
if let Some(timeout) = act.backoff.next_backoff() {
ctx.run_later(timeout, |_, ctx| ctx.stop());
}
}
})
.wait(ctx);
}
}
impl Supervised for RedisActor {
fn restarting(&mut self, _: &mut Self::Context) {
self.cell.take();
for tx in self.queue.drain(..) {
let _ = tx.send(Err(Error::Disconnected));
}
}
}
impl actix::io::WriteHandler<io::Error> for RedisActor {
fn error(&mut self, err: io::Error, _: &mut Self::Context) -> Running {
warn!("Redis connection dropped: {} error: {}", self.addr, err);
Running::Stop
}
}
impl StreamHandler<Result<RespValue, RespError>> for RedisActor {
fn handle(&mut self, msg: Result<RespValue, RespError>, ctx: &mut Self::Context) {
match msg {
Err(e) => {
if let Some(tx) = self.queue.pop_front() {
let _ = tx.send(Err(e.into()));
}
ctx.stop();
}
Ok(val) => {
if let Some(tx) = self.queue.pop_front() {
let _ = tx.send(Ok(val));
}
}
}
}
}
impl Handler<Command> for RedisActor {
type Result = ResponseFuture<Result<RespValue, Error>>;
fn handle(&mut self, msg: Command, _: &mut Self::Context) -> Self::Result {
let (tx, rx) = oneshot::channel();
if let Some(ref mut cell) = self.cell {
self.queue.push_back(tx);
cell.write(msg.0);
} else {
let _ = tx.send(Err(Error::NotConnected));
}
Box::pin(async move { rx.await.map_err(|_| Error::Disconnected)? })
}
}

View File

@ -0,0 +1,42 @@
#[macro_use]
extern crate redis_async;
use actix_redis::{Command, Error, RedisActor, RespValue};
#[actix_web::test]
async fn test_error_connect() {
let addr = RedisActor::start("localhost:54000");
let _addr2 = addr.clone();
let res = addr.send(Command(resp_array!["GET", "test"])).await;
match res {
Ok(Err(Error::NotConnected)) => (),
_ => panic!("Should not happen {:?}", res),
}
}
#[actix_web::test]
async fn test_redis() {
env_logger::init();
let addr = RedisActor::start("127.0.0.1:6379");
let res = addr
.send(Command(resp_array!["SET", "test", "value"]))
.await;
match res {
Ok(Ok(resp)) => {
assert_eq!(resp, RespValue::SimpleString("OK".to_owned()));
let res = addr.send(Command(resp_array!["GET", "test"])).await;
match res {
Ok(Ok(resp)) => {
println!("RESP: {resp:?}");
assert_eq!(resp, RespValue::BulkString((&b"value"[..]).into()));
}
_ => panic!("Should not happen {:?}", res),
}
}
_ => panic!("Should not happen {:?}", res),
}
}

View File

@ -2,24 +2,6 @@
## Unreleased ## 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 ## 0.9.0
- Remove use of `async-trait` on `SessionStore` trait. - Remove use of `async-trait` on `SessionStore` trait.

View File

@ -1,13 +1,13 @@
[package] [package]
name = "actix-session" name = "actix-session"
version = "0.10.1" version = "0.9.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Luca Palmieri <rust@lpalmieri.com>", "Luca Palmieri <rust@lpalmieri.com>",
] ]
description = "Session management for Actix Web" description = "Session management for Actix Web"
keywords = ["http", "web", "framework", "async", "session"] keywords = ["http", "web", "framework", "async", "session"]
repository.workspace = true repository = "https://github.com/actix/actix-extras/tree/master/actix-session"
homepage.workspace = true homepage.workspace = true
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
@ -20,10 +20,9 @@ all-features = true
[features] [features]
default = [] default = []
cookie-session = [] cookie-session = []
redis-session = ["dep:redis"] redis-actor-session = ["actix-redis", "actix", "futures-core", "rand"]
redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"] redis-rs-session = ["redis", "rand"]
redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"] redis-rs-tls-session = ["redis-rs-session", "redis/tokio-native-tls-comp"]
redis-pool = ["dep:deadpool-redis"]
[dependencies] [dependencies]
actix-service = "2" actix-service = "2"
@ -31,30 +30,31 @@ actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] } actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
anyhow = "1" anyhow = "1"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = "0.99.7"
rand = "0.9" rand = { version = "0.8", optional = true }
serde = { version = "1" } serde = { version = "1" }
serde_json = { version = "1" } serde_json = { version = "1" }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# redis-session # redis-actor-session
redis = { version = "0.29", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true } actix = { version = "0.13", default-features = false, optional = true }
deadpool-redis = { version = "0.20", optional = true } actix-redis = { version = "0.12", optional = true }
futures-core = { version = "0.3.17", optional = true }
# redis-rs-session
redis = { version = "0.24", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
[dev-dependencies] [dev-dependencies]
actix-session = { path = ".", features = ["cookie-session", "redis-session"] } actix-session = { path = ".", features = ["cookie-session", "redis-actor-session", "redis-rs-session"] }
actix-test = "0.1" actix-test = "0.1.0-beta.10"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] } actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } env_logger = "0.11"
tracing = "0.1.30" log = "0.4"
[lints]
workspace = true
[[example]] [[example]]
name = "basic" name = "basic"
required-features = ["redis-session"] required-features = ["redis-actor-session"]
[[example]] [[example]]
name = "authentication" name = "authentication"
required-features = ["redis-session"] required-features = ["redis-actor-session"]

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session) [![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session)
[![Documentation](https://docs.rs/actix-session/badge.svg?version=0.10.1)](https://docs.rs/actix-session/0.10.1) [![Documentation](https://docs.rs/actix-session/badge.svg?version=0.9.0)](https://docs.rs/actix-session/0.9.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-session) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-session)
[![Dependency Status](https://deps.rs/crate/actix-session/0.10.1/status.svg)](https://deps.rs/crate/actix-session/0.10.1) [![Dependency Status](https://deps.rs/crate/actix-session/0.9.0/status.svg)](https://deps.rs/crate/actix-session/0.9.0)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@ -25,7 +25,7 @@ We refer to the cookie used for sessions as a **session cookie**. Its content is
`actix-session` provides an easy-to-use framework to manage sessions in applications built on top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality provided by `actix-session`; it takes care of all the session cookie handling and instructs the **storage backend** to create/delete/update the session state based on the operations performed against the active [`Session`]. `actix-session` provides an easy-to-use framework to manage sessions in applications built on top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality provided by `actix-session`; it takes care of all the session cookie handling and instructs the **storage backend** to create/delete/update the session state based on the operations performed against the active [`Session`].
`actix-session` provides some built-in storage backends: ([`CookieSessionStore`], [`RedisSessionStore`]) - you can create a custom storage backend by implementing the [`SessionStore`] trait. `actix-session` provides some built-in storage backends: ([`CookieSessionStore`], [`RedisSessionStore`], and [`RedisActorSessionStore`]) - you can create a custom storage backend by implementing the [`SessionStore`] trait.
Further reading on sessions: Further reading on sessions:
@ -94,26 +94,34 @@ 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. - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature flag.
```console ```toml
cargo add actix-session --features=cookie-session [dependencies]
# ...
actix-session = { version = "...", features = ["cookie-session"] }
``` ```
- a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the `redis-session` feature flag. - a Redis-based backend via [`actix-redis`](https://docs.rs/actix-redis), [`RedisActorSessionStore`], using the `redis-actor-session` feature flag.
```console ```toml
cargo add actix-session --features=redis-session [dependencies]
# ...
actix-session = { version = "...", features = ["redis-actor-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): - a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using the `redis-rs-session` feature flag.
```console ```toml
cargo add actix-session --features=redis-session-native-tls [dependencies]
# ...
actix-session = { version = "...", features = ["redis-rs-session"] }
``` ```
If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag: Add the `redis-rs-tls-session` feature flag if you want to connect to Redis using a secured connection:
```console ```toml
cargo add actix-session --features=redis-session-rustls [dependencies]
# ...
actix-session = { version = "...", features = ["redis-rs-session", "redis-rs-tls-session"] }
``` ```
You can implement your own session storage backend using the [`SessionStore`] trait. You can implement your own session storage backend using the [`SessionStore`] trait.
@ -121,5 +129,6 @@ You can implement your own session storage backend using the [`SessionStore`] tr
[`SessionStore`]: storage::SessionStore [`SessionStore`]: storage::SessionStore
[`CookieSessionStore`]: storage::CookieSessionStore [`CookieSessionStore`]: storage::CookieSessionStore
[`RedisSessionStore`]: storage::RedisSessionStore [`RedisSessionStore`]: storage::RedisSessionStore
[`RedisActorSessionStore`]: storage::RedisActorSessionStore
<!-- cargo-rdme end --> <!-- cargo-rdme end -->

View File

@ -1,12 +1,10 @@
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware}; use actix_session::{storage::RedisActorSessionStore, Session, SessionMiddleware};
use actix_web::{ use actix_web::{
cookie::{Key, SameSite}, cookie::{Key, SameSite},
error::InternalError, error::InternalError,
middleware, web, App, Error, HttpResponse, HttpServer, Responder, middleware, web, App, Error, HttpResponse, HttpServer, Responder,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
#[derive(Deserialize)] #[derive(Deserialize)]
struct Credentials { struct Credentials {
@ -73,21 +71,12 @@ async fn secret(session: Session) -> Result<impl Responder, Error> {
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt() env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
// The signing key would usually be read from a configuration file/environment variables. // The signing key would usually be read from a configuration file/environment variables.
let signing_key = Key::generate(); let signing_key = Key::generate();
tracing::info!("setting up Redis session storage"); log::info!("starting HTTP server at http://localhost:8080");
let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
tracing::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
@ -95,12 +84,15 @@ async fn main() -> std::io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
// cookie session middleware // cookie session middleware
.wrap( .wrap(
SessionMiddleware::builder(storage.clone(), signing_key.clone()) SessionMiddleware::builder(
// allow the cookie to be accessed from javascript RedisActorSessionStore::new("127.0.0.1:6379"),
.cookie_http_only(false) signing_key.clone(),
// allow the cookie only from the current domain )
.cookie_same_site(SameSite::Strict) // allow the cookie to be accessed from javascript
.build(), .cookie_http_only(false)
// allow the cookie only from the current domain
.cookie_same_site(SameSite::Strict)
.build(),
) )
.route("/login", web::post().to(login)) .route("/login", web::post().to(login))
.route("/secret", web::get().to(secret)) .route("/secret", web::get().to(secret))

View File

@ -1,7 +1,5 @@
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware}; use actix_session::{storage::RedisActorSessionStore, Session, SessionMiddleware};
use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder}; use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
/// simple handler /// simple handler
async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> { async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> {
@ -20,28 +18,22 @@ async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Err
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt() env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
// The signing key would usually be read from a configuration file/environment variables. // The signing key would usually be read from a configuration file/environment variables.
let signing_key = Key::generate(); let signing_key = Key::generate();
tracing::info!("setting up Redis session storage"); log::info!("starting HTTP server at http://localhost:8080");
let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
tracing::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
// enable logger // enable logger
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
// cookie session middleware // cookie session middleware
.wrap(SessionMiddleware::new(storage.clone(), signing_key.clone())) .wrap(SessionMiddleware::new(
RedisActorSessionStore::new("127.0.0.1:6379"),
signing_key.clone(),
))
// register simple route, handle all methods // register simple route, handle all methods
.service(web::resource("/").to(index)) .service(web::resource("/").to(index))
}) })

View File

@ -1,7 +1,7 @@
//! Configuration options to tune the behaviour of [`SessionMiddleware`]. //! Configuration options to tune the behaviour of [`SessionMiddleware`].
use actix_web::cookie::{time::Duration, Key, SameSite}; use actix_web::cookie::{time::Duration, Key, SameSite};
use derive_more::derive::From; use derive_more::From;
use crate::{storage::SessionStore, SessionMiddleware}; use crate::{storage::SessionStore, SessionMiddleware};

View File

@ -1,136 +1,149 @@
//! Session management for Actix Web. /*!
//! Session management for Actix Web.
//! The HTTP protocol, at a first glance, is stateless: the client sends a request, the server
//! parses its content, performs some processing and returns a response. The outcome is only The HTTP protocol, at a first glance, is stateless: the client sends a request, the server
//! influenced by the provided inputs (i.e. the request content) and whatever state the server parses its content, performs some processing and returns a response. The outcome is only
//! queries while performing its processing. influenced by the provided inputs (i.e. the request content) and whatever state the server
//! queries while performing its processing.
//! Stateless systems are easier to reason about, but they are not quite as powerful as we need them
//! to be - e.g. how do you authenticate a user? The user would be forced to authenticate **for Stateless systems are easier to reason about, but they are not quite as powerful as we need them
//! every single request**. That is, for example, how 'Basic' Authentication works. While it may to be - e.g. how do you authenticate a user? The user would be forced to authenticate **for
//! work for a machine user (i.e. an API client), it is impractical for a person—you do not want a every single request**. That is, for example, how 'Basic' Authentication works. While it may
//! login prompt on every single page you navigate to! work for a machine user (i.e. an API client), it is impractical for a personyou do not want a
//! login prompt on every single page you navigate to!
//! There is a solution - **sessions**. Using sessions the server can attach state to a set of
//! requests coming from the same client. They are built on top of cookies - the server sets a There is a solution - **sessions**. Using sessions the server can attach state to a set of
//! cookie in the HTTP response (`Set-Cookie` header), the client (e.g. the browser) will store the requests coming from the same client. They are built on top of cookies - the server sets a
//! cookie and play it back to the server when sending new requests (using the `Cookie` header). cookie in the HTTP response (`Set-Cookie` header), the client (e.g. the browser) will store the
//! cookie and play it back to the server when sending new requests (using the `Cookie` header).
//! We refer to the cookie used for sessions as a **session cookie**. Its content is called
//! **session key** (or **session ID**), while the state attached to the session is referred to as We refer to the cookie used for sessions as a **session cookie**. Its content is called
//! **session state**. **session key** (or **session ID**), while the state attached to the session is referred to as
//! **session state**.
//! `actix-session` provides an easy-to-use framework to manage sessions in applications built on
//! top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality `actix-session` provides an easy-to-use framework to manage sessions in applications built on
//! provided by `actix-session`; it takes care of all the session cookie handling and instructs the top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality
//! **storage backend** to create/delete/update the session state based on the operations performed provided by `actix-session`; it takes care of all the session cookie handling and instructs the
//! against the active [`Session`]. **storage backend** to create/delete/update the session state based on the operations performed
//! against the active [`Session`].
//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
//! [`RedisSessionStore`]) - you can create a custom storage backend by implementing the `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
//! [`SessionStore`] trait. [`RedisSessionStore`], and [`RedisActorSessionStore`]) - you can create a custom storage backend
//! by implementing the [`SessionStore`] trait.
//! Further reading on sessions:
//! - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265); Further reading on sessions:
//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html). - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265);
//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
//! # Getting started
//! To start using sessions in your Actix Web application you must register [`SessionMiddleware`] # Getting started
//! as a middleware on your `App`: To start using sessions in your Actix Web application you must register [`SessionMiddleware`]
//! as a middleware on your `App`:
//! ```no_run
//! use actix_web::{web, App, HttpServer, HttpResponse, Error}; ```no_run
//! use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore}; use actix_web::{web, App, HttpServer, HttpResponse, Error};
//! use actix_web::cookie::Key; use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
//! use actix_web::cookie::Key;
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> { #[actix_web::main]
//! // When using `Key::generate()` it is important to initialize outside of the async fn main() -> std::io::Result<()> {
//! // `HttpServer::new` closure. When deployed the secret key should be read from a // When using `Key::generate()` it is important to initialize outside of the
//! // configuration file or environment variables. // `HttpServer::new` closure. When deployed the secret key should be read from a
//! let secret_key = Key::generate(); // configuration file or environment variables.
//! let secret_key = Key::generate();
//! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
//! .await let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
//! .unwrap(); .await
//! .unwrap();
//! HttpServer::new(move ||
//! App::new() HttpServer::new(move ||
//! // Add session management to your application using Redis for session state storage App::new()
//! .wrap( // Add session management to your application using Redis for session state storage
//! SessionMiddleware::new( .wrap(
//! redis_store.clone(), SessionMiddleware::new(
//! secret_key.clone(), redis_store.clone(),
//! ) secret_key.clone(),
//! ) )
//! .default_service(web::to(|| HttpResponse::Ok()))) )
//! .bind(("127.0.0.1", 8080))? .default_service(web::to(|| HttpResponse::Ok())))
//! .run() .bind(("127.0.0.1", 8080))?
//! .await .run()
//! } .await
//! ``` }
//! ```
//! The session state can be accessed and modified by your request handlers using the [`Session`]
//! extractor. Note that this doesn't work in the stream of a streaming response. The session state can be accessed and modified by your request handlers using the [`Session`]
//! extractor. Note that this doesn't work in the stream of a streaming response.
//! ```no_run
//! use actix_web::Error; ```no_run
//! use actix_session::Session; use actix_web::Error;
//! use actix_session::Session;
//! fn index(session: Session) -> Result<&'static str, Error> {
//! // access the session state fn index(session: Session) -> Result<&'static str, Error> {
//! if let Some(count) = session.get::<i32>("counter")? { // access the session state
//! println!("SESSION value: {}", count); if let Some(count) = session.get::<i32>("counter")? {
//! // modify the session state println!("SESSION value: {}", count);
//! session.insert("counter", count + 1)?; // modify the session state
//! } else { session.insert("counter", count + 1)?;
//! session.insert("counter", 1)?; } else {
//! } session.insert("counter", 1)?;
//! }
//! Ok("Welcome!")
//! } Ok("Welcome!")
//! ``` }
//! ```
//! # Choosing A Backend
//! # Choosing A Backend
//! By default, `actix-session` does not provide any storage backend to retrieve and save the state
//! attached to your sessions. You can enable: By default, `actix-session` does not provide any storage backend to retrieve and save the state
//! attached to your sessions. You can enable:
//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
//! flag. - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
//! flag.
//! ```console
//! cargo add actix-session --features=cookie-session ```toml
//! ``` [dependencies]
//! # ...
//! - a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the actix-session = { version = "...", features = ["cookie-session"] }
//! `redis-session` feature flag. ```
//!
//! ```console - a Redis-based backend via [`actix-redis`](https://docs.rs/actix-redis),
//! cargo add actix-session --features=redis-session [`RedisActorSessionStore`], using the `redis-actor-session` feature flag.
//! ```
//! ```toml
//! Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure [dependencies]
//! connection (via the `native-tls` crate): # ...
//! actix-session = { version = "...", features = ["redis-actor-session"] }
//! ```console ```
//! cargo add actix-session --features=redis-session-native-tls
//! ``` - a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using
//! the `redis-rs-session` feature flag.
//! If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
//! ```toml
//! ```console [dependencies]
//! cargo add actix-session --features=redis-session-rustls # ...
//! ``` actix-session = { version = "...", features = ["redis-rs-session"] }
//! ```
//! You can implement your own session storage backend using the [`SessionStore`] trait.
//! Add the `redis-rs-tls-session` feature flag if you want to connect to Redis using a secured
//! [`SessionStore`]: storage::SessionStore connection:
//! [`CookieSessionStore`]: storage::CookieSessionStore
//! [`RedisSessionStore`]: storage::RedisSessionStore ```toml
[dependencies]
# ...
actix-session = { version = "...", features = ["redis-rs-session", "redis-rs-tls-session"] }
```
You can implement your own session storage backend using the [`SessionStore`] trait.
[`SessionStore`]: storage::SessionStore
[`CookieSessionStore`]: storage::CookieSessionStore
[`RedisSessionStore`]: storage::RedisSessionStore
[`RedisActorSessionStore`]: storage::RedisActorSessionStore
*/
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -148,7 +161,6 @@ pub use self::{
}; };
#[cfg(test)] #[cfg(test)]
#[allow(missing_docs)]
pub mod test_helpers { pub mod test_helpers {
use actix_web::cookie::Key; use actix_web::cookie::Key;

View File

@ -47,7 +47,7 @@ use crate::{
/// # Examples /// # Examples
/// ```no_run /// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error}; /// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore}; /// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
/// use actix_web::cookie::Key; /// use actix_web::cookie::Key;
/// ///
/// // The secret key would usually be read from a configuration file/environment variables. /// // The secret key would usually be read from a configuration file/environment variables.
@ -59,20 +59,20 @@ use crate::{
/// #[actix_web::main] /// #[actix_web::main]
/// async fn main() -> std::io::Result<()> { /// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key(); /// let secret_key = get_secret_key();
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap(); /// let redis_connection_string = "127.0.0.1:6379";
/// /// HttpServer::new(move ||
/// HttpServer::new(move || { /// App::new()
/// App::new() /// // Add session management to your application using Redis for session state storage
/// // Add session management to your application using Redis as storage /// .wrap(
/// .wrap(SessionMiddleware::new( /// SessionMiddleware::new(
/// storage.clone(), /// RedisActorSessionStore::new(redis_connection_string),
/// secret_key.clone(), /// secret_key.clone()
/// )) /// )
/// .default_service(web::to(|| HttpResponse::Ok())) /// )
/// }) /// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))? /// .bind(("127.0.0.1", 8080))?
/// .run() /// .run()
/// .await /// .await
/// } /// }
/// ``` /// ```
/// ///
@ -80,7 +80,7 @@ use crate::{
/// ///
/// ```no_run /// ```no_run
/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web}; /// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore}; /// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
/// use actix_session::config::PersistentSession; /// use actix_session::config::PersistentSession;
/// ///
/// // The secret key would usually be read from a configuration file/environment variables. /// // The secret key would usually be read from a configuration file/environment variables.
@ -92,23 +92,25 @@ use crate::{
/// #[actix_web::main] /// #[actix_web::main]
/// async fn main() -> std::io::Result<()> { /// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key(); /// let secret_key = get_secret_key();
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap(); /// let redis_connection_string = "127.0.0.1:6379";
/// /// HttpServer::new(move ||
/// HttpServer::new(move || { /// App::new()
/// App::new()
/// // Customise session length! /// // Customise session length!
/// .wrap( /// .wrap(
/// SessionMiddleware::builder(storage.clone(), secret_key.clone()) /// SessionMiddleware::builder(
/// .session_lifecycle( /// RedisActorSessionStore::new(redis_connection_string),
/// PersistentSession::default().session_ttl(time::Duration::days(5)), /// secret_key.clone()
/// ) /// )
/// .build(), /// .session_lifecycle(
/// PersistentSession::default()
/// .session_ttl(time::Duration::days(5))
/// )
/// .build(),
/// ) /// )
/// .default_service(web::to(|| HttpResponse::Ok())) /// .default_service(web::to(|| HttpResponse::Ok())))
/// }) /// .bind(("127.0.0.1", 8080))?
/// .bind(("127.0.0.1", 8080))? /// .run()
/// .run() /// .await
/// .await
/// } /// }
/// ``` /// ```
#[derive(Clone)] #[derive(Clone)]

View File

@ -14,7 +14,7 @@ use actix_web::{
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
}; };
use anyhow::Context; use anyhow::Context;
use derive_more::derive::{Display, From}; use derive_more::{Display, From};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
/// The primary interface to access and modify session state. /// The primary interface to access and modify session state.
@ -33,9 +33,6 @@ use serde::{de::DeserializeOwned, Serialize};
/// session.insert("counter", 1)?; /// session.insert("counter", 1)?;
/// } /// }
/// ///
/// // or use the shorthand
/// session.update_or("counter", 1, |count: i32| count + 1);
///
/// Ok("Welcome!") /// Ok("Welcome!")
/// } /// }
/// # actix_web::web::to(index); /// # actix_web::web::to(index);
@ -100,11 +97,6 @@ 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. /// Get all raw key-value data from the session.
/// ///
/// Note that values are JSON encoded. /// Note that values are JSON encoded.
@ -122,9 +114,7 @@ impl Session {
/// Any serializable value can be used and will be encoded as JSON in session data, hence why /// 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. /// only a reference to the value is taken.
/// ///
/// # Errors /// It returns an error if it fails to serialize `value` to JSON.
///
/// Returns an error if JSON serialization of `value` fails.
pub fn insert<T: Serialize>( pub fn insert<T: Serialize>(
&self, &self,
key: impl Into<String>, key: impl Into<String>,
@ -142,8 +132,9 @@ impl Session {
.with_context(|| { .with_context(|| {
format!( format!(
"Failed to serialize the provided `{}` type instance as JSON in order to \ "Failed to serialize the provided `{}` type instance as JSON in order to \
attach as session data to the `{key}` key", attach as session data to the `{}` key",
std::any::type_name::<T>(), std::any::type_name::<T>(),
&key
) )
}) })
.map_err(SessionInsertError)?; .map_err(SessionInsertError)?;
@ -154,83 +145,6 @@ impl Session {
Ok(()) 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. /// Remove value from the session.
/// ///
/// If present, the JSON encoded value is returned. /// If present, the JSON encoded value is returned.
@ -374,7 +288,7 @@ impl FromRequest for Session {
/// Error returned by [`Session::get`]. /// Error returned by [`Session::get`].
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[display("{_0}")] #[display(fmt = "{_0}")]
pub struct SessionGetError(anyhow::Error); pub struct SessionGetError(anyhow::Error);
impl StdError for SessionGetError { impl StdError for SessionGetError {
@ -391,7 +305,7 @@ impl ResponseError for SessionGetError {
/// Error returned by [`Session::insert`]. /// Error returned by [`Session::insert`].
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[display("{_0}")] #[display(fmt = "{_0}")]
pub struct SessionInsertError(anyhow::Error); pub struct SessionInsertError(anyhow::Error);
impl StdError for SessionInsertError { impl StdError for SessionInsertError {
@ -405,20 +319,3 @@ impl ResponseError for SessionInsertError {
HttpResponse::new(self.status_code()) 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())
}
}

View File

@ -31,7 +31,7 @@ impl SessionExt for ServiceResponse {
} }
} }
impl SessionExt for GuardContext<'_> { impl<'a> SessionExt for GuardContext<'a> {
fn get_session(&self) -> Session { fn get_session(&self) -> Session {
Session::get_session(&mut self.req_data_mut()) Session::get_session(&mut self.req_data_mut())
} }

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, future::Future}; use std::{collections::HashMap, future::Future};
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use derive_more::derive::Display; use derive_more::Display;
use super::SessionKey; use super::SessionKey;
@ -53,11 +53,11 @@ pub trait SessionStore {
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum LoadError { pub enum LoadError {
/// Failed to deserialize session state. /// Failed to deserialize session state.
#[display("Failed to deserialize session state")] #[display(fmt = "Failed to deserialize session state")]
Deserialization(anyhow::Error), Deserialization(anyhow::Error),
/// Something went wrong when retrieving the session state. /// Something went wrong when retrieving the session state.
#[display("Something went wrong when retrieving the session state")] #[display(fmt = "Something went wrong when retrieving the session state")]
Other(anyhow::Error), Other(anyhow::Error),
} }
@ -74,11 +74,11 @@ impl std::error::Error for LoadError {
#[derive(Debug, Display)] #[derive(Debug, Display)]
pub enum SaveError { pub enum SaveError {
/// Failed to serialize session state. /// Failed to serialize session state.
#[display("Failed to serialize session state")] #[display(fmt = "Failed to serialize session state")]
Serialization(anyhow::Error), Serialization(anyhow::Error),
/// Something went wrong when persisting the session state. /// Something went wrong when persisting the session state.
#[display("Something went wrong when persisting the session state")] #[display(fmt = "Something went wrong when persisting the session state")]
Other(anyhow::Error), Other(anyhow::Error),
} }
@ -95,11 +95,11 @@ impl std::error::Error for SaveError {
/// Possible failures modes for [`SessionStore::update`]. /// Possible failures modes for [`SessionStore::update`].
pub enum UpdateError { pub enum UpdateError {
/// Failed to serialize session state. /// Failed to serialize session state.
#[display("Failed to serialize session state")] #[display(fmt = "Failed to serialize session state")]
Serialization(anyhow::Error), Serialization(anyhow::Error),
/// Something went wrong when updating the session state. /// Something went wrong when updating the session state.
#[display("Something went wrong when updating the session state.")] #[display(fmt = "Something went wrong when updating the session state.")]
Other(anyhow::Error), Other(anyhow::Error),
} }

View File

@ -1,19 +1,28 @@
//! Pluggable storage backends for session state. //! Pluggable storage backends for session state.
#[cfg(feature = "cookie-session")]
mod cookie;
mod interface; mod interface;
#[cfg(feature = "redis-session")]
mod redis_rs;
mod session_key; mod session_key;
mod utils;
#[cfg(feature = "cookie-session")]
pub use self::cookie::CookieSessionStore;
#[cfg(feature = "redis-session")]
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
pub use self::{ pub use self::{
interface::{LoadError, SaveError, SessionStore, UpdateError}, interface::{LoadError, SaveError, SessionStore, UpdateError},
session_key::SessionKey, session_key::SessionKey,
utils::generate_session_key,
}; };
#[cfg(feature = "cookie-session")]
mod cookie;
#[cfg(feature = "redis-actor-session")]
mod redis_actor;
#[cfg(feature = "redis-rs-session")]
mod redis_rs;
#[cfg(any(feature = "redis-actor-session", feature = "redis-rs-session"))]
mod utils;
#[cfg(feature = "cookie-session")]
pub use cookie::CookieSessionStore;
#[cfg(feature = "redis-actor-session")]
pub use redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
#[cfg(feature = "redis-rs-session")]
pub use redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::Error; use anyhow::Error;
use redis::{aio::ConnectionManager, AsyncCommands, Client, Cmd, FromRedisValue, Value}; use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
use super::SessionKey; use super::SessionKey;
use crate::storage::{ use crate::storage::{
@ -44,7 +44,7 @@ use crate::storage::{
/// ``` /// ```
/// ///
/// # TLS support /// # TLS support
/// Add the `redis-session-native-tls` or `redis-session-rustls` feature flag to enable TLS support. You can then establish a TLS /// Add the `redis-rs-tls-session` feature flag to enable TLS support. You can then establish a TLS
/// connection to Redis using the `rediss://` URL scheme: /// connection to Redis using the `rediss://` URL scheme:
/// ///
/// ```no_run /// ```no_run
@ -56,38 +56,14 @@ use crate::storage::{
/// # }) /// # })
/// ``` /// ```
/// ///
/// # Pooled Redis Connections
///
/// 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 /// # Implementation notes
/// `RedisSessionStore` leverages [`redis-rs`] as Redis client.
/// ///
/// `RedisSessionStore` leverages the [`redis`] crate as the underlying Redis client. /// [`redis-rs`]: https://github.com/mitsuhiko/redis-rs
#[derive(Clone)] #[derive(Clone)]
pub struct RedisSessionStore { pub struct RedisSessionStore {
configuration: CacheConfiguration, configuration: CacheConfiguration,
client: RedisSessionConn, client: ConnectionManager,
}
#[derive(Clone)]
enum RedisSessionConn {
/// Single connection.
Single(ConnectionManager),
/// Connection pool.
#[cfg(feature = "redis-pool")]
Pool(deadpool_redis::Pool),
} }
#[derive(Clone)] #[derive(Clone)]
@ -104,77 +80,34 @@ impl Default for CacheConfiguration {
} }
impl RedisSessionStore { impl RedisSessionStore {
/// Returns a fluent API builder to configure [`RedisSessionStore`]. /// A fluent API to configure [`RedisSessionStore`].
/// /// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] /// connection string for Redis.
/// - a connection string for Redis. pub fn builder<S: Into<String>>(connection_string: S) -> RedisSessionStoreBuilder {
pub fn builder(connection_string: impl Into<String>) -> RedisSessionStoreBuilder {
RedisSessionStoreBuilder { RedisSessionStoreBuilder {
configuration: CacheConfiguration::default(), configuration: CacheConfiguration::default(),
conn_builder: RedisSessionConnBuilder::Single(connection_string.into()), connection_string: connection_string.into(),
} }
} }
/// Returns a fluent API builder to configure [`RedisSessionStore`]. /// 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
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] /// connection string for Redis.
/// - a pool object for Redis. pub async fn new<S: Into<String>>(
#[cfg(feature = "redis-pool")] connection_string: S,
pub fn builder_pooled(pool: impl Into<deadpool_redis::Pool>) -> RedisSessionStoreBuilder { ) -> Result<RedisSessionStore, anyhow::Error> {
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 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 /// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
/// parameters. /// parameters.
///
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore
#[must_use] #[must_use]
pub struct RedisSessionStoreBuilder { pub struct RedisSessionStoreBuilder {
connection_string: String,
configuration: CacheConfiguration, 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 { impl RedisSessionStoreBuilder {
@ -187,10 +120,11 @@ impl RedisSessionStoreBuilder {
self self
} }
/// Finalises builder and returns a [`RedisSessionStore`] instance. /// Finalise the builder and return a [`RedisActorSessionStore`] instance.
pub async fn build(self) -> anyhow::Result<RedisSessionStore> { ///
let client = self.conn_builder.into_client().await?; /// [`RedisActorSessionStore`]: crate::storage::RedisActorSessionStore
pub async fn build(self) -> Result<RedisSessionStore, anyhow::Error> {
let client = ConnectionManager::new(redis::Client::open(self.connection_string)?).await?;
Ok(RedisSessionStore { Ok(RedisSessionStore {
configuration: self.configuration, configuration: self.configuration,
client, client,
@ -205,6 +139,7 @@ impl SessionStore for RedisSessionStore {
let value: Option<String> = self let value: Option<String> = self
.execute_command(redis::cmd("GET").arg(&[&cache_key])) .execute_command(redis::cmd("GET").arg(&[&cache_key]))
.await .await
.map_err(Into::into)
.map_err(LoadError::Other)?; .map_err(LoadError::Other)?;
match value { match value {
@ -226,19 +161,15 @@ impl SessionStore for RedisSessionStore {
let session_key = generate_session_key(); let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.execute_command::<()>( self.execute_command(redis::cmd("SET").arg(&[
redis::cmd("SET") &cache_key,
.arg(&[ &body,
&cache_key, // key "NX", // NX: only set the key if it does not already exist
&body, // value "EX", // EX: set expiry
"NX", // only set the key if it does not already exist &format!("{}", ttl.whole_seconds()),
"EX", // set expiry / TTL ]))
])
.arg(
ttl.whole_seconds(), // EXpiry in seconds
),
)
.await .await
.map_err(Into::into)
.map_err(SaveError::Other)?; .map_err(SaveError::Other)?;
Ok(session_key) Ok(session_key)
@ -256,7 +187,7 @@ impl SessionStore for RedisSessionStore {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let v: Value = self let v: redis::Value = self
.execute_command(redis::cmd("SET").arg(&[ .execute_command(redis::cmd("SET").arg(&[
&cache_key, &cache_key,
&body, &body,
@ -265,6 +196,7 @@ impl SessionStore for RedisSessionStore {
&format!("{}", ttl.whole_seconds()), &format!("{}", ttl.whole_seconds()),
])) ]))
.await .await
.map_err(Into::into)
.map_err(UpdateError::Other)?; .map_err(UpdateError::Other)?;
match v { match v {
@ -280,7 +212,7 @@ impl SessionStore for RedisSessionStore {
SaveError::Other(err) => UpdateError::Other(err), SaveError::Other(err) => UpdateError::Other(err),
}) })
} }
Value::Int(_) | Value::Okay | Value::SimpleString(_) => Ok(session_key), Value::Int(_) | Value::Okay | Value::Status(_) => Ok(session_key),
val => Err(UpdateError::Other(anyhow::anyhow!( val => Err(UpdateError::Other(anyhow::anyhow!(
"Failed to update session state. {:?}", "Failed to update session state. {:?}",
val val
@ -288,33 +220,21 @@ impl SessionStore for RedisSessionStore {
} }
} }
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> anyhow::Result<()> { async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
match self.client { self.client
RedisSessionConn::Single(ref conn) => { .clone()
conn.clone() .expire(&cache_key, ttl.whole_seconds())
.expire::<_, ()>(&cache_key, ttl.whole_seconds()) .await?;
.await?;
}
#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
pool.get()
.await?
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
}
}
Ok(()) Ok(())
} }
async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> { async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
self.execute_command::<()>(redis::cmd("DEL").arg(&[&cache_key]))
.await .await
.map_err(Into::into)
.map_err(UpdateError::Other)?; .map_err(UpdateError::Other)?;
Ok(()) Ok(())
@ -336,55 +256,24 @@ impl RedisSessionStore {
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for /// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
/// a different more meaningful reason). /// a different more meaningful reason).
#[allow(clippy::needless_pass_by_ref_mut)] #[allow(clippy::needless_pass_by_ref_mut)]
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<T> { async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
let mut can_retry = true; let mut can_retry = true;
match self.client { loop {
RedisSessionConn::Single(ref conn) => { match cmd.query_async(&mut self.client.clone()).await {
let mut conn = conn.clone(); 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."
);
loop { // Retry at most once
match cmd.query_async(&mut conn).await { can_retry = false;
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 continue;
can_retry = false; } else {
return Err(err);
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());
}
}
} }
} }
} }
@ -397,27 +286,14 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use actix_web::cookie::time; use actix_web::cookie::time;
#[cfg(not(feature = "redis-session"))]
use deadpool_redis::{Config, Runtime};
use super::*; use super::*;
use crate::test_helpers::acceptance_test_suite; use crate::test_helpers::acceptance_test_suite;
async fn redis_store() -> RedisSessionStore { async fn redis_store() -> RedisSessionStore {
#[cfg(feature = "redis-session")] RedisSessionStore::new("redis://127.0.0.1:6379")
{ .await
RedisSessionStore::new("redis://127.0.0.1:6379") .unwrap()
.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] #[actix_web::test]
@ -437,25 +313,12 @@ mod tests {
async fn loading_an_invalid_session_state_returns_deserialization_error() { async fn loading_an_invalid_session_state_returns_deserialization_error() {
let store = redis_store().await; let store = redis_store().await;
let session_key = generate_session_key(); let session_key = generate_session_key();
store
match store.client { .client
RedisSessionConn::Single(ref conn) => conn .clone()
.clone() .set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json") .await
.await .unwrap();
.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!( assert!(matches!(
store.load(&session_key).await.unwrap_err(), store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_), LoadError::Deserialization(_),

View File

@ -1,4 +1,4 @@
use derive_more::derive::{Display, From}; use derive_more::{Display, From};
/// A session key, the string stored in a client-side cookie to associate a user with its session /// A session key, the string stored in a client-side cookie to associate a user with its session
/// state on the backend. /// state on the backend.
@ -7,7 +7,7 @@ use derive_more::derive::{Display, From};
/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are /// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
/// required to be smaller than 4064 bytes. /// required to be smaller than 4064 bytes.
/// ///
/// ``` /// ```rust
/// use actix_session::storage::SessionKey; /// use actix_session::storage::SessionKey;
/// ///
/// let key: String = std::iter::repeat('a').take(4065).collect(); /// let key: String = std::iter::repeat('a').take(4065).collect();
@ -45,7 +45,7 @@ impl From<SessionKey> for String {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[display("The provided string is not a valid session key")] #[display(fmt = "The provided string is not a valid session key")]
pub struct InvalidSessionKeyError(anyhow::Error); pub struct InvalidSessionKeyError(anyhow::Error);
impl std::error::Error for InvalidSessionKeyError { impl std::error::Error for InvalidSessionKeyError {

View File

@ -1,13 +1,17 @@
use rand::distr::{Alphanumeric, SampleString as _}; use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
use crate::storage::SessionKey; use crate::storage::SessionKey;
/// Session key generation routine that follows [OWASP recommendations]. /// Session key generation routine that follows [OWASP recommendations].
/// ///
/// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy /// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
pub fn generate_session_key() -> SessionKey { pub(crate) fn generate_session_key() -> SessionKey {
Alphanumeric let value = std::iter::repeat(())
.sample_string(&mut rand::rng(), 64) .map(|()| OsRng.sample(Alphanumeric))
.try_into() .take(64)
.expect("generated string should be within size range for a session key") .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()
} }

View File

@ -69,16 +69,6 @@ async fn session_entries() {
map.contains_key("test_num"); 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] #[actix_web::test]
async fn insert_session_after_renew() { async fn insert_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session(); let session = test::TestRequest::default().to_srv_request().get_session();
@ -93,35 +83,6 @@ async fn insert_session_after_renew() {
assert_eq!(session.status(), SessionStatus::Renewed); 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] #[actix_web::test]
async fn remove_session_after_renew() { async fn remove_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session(); let session = test::TestRequest::default().to_srv_request().get_session();

View File

@ -2,13 +2,6 @@
## Unreleased ## 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. - Minimum supported Rust version (MSRV) is now 1.75.
## 0.7.1 ## 0.7.1

View File

@ -1,30 +1,25 @@
[package] [package]
name = "actix-settings" name = "actix-settings"
version = "0.8.0" version = "0.7.1"
authors = [ authors = [
"Joey Ezechiels <joey.ezechiels@gmail.com>", "Joey Ezechiels <joey.ezechiels@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
description = "Easily manage Actix Web's settings from a TOML file and environment variables" description = "Easily manage Actix Web's settings from a TOML file and environment variables"
repository.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features]
openssl = ["dep:openssl", "actix-web/openssl"]
[dependencies] [dependencies]
actix-http = "3" actix-http = "3"
actix-service = "2" actix-service = "2"
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display", "error"] } derive_more = "0.99.7"
once_cell = "1.21" once_cell = "1.13"
openssl = { version = "0.10", features = ["v110"], optional = true }
regex = "1.5" regex = "1.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = "0.8" toml = "0.8"
@ -32,6 +27,3 @@ toml = "0.8"
[dev-dependencies] [dev-dependencies]
actix-web = "4" actix-web = "4"
env_logger = "0.11" env_logger = "0.11"
[lints]
workspace = true

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings) [![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings)
[![Documentation](https://docs.rs/actix-settings/badge.svg?version=0.8.0)](https://docs.rs/actix-settings/0.8.0) [![Documentation](https://docs.rs/actix-settings/badge.svg?version=0.7.1)](https://docs.rs/actix-settings/0.7.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-settings) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-settings)
[![Dependency Status](https://deps.rs/crate/actix-settings/0.8.0/status.svg)](https://deps.rs/crate/actix-settings/0.8.0) [![Dependency Status](https://deps.rs/crate/actix-settings/0.7.1/status.svg)](https://deps.rs/crate/actix-settings/0.7.1)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@ -23,6 +23,10 @@ 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. Have a look at [the usage example][usage] to see how.
## WIP
Configuration options for TLS set up are not yet implemented.
## Special Thanks ## Special Thanks
This crate was made possible by support from Accept B.V and [@jjpe]. This crate was made possible by support from Accept B.V and [@jjpe].

View File

@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> {
} }
}) })
// apply the `Settings` to Actix Web's `HttpServer` // apply the `Settings` to Actix Web's `HttpServer`
.try_apply_settings(&settings)? .apply_settings(&settings)
.run() .run()
.await .await
} }

View File

@ -1,24 +1,22 @@
use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError}; use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError};
use derive_more::derive::{Display, Error}; use derive_more::{Display, Error};
#[cfg(feature = "openssl")]
use openssl::error::ErrorStack as OpenSSLError;
use toml::de::Error as TomlError; use toml::de::Error as TomlError;
/// Errors that can be returned from methods in this crate. /// Errors that can be returned from methods in this crate.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
pub enum Error { pub enum Error {
/// Environment variable does not exists or is invalid. /// Environment variable does not exists or is invalid.
#[display("Env var error: {_0}")] #[display(fmt = "Env var error: {_0}")]
EnvVarError(VarError), EnvVarError(VarError),
/// File already exists on disk. /// File already exists on disk.
#[display("File exists: {}", _0.display())] #[display(fmt = "File exists: {}", "_0.display()")]
FileExists(#[error(not(source))] PathBuf), FileExists(#[error(not(source))] PathBuf),
/// Invalid value. /// Invalid value.
#[allow(missing_docs)] #[allow(missing_docs)]
#[display("Expected {expected}, got {got} (@ {file}:{line}:{column})")] #[display(fmt = "Expected {expected}, got {got} (@ {file}:{line}:{column})")]
InvalidValue { InvalidValue {
expected: &'static str, expected: &'static str,
got: String, got: String,
@ -28,28 +26,23 @@ pub enum Error {
}, },
/// I/O error. /// I/O error.
#[display("I/O error: {_0}")] #[display(fmt = "")]
IoError(io::Error), IoError(io::Error),
/// OpenSSL Error.
#[cfg(feature = "openssl")]
#[display("OpenSSL error: {_0}")]
OpenSSLError(OpenSSLError),
/// Value is not a boolean. /// Value is not a boolean.
#[display("Failed to parse boolean: {_0}")] #[display(fmt = "Failed to parse boolean: {_0}")]
ParseBoolError(ParseBoolError), ParseBoolError(ParseBoolError),
/// Value is not an integer. /// Value is not an integer.
#[display("Failed to parse integer: {_0}")] #[display(fmt = "Failed to parse integer: {_0}")]
ParseIntError(ParseIntError), ParseIntError(ParseIntError),
/// Value is not an address. /// Value is not an address.
#[display("Failed to parse address: {_0}")] #[display(fmt = "Failed to parse address: {_0}")]
ParseAddressError(#[error(not(source))] String), ParseAddressError(#[error(not(source))] String),
/// Error deserializing as TOML. /// Error deserializing as TOML.
#[display("TOML error: {_0}")] #[display(fmt = "TOML error: {_0}")]
TomlError(TomlError), TomlError(TomlError),
} }
@ -71,13 +64,6 @@ 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 { impl From<ParseBoolError> for Error {
fn from(err: ParseBoolError) -> Self { fn from(err: ParseBoolError) -> Self {
Self::ParseBoolError(err) Self::ParseBoolError(err)
@ -115,9 +101,6 @@ impl From<Error> for io::Error {
Error::IoError(io_error) => 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(_) => { Error::ParseBoolError(_) => {
io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
} }

View File

@ -54,14 +54,15 @@
//! } //! }
//! }) //! })
//! // apply the `Settings` to Actix Web's `HttpServer` //! // apply the `Settings` to Actix Web's `HttpServer`
//! .try_apply_settings(&settings)? //! .apply_settings(&settings)
//! .run() //! .run()
//! .await //! .await
//! } //! }
//! ``` //! ```
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
@ -89,14 +90,12 @@ mod error;
mod parse; mod parse;
mod settings; mod settings;
#[cfg(feature = "openssl")]
pub use self::settings::Tls;
pub use self::{ pub use self::{
error::Error, error::Error,
parse::Parse, parse::Parse,
settings::{ settings::{
ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode, ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode,
NumWorkers, Timeout, NumWorkers, Timeout, Tls,
}, },
}; };
@ -241,28 +240,10 @@ where
} }
/// Extension trait for applying parsed settings to the server object. /// Extension trait for applying parsed settings to the server object.
pub trait ApplySettings<S>: Sized { pub trait ApplySettings<S> {
/// Applies some settings object value to `self`. /// Apply some settings object value to `self`.
/// #[must_use]
/// The default implementation calls [`try_apply_settings()`]. fn apply_settings(self, settings: &S) -> Self;
///
/// # 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> impl<F, I, S, B> ApplySettings<ActixSettings> for HttpServer<F, I, S, B>
@ -276,27 +257,17 @@ where
S::Future: 'static, S::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
fn apply_settings(self, settings: &ActixSettings) -> Self { fn apply_settings(mut self, settings: &ActixSettings) -> Self {
self.try_apply_settings(settings).unwrap() if settings.tls.enabled {
} // for Address { host, port } in &settings.actix.hosts {
// self = self.bind(format!("{}:{}", host, port))
fn try_apply_settings(mut self, settings: &ActixSettings) -> AsResult<Self> { // .unwrap(/*TODO*/);
for Address { host, port } in &settings.hosts { // }
#[cfg(feature = "openssl")] unimplemented!("[ApplySettings] TLS support has not been implemented yet.");
{ } else {
if settings.tls.enabled { for Address { host, port } in &settings.hosts {
self = self.bind_openssl( self = self.bind(format!("{host}:{port}"))
format!("{}:{}", host, port), .unwrap(/*TODO*/);
settings.tls.get_ssl_acceptor_builder()?,
)?;
} else {
self = self.bind(format!("{host}:{port}"))?;
}
}
#[cfg(not(feature = "openssl"))]
{
self = self.bind(format!("{host}:{port}"))?;
} }
} }
@ -349,7 +320,7 @@ where
Timeout::Seconds(n) => self.shutdown_timeout(n as u64), Timeout::Seconds(n) => self.shutdown_timeout(n as u64),
}; };
Ok(self) self
} }
} }
@ -366,11 +337,7 @@ where
A: de::DeserializeOwned, A: de::DeserializeOwned,
{ {
fn apply_settings(self, settings: &BasicSettings<A>) -> Self { fn apply_settings(self, settings: &BasicSettings<A>) -> Self {
self.try_apply_settings(&settings.actix).unwrap() self.apply_settings(&settings.actix)
}
fn try_apply_settings(self, settings: &BasicSettings<A>) -> AsResult<Self> {
self.try_apply_settings(&settings.actix)
} }
} }
@ -383,8 +350,7 @@ mod tests {
#[test] #[test]
fn apply_settings() { fn apply_settings() {
let settings = Settings::parse_toml("Server.toml").unwrap(); let settings = Settings::parse_toml("Server.toml").unwrap();
let server = HttpServer::new(App::new).try_apply_settings(&settings); let _ = HttpServer::new(App::new).apply_settings(&settings);
assert!(server.is_ok());
} }
#[test] #[test]
@ -697,7 +663,6 @@ mod tests {
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42)); assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_tls_enabled() { fn override_field_tls_enabled() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();
@ -706,7 +671,6 @@ mod tests {
assert!(settings.actix.tls.enabled); assert!(settings.actix.tls.enabled);
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_with_env_var_tls_enabled() { fn override_field_with_env_var_tls_enabled() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();
@ -720,7 +684,6 @@ mod tests {
assert!(settings.actix.tls.enabled); assert!(settings.actix.tls.enabled);
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_tls_certificate() { fn override_field_tls_certificate() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();
@ -739,7 +702,6 @@ mod tests {
); );
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_with_env_var_tls_certificate() { fn override_field_with_env_var_tls_certificate() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();
@ -762,7 +724,6 @@ mod tests {
); );
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_tls_private_key() { fn override_field_tls_private_key() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();
@ -781,7 +742,6 @@ mod tests {
); );
} }
#[cfg(feature = "openssl")]
#[test] #[test]
fn override_field_with_env_var_tls_private_key() { fn override_field_with_env_var_tls_private_key() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template();

View File

@ -43,7 +43,7 @@ impl<'de> de::Deserialize<'de> for Backlog {
{ {
struct BacklogVisitor; struct BacklogVisitor;
impl de::Visitor<'_> for BacklogVisitor { impl<'de> de::Visitor<'de> for BacklogVisitor {
type Value = Backlog; type Value = Backlog;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -68,7 +68,7 @@ impl<'de> de::Deserialize<'de> for KeepAlive {
{ {
struct KeepAliveVisitor; struct KeepAliveVisitor;
impl de::Visitor<'_> for KeepAliveVisitor { impl<'de> de::Visitor<'de> for KeepAliveVisitor {
type Value = KeepAlive; type Value = KeepAlive;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnectionRate {
{ {
struct MaxConnectionRateVisitor; struct MaxConnectionRateVisitor;
impl de::Visitor<'_> for MaxConnectionRateVisitor { impl<'de> de::Visitor<'de> for MaxConnectionRateVisitor {
type Value = MaxConnectionRate; type Value = MaxConnectionRate;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnections {
{ {
struct MaxConnectionsVisitor; struct MaxConnectionsVisitor;
impl de::Visitor<'_> for MaxConnectionsVisitor { impl<'de> de::Visitor<'de> for MaxConnectionsVisitor {
type Value = MaxConnections; type Value = MaxConnections;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -8,15 +8,12 @@ mod max_connections;
mod mode; mod mode;
mod num_workers; mod num_workers;
mod timeout; mod timeout;
#[cfg(feature = "openssl")]
mod tls; mod tls;
#[cfg(feature = "openssl")]
pub use self::tls::Tls;
pub use self::{ pub use self::{
address::Address, backlog::Backlog, keep_alive::KeepAlive, address::Address, backlog::Backlog, keep_alive::KeepAlive,
max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode, max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode,
num_workers::NumWorkers, timeout::Timeout, num_workers::NumWorkers, timeout::Timeout, tls::Tls,
}; };
/// Settings types for Actix Web. /// Settings types for Actix Web.
@ -60,6 +57,5 @@ pub struct ActixSettings {
pub shutdown_timeout: Timeout, pub shutdown_timeout: Timeout,
/// TLS (HTTPS) configuration. /// TLS (HTTPS) configuration.
#[cfg(feature = "openssl")]
pub tls: Tls, pub tls: Tls,
} }

View File

@ -39,7 +39,7 @@ impl<'de> de::Deserialize<'de> for NumWorkers {
{ {
struct NumWorkersVisitor; struct NumWorkersVisitor;
impl de::Visitor<'_> for NumWorkersVisitor { impl<'de> de::Visitor<'de> for NumWorkersVisitor {
type Value = NumWorkers; type Value = NumWorkers;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -71,7 +71,7 @@ impl<'de> de::Deserialize<'de> for Timeout {
{ {
struct TimeoutVisitor; struct TimeoutVisitor;
impl de::Visitor<'_> for TimeoutVisitor { impl<'de> de::Visitor<'de> for TimeoutVisitor {
type Value = Timeout; type Value = Timeout;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -1,16 +1,13 @@
use std::path::PathBuf; use std::path::PathBuf;
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
use serde::Deserialize; use serde::Deserialize;
use crate::AsResult;
/// TLS (HTTPS) configuration. /// TLS (HTTPS) configuration.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[doc(alias = "ssl", alias = "https")] #[doc(alias = "ssl", alias = "https")]
pub struct Tls { pub struct Tls {
/// True if accepting TLS connections should be enabled. /// Tru if accepting TLS connections should be enabled.
pub enabled: bool, pub enabled: bool,
/// Path to certificate `.pem` file. /// Path to certificate `.pem` file.
@ -19,39 +16,3 @@ pub struct Tls {
/// Path to private key `.pem` file. /// Path to private key `.pem` file.
pub private_key: PathBuf, 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)
}
}

View File

@ -2,8 +2,6 @@
## Unreleased ## Unreleased
## 0.8.2
- Minimum supported Rust version (MSRV) is now 1.75. - Minimum supported Rust version (MSRV) is now 1.75.
## 0.8.1 ## 0.8.1

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-httpauth" name = "actix-web-httpauth"
version = "0.8.2" version = "0.8.1"
description = "HTTP authentication schemes for Actix Web" description = "HTTP authentication schemes for Actix Web"
categories = ["web-programming"] categories = ["web-programming"]
keywords = ["http", "web", "framework", "authentication", "security"] keywords = ["http", "web", "framework", "authentication", "security"]
@ -8,8 +8,8 @@ authors = [
"svartalf <self@svartalf.info>", "svartalf <self@svartalf.info>",
"Yuki Okushi <huyuumi.dev@gmail.com>", "Yuki Okushi <huyuumi.dev@gmail.com>",
] ]
repository.workspace = true homepage = "https://actix.rs"
homepage.workspace = true repository = "https://github.com/actix/actix-extras"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -32,8 +32,3 @@ pin-project-lite = "0.2.7"
actix-cors = "0.7" actix-cors = "0.7"
actix-service = "2" actix-service = "2"
actix-web = { version = "4.1", default-features = false, features = ["macros"] } actix-web = { version = "4.1", default-features = false, features = ["macros"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1.30"
[lints]
workspace = true

View File

@ -5,9 +5,9 @@
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth) [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth)
[![Documentation](https://docs.rs/actix-web-httpauth/badge.svg?version=0.8.2)](https://docs.rs/actix-web-httpauth/0.8.2) [![Documentation](https://docs.rs/actix-web-httpauth/badge.svg?version=0.8.1)](https://docs.rs/actix-web-httpauth/0.8.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-web-httpauth) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-web-httpauth)
[![Dependency Status](https://deps.rs/crate/actix-web-httpauth/0.8.2/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.8.2) [![Dependency Status](https://deps.rs/crate/actix-web-httpauth/0.8.1/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.8.1)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -1,57 +1,24 @@
use actix_web::{ use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpServer};
dev::ServiceRequest, error, get, middleware::Logger, App, Error, HttpServer, Responder, use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication};
};
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
/// Validator that:
/// - accepts Bearer auth;
/// - returns a custom response for requests without a valid Bearer Authorization header;
/// - rejects tokens containing an "x" (for quick testing using command line HTTP clients).
async fn validator( async fn validator(
req: ServiceRequest, req: ServiceRequest,
credentials: Option<BearerAuth>, _credentials: BasicAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> { ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let Some(credentials) = credentials else {
return Err((error::ErrorBadRequest("no bearer header"), req));
};
eprintln!("{credentials:?}");
if credentials.token().contains('x') {
return Err((error::ErrorBadRequest("token contains x"), req));
}
Ok(req) Ok(req)
} }
#[get("/")]
async fn index(auth: BearerAuth) -> impl Responder {
format!("authenticated for token: {}", auth.token().to_owned())
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.without_time()
.init();
HttpServer::new(|| { HttpServer::new(|| {
let auth = HttpAuthentication::with_fn(validator); let auth = HttpAuthentication::basic(validator);
App::new() App::new()
.service(index) .wrap(middleware::Logger::default())
.wrap(auth) .wrap(auth)
.wrap(Logger::default().log_target("@")) .service(web::resource("/").to(|| async { "Test\r\n" }))
}) })
.bind("127.0.0.1:8080")? .bind("127.0.0.1:8080")?
.workers(2) .workers(1)
.run() .run()
.await .await
} }

View File

@ -15,7 +15,8 @@
//! [Middleware]: self::middleware //! [Middleware]: self::middleware
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible, missing_docs)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@ -43,55 +43,6 @@ where
{ {
/// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and /// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
/// validation callback `F`. /// validation callback `F`.
///
/// This function can be used to implement optional authentication and/or custom responses to
/// missing authentication.
///
/// # Examples
///
/// ## Required Basic Auth
///
/// ```no_run
/// # use actix_web_httpauth::extractors::basic::BasicAuth;
/// # use actix_web::dev::ServiceRequest;
/// async fn validator(
/// req: ServiceRequest,
/// credentials: BasicAuth,
/// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
/// eprintln!("{credentials:?}");
///
/// if credentials.user_id().contains('x') {
/// return Err((actix_web::error::ErrorBadRequest("user ID contains x"), req));
/// }
///
/// Ok(req)
/// }
/// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
/// ```
///
/// ## Optional Bearer Auth
///
/// ```no_run
/// # use actix_web_httpauth::extractors::bearer::BearerAuth;
/// # use actix_web::dev::ServiceRequest;
/// async fn validator(
/// req: ServiceRequest,
/// credentials: Option<BearerAuth>,
/// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
/// let Some(credentials) = credentials else {
/// return Err((actix_web::error::ErrorBadRequest("no bearer header"), req));
/// };
///
/// eprintln!("{credentials:?}");
///
/// if credentials.token().contains('x') {
/// return Err((actix_web::error::ErrorBadRequest("token contains x"), req));
/// }
///
/// Ok(req)
/// }
/// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
/// ```
pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> { pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
HttpAuthentication { HttpAuthentication {
process_fn: Arc::new(process_fn), process_fn: Arc::new(process_fn),

View File

@ -12,8 +12,8 @@ struct Quoted<'a> {
state: State, state: State,
} }
impl Quoted<'_> { impl<'a> Quoted<'a> {
pub fn new(s: &str) -> Quoted<'_> { pub fn new(s: &'a str) -> Quoted<'_> {
Quoted { Quoted {
inner: s.split('"').peekable(), inner: s.split('"').peekable(),
state: State::YieldStr, state: State::YieldStr,

View File

@ -2,16 +2,8 @@
## Unreleased ## Unreleased
- 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. - Remove type parameters from `Session::{text, binary}()` methods, replacing with equivalent `impl Trait` parameters.
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream. - `Session::text()` now receives an `impl Into<ByteString>`, making broadcasting text messages more efficient.
## 0.2.5 ## 0.2.5

View File

@ -1,15 +1,14 @@
[package] [package]
name = "actix-ws" name = "actix-ws"
version = "0.3.0" version = "0.2.0"
description = "WebSockets for Actix Web, without actors" description = "WebSockets for Actix Web, without actors"
categories = ["web-programming::websocket"] categories = ["web-programming::websocket"]
keywords = ["actix", "web", "websocket", "websockets", "streaming"] keywords = ["actix", "web", "websocket", "websockets", "http"]
authors = [ authors = [
"asonix <asonix@asonix.dog>", "asonix <asonix@asonix.dog>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
repository.workspace = true repository = "https://github.com/actix/actix-extras"
homepage.workspace = true
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@ -20,14 +19,13 @@ actix-http = { version = "3", default-features = false, features = ["ws"] }
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false }
bytestring = "1" bytestring = "1"
futures-core = "0.3.17" futures-core = "0.3.17"
tokio = { version = "1.24", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = "4.8" actix-rt = "2.6"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } actix-web = "4.0.1"
tokio = { version = "1.24", features = ["sync", "rt", "macros"] } anyhow = "1.0"
tracing = "0.1.30" futures-util = "0.3.17"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } log = "0.4"
pretty_env_logger = "0.5"
[lints] tokio = { version = "1", features = ["sync"] }
workspace = true

View File

@ -1,38 +1,48 @@
# `actix-ws` # Actix WS (Next Gen)
> WebSockets for Actix Web, without actors. > WebSockets for Actix Web, without actors.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws) [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws)
[![Documentation](https://docs.rs/actix-ws/badge.svg?version=0.3.0)](https://docs.rs/actix-ws/0.3.0) [![Documentation](https://docs.rs/actix-ws/badge.svg?version=0.2.0)](https://docs.rs/actix-ws/0.2.0)
![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-ws)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-ws.svg) [![Dependency Status](https://deps.rs/crate/actix-ws/0.2.0/status.svg)](https://deps.rs/crate/actix-ws/0.2.0)
<br />
[![Dependency Status](https://deps.rs/crate/actix-ws/0.3.0/status.svg)](https://deps.rs/crate/actix-ws/0.3.0)
[![Download](https://img.shields.io/crates/d/actix-ws.svg)](https://crates.io/crates/actix-ws)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
## Example ## 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
```toml
# Cargo.toml
anyhow = "1"
actix-web = "4"
actix-ws-ng = "0.3"
```
```rust ```rust
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder}; // main.rs
use actix_web::{middleware::Logger, web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_ws::Message; use actix_ws::Message;
async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result<impl Responder> { async fn ws(req: HttpRequest, body: web::Payload) -> Result<HttpResponse, Error> {
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
actix_web::rt::spawn(async move { actix_rt::spawn(async move {
while let Some(Ok(msg)) = msg_stream.recv().await { while let Some(Ok(msg)) = msg_stream.next().await {
match msg { match msg {
Message::Ping(bytes) => { Message::Ping(bytes) => {
if session.pong(&bytes).await.is_err() { if session.pong(&bytes).await.is_err() {
return; return;
} }
} }
Message::Text(msg) => println!("Got text: {msg}"), Message::Text(s) => println!("Got text, {}", s),
_ => break, _ => break,
} }
} }
@ -44,7 +54,7 @@ async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result<impl Resp
} }
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> Result<(), anyhow::Error> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(Logger::default()) .wrap(Logger::default())
@ -58,12 +68,6 @@ 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 ## License
This project is licensed under either of This project is licensed under either of

View File

@ -1,69 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<title>Chat</title>
<script>
function onLoad() {
console.log("BOOTING");
const socket = new WebSocket("ws://localhost:8080/ws");
const input = document.getElementById("chat-input");
const logs = document.getElementById("chat-logs");
if (!input || !logs) {
alert("Couldn't find required elements");
console.err("Couldn't find required elements");
return;
}
input.addEventListener(
"keyup",
(event) => {
if (event.isComposing) {
return;
}
if (event.key != "Enter") {
return;
}
socket.send(input.value);
input.value = "";
},
false
);
socket.onmessage = (event) => {
const newNode = document.createElement("li");
newNode.textContent = event.data;
let firstChild = null;
for (const n of logs.childNodes.values()) {
if (n.nodeType == 1) {
firstChild = n;
break;
}
}
if (firstChild) {
logs.insertBefore(newNode, firstChild);
} else {
logs.appendChild(newNode);
}
};
window.addEventListener("beforeunload", () => {
socket.close();
});
}
if (document.readyState === "complete") {
onLoad();
} else {
document.addEventListener("DOMContentLoaded", onLoad, false);
}
</script>
</head>
<body>
<input id="chat-input" type="test" />
<ul id="chat-logs"></ul>
</body>
</html>

View File

@ -1,18 +1,13 @@
use std::{ use std::{
io,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use actix_web::{ use actix_web::{middleware::Logger, web, App, HttpRequest, HttpResponse, HttpServer};
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 futures_util::{stream::FuturesUnordered, StreamExt as _};
use log::info;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
#[derive(Clone)] #[derive(Clone)]
struct Chat { struct Chat {
@ -36,19 +31,15 @@ impl Chat {
self.inner.lock().await.sessions.push(session); self.inner.lock().await.sessions.push(session);
} }
async fn send(&self, msg: impl Into<ByteString>) { async fn send(&self, msg: String) {
let msg = msg.into();
let mut inner = self.inner.lock().await; let mut inner = self.inner.lock().await;
let mut unordered = FuturesUnordered::new(); let mut unordered = FuturesUnordered::new();
for mut session in inner.sessions.drain(..) { for mut session in inner.sessions.drain(..) {
let msg = msg.clone(); let msg = msg.clone();
unordered.push(async move { unordered.push(async move {
let res = session.text(msg).await; let res = session.text(msg).await;
res.map(|_| session) res.map(|_| session).map_err(|_| info!("Dropping session"))
.map_err(|_| tracing::debug!("Dropping session"))
}); });
} }
@ -65,21 +56,17 @@ async fn ws(
body: web::Payload, body: web::Payload,
chat: web::Data<Chat>, chat: web::Data<Chat>,
) -> Result<HttpResponse, actix_web::Error> { ) -> Result<HttpResponse, actix_web::Error> {
let (response, mut session, stream) = actix_ws::handle(&req, body)?; let (response, mut session, mut 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; chat.insert(session.clone()).await;
tracing::info!("Inserted session"); info!("Inserted session");
let alive = Arc::new(Mutex::new(Instant::now())); let alive = Arc::new(Mutex::new(Instant::now()));
let mut session2 = session.clone(); let mut session2 = session.clone();
let alive2 = alive.clone(); let alive2 = alive.clone();
actix_web::rt::spawn(async move { actix_rt::spawn(async move {
let mut interval = actix_web::rt::time::interval(Duration::from_secs(5)); let mut interval = actix_rt::time::interval(Duration::from_secs(5));
loop { loop {
interval.tick().await; interval.tick().await;
if session2.ping(b"").await.is_err() { if session2.ping(b"").await.is_err() {
@ -93,54 +80,117 @@ async fn ws(
} }
}); });
actix_web::rt::spawn(async move { actix_rt::spawn(async move {
while let Some(Ok(msg)) = stream.recv().await { while let Some(Ok(msg)) = stream.next().await {
match msg { match msg {
AggregatedMessage::Ping(bytes) => { Message::Ping(bytes) => {
if session.pong(&bytes).await.is_err() { if session.pong(&bytes).await.is_err() {
return; return;
} }
} }
Message::Text(s) => {
AggregatedMessage::Text(string) => { info!("Relaying text, {}", s);
tracing::info!("Relaying text, {string}"); let s: &str = s.as_ref();
chat.send(string).await; chat.send(s.into()).await;
} }
Message::Close(reason) => {
AggregatedMessage::Close(reason) => {
let _ = session.close(reason).await; let _ = session.close(reason).await;
tracing::info!("Got close, bailing"); info!("Got close, bailing");
return; return;
} }
Message::Continuation(_) => {
AggregatedMessage::Pong(_) => { let _ = session.close(None).await;
info!("Got continuation, bailing");
return;
}
Message::Pong(_) => {
*alive.lock().await = Instant::now(); *alive.lock().await = Instant::now();
} }
_ => (), _ => (),
}; };
} }
let _ = session.close(None).await; let _ = session.close(None).await;
}); });
tracing::info!("Spawned"); info!("Spawned");
Ok(response) Ok(response)
} }
async fn index() -> impl Responder { async fn index() -> HttpResponse {
Html::new(include_str!("chat.html").to_owned()) let s = r#"
<html>
<head>
<meta charset="utf-8" />
<title>Chat</title>
<script>
function onLoad() {
console.log("BOOTING");
const socket = new WebSocket("ws://localhost:8080/ws");
const input = document.getElementById("chat-input");
const logs = document.getElementById("chat-logs");
if (!input || !logs) {
alert("Couldn't find required elements");
console.err("Couldn't find required elements");
return;
}
input.addEventListener("keyup", event => {
if (event.isComposing) {
return;
}
if (event.key != "Enter") {
return;
}
socket.send(input.value);
input.value = "";
}, false);
socket.onmessage = event => {
const newNode = document.createElement("li");
newNode.textContent = event.data;
let firstChild = null;
for (const n of logs.childNodes.values()) {
if (n.nodeType == 1) {
firstChild = n;
break;
}
}
if (firstChild) {
logs.insertBefore(newNode, firstChild);
} else {
logs.appendChild(newNode);
}
};
window.addEventListener("beforeunload", () => { socket.close() });
} }
#[tokio::main(flavor = "current_thread")] if (document.readyState === "complete") {
async fn main() -> io::Result<()> { onLoad();
tracing_subscriber::fmt() } else {
.with_env_filter( document.addEventListener("DOMContentLoaded", onLoad, false);
EnvFilter::builder() }
.with_default_directive(LevelFilter::INFO.into()) </script>
.from_env_lossy(), </head>
) <body>
.init(); <input id="chat-input" type="test" />
<ul id="chat-logs">
</ul>
</body>
</html>
"#;
HttpResponse::Ok().content_type("text/html").body(s)
}
#[actix_rt::main]
async fn main() -> Result<(), anyhow::Error> {
std::env::set_var("RUST_LOG", "info");
pretty_env_logger::init();
let chat = Chat::new(); let chat = Chat::new();
HttpServer::new(move || { HttpServer::new(move || {

View File

@ -1,216 +0,0 @@
//! 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()
}

View File

@ -1,7 +1,7 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
future::poll_fn, future::poll_fn,
io, mem, io,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -15,15 +15,25 @@ use actix_web::{
web::{Bytes, BytesMut}, web::{Bytes, BytesMut},
Error, Error,
}; };
use bytestring::ByteString;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use crate::AggregatedMessageStream; /// A response body for Websocket HTTP Requests
/// Response body for a WebSocket.
pub struct StreamingBody { pub struct StreamingBody {
session_rx: Receiver<Message>, 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>, messages: VecDeque<Message>,
buf: BytesMut, buf: BytesMut,
codec: Codec, codec: Codec,
@ -42,16 +52,6 @@ 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 { impl MessageStream {
pub(super) fn new(payload: Payload) -> Self { pub(super) fn new(payload: Payload) -> Self {
MessageStream { MessageStream {
@ -63,50 +63,13 @@ impl MessageStream {
} }
} }
/// Sets the maximum permitted size for received WebSocket frames, in bytes. /// Wait for the next item from the message stream
/// ///
/// By default, up to 64KiB is allowed. /// ```rust,ignore
///
/// 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;
/// # async fn test(mut stream: MessageStream) {
/// while let Some(Ok(msg)) = stream.recv().await { /// while let Some(Ok(msg)) = stream.recv().await {
/// // handle message /// // handle message
/// } /// }
/// # }
/// ``` /// ```
#[must_use]
pub async fn recv(&mut self) -> Option<Result<Message, ProtocolError>> { pub async fn recv(&mut self) -> Option<Result<Message, ProtocolError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
} }
@ -136,17 +99,13 @@ impl Stream for StreamingBody {
} }
while let Some(msg) = this.messages.pop_front() { while let Some(msg) = this.messages.pop_front() {
if let Err(err) = this.codec.encode(msg, &mut this.buf) { if let Err(e) = this.codec.encode(msg, &mut this.buf) {
return Poll::Ready(Some(Err(err.into()))); return Poll::Ready(Some(Err(e.into())));
} }
} }
if !this.buf.is_empty() { if !this.buf.is_empty() {
return Poll::Ready(Some(Ok(mem::take(&mut this.buf).freeze()))); return Poll::Ready(Some(Ok(this.buf.split().freeze())));
}
if this.closing {
return Poll::Ready(None);
} }
Poll::Pending Poll::Pending
@ -173,8 +132,11 @@ impl Stream for MessageStream {
Poll::Ready(Some(Ok(bytes))) => { Poll::Ready(Some(Ok(bytes))) => {
this.buf.extend_from_slice(&bytes); this.buf.extend_from_slice(&bytes);
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::other(err))))); return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new(
io::ErrorKind::Other,
e.to_string(),
)))));
} }
Poll::Ready(None) => { Poll::Ready(None) => {
this.closing = true; this.closing = true;
@ -189,11 +151,12 @@ impl Stream for MessageStream {
while let Some(frame) = this.codec.decode(&mut this.buf)? { while let Some(frame) = this.codec.decode(&mut this.buf)? {
let message = match frame { let message = match frame {
Frame::Text(bytes) => { Frame::Text(bytes) => {
ByteString::try_from(bytes) let s = std::str::from_utf8(&bytes)
.map(Message::Text) .map_err(|e| {
.map_err(|err| { ProtocolError::Io(io::Error::new(io::ErrorKind::Other, e.to_string()))
ProtocolError::Io(io::Error::new(io::ErrorKind::InvalidData, err))
})? })?
.to_string();
Message::Text(s.into())
} }
Frame::Binary(bytes) => Message::Binary(bytes), Frame::Binary(bytes) => Message::Binary(bytes),
Frame::Ping(bytes) => Message::Ping(bytes), Frame::Ping(bytes) => Message::Ping(bytes),

View File

@ -2,12 +2,13 @@
//! //!
//! For usage, see documentation on [`handle()`]. //! For usage, see documentation on [`handle()`].
#![deny(rust_2018_idioms, nonstandard_style, future_incompatible)]
#![warn(missing_docs)] #![warn(missing_docs)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use actix_http::ws::{CloseCode, CloseReason, Item, Message, ProtocolError}; pub use actix_http::ws::{CloseCode, CloseReason, Message, ProtocolError};
use actix_http::{ use actix_http::{
body::{BodyStream, MessageBody}, body::{BodyStream, MessageBody},
ws::handshake, ws::handshake,
@ -15,28 +16,25 @@ use actix_http::{
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use tokio::sync::mpsc::channel; use tokio::sync::mpsc::channel;
mod aggregated; mod fut;
mod session; mod session;
mod stream;
pub use self::{ pub use self::{
aggregated::{AggregatedMessage, AggregatedMessageStream}, fut::{MessageStream, StreamingBody},
session::{Closed, Session}, session::{Closed, Session},
stream::{MessageStream, StreamingBody},
}; };
/// Begin handling websocket traffic /// Begin handling websocket traffic
/// ///
/// ```no_run /// ```no_run
/// use std::io; /// use actix_web::{middleware::Logger, web, App, Error, HttpRequest, HttpResponse, HttpServer};
/// use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder};
/// use actix_ws::Message; /// use actix_ws::Message;
/// use futures_util::StreamExt as _; /// use futures::stream::StreamExt as _;
/// ///
/// async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result<impl Responder> { /// async fn ws(req: HttpRequest, body: web::Payload) -> Result<HttpResponse, Error> {
/// let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; /// let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
/// ///
/// actix_web::rt::spawn(async move { /// actix_rt::spawn(async move {
/// while let Some(Ok(msg)) = msg_stream.next().await { /// while let Some(Ok(msg)) = msg_stream.next().await {
/// match msg { /// match msg {
/// Message::Ping(bytes) => { /// Message::Ping(bytes) => {
@ -44,8 +42,7 @@ pub use self::{
/// return; /// return;
/// } /// }
/// } /// }
/// /// Message::Text(s) => println!("Got text, {}", s),
/// Message::Text(msg) => println!("Got text: {msg}"),
/// _ => break, /// _ => break,
/// } /// }
/// } /// }
@ -56,16 +53,18 @@ pub use self::{
/// Ok(response) /// Ok(response)
/// } /// }
/// ///
/// #[tokio::main(flavor = "current_thread")] /// #[actix_rt::main]
/// async fn main() -> io::Result<()> { /// async fn main() -> Result<(), anyhow::Error> {
/// HttpServer::new(move || { /// HttpServer::new(move || {
/// App::new() /// App::new()
/// .route("/ws", web::get().to(ws))
/// .wrap(Logger::default()) /// .wrap(Logger::default())
/// .route("/ws", web::get().to(ws))
/// }) /// })
/// .bind(("127.0.0.1", 8080))? /// .bind("127.0.0.1:8080")?
/// .run() /// .run()
/// .await /// .await?;
///
/// Ok(())
/// } /// }
/// ``` /// ```
pub fn handle( pub fn handle(

View File

@ -1,19 +1,16 @@
use std::{ use std::sync::{
fmt, atomic::{AtomicBool, Ordering},
sync::{ Arc,
atomic::{AtomicBool, Ordering},
Arc,
},
}; };
use actix_http::ws::{CloseReason, Item, Message}; use actix_http::ws::{CloseReason, Message};
use actix_web::web::Bytes; use actix_web::web::Bytes;
use bytestring::ByteString; use bytestring::ByteString;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
/// A handle into the websocket session. /// A handle into the websocket session.
/// ///
/// This type can be used to send messages into the WebSocket. /// This type can be used to send messages into the websocket.
#[derive(Clone)] #[derive(Clone)]
pub struct Session { pub struct Session {
inner: Option<Sender<Message>>, inner: Option<Sender<Message>>,
@ -24,9 +21,9 @@ pub struct Session {
#[derive(Debug)] #[derive(Debug)]
pub struct Closed; pub struct Closed;
impl fmt::Display for Closed { impl std::fmt::Display for Closed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Session is closed") write!(f, "Session is closed")
} }
} }
@ -46,15 +43,12 @@ impl Session {
} }
} }
/// Sends text into the WebSocket. /// Send text into the websocket
/// ///
/// ```no_run /// ```rust,ignore
/// # use actix_ws::Session;
/// # async fn test(mut session: Session) {
/// if session.text("Some text").await.is_err() { /// if session.text("Some text").await.is_err() {
/// // session closed /// // session closed
/// } /// }
/// # }
/// ``` /// ```
pub async fn text(&mut self, msg: impl Into<ByteString>) -> Result<(), Closed> { pub async fn text(&mut self, msg: impl Into<ByteString>) -> Result<(), Closed> {
self.pre_check(); self.pre_check();
@ -68,15 +62,12 @@ impl Session {
} }
} }
/// Sends raw bytes into the WebSocket. /// Send raw bytes into the websocket
/// ///
/// ```no_run /// ```rust,ignore
/// # use actix_ws::Session; /// if session.binary(b"some bytes").await.is_err() {
/// # async fn test(mut session: Session) {
/// if session.binary(&b"some bytes"[..]).await.is_err() {
/// // session closed /// // session closed
/// } /// }
/// # }
/// ``` /// ```
pub async fn binary(&mut self, msg: impl Into<Bytes>) -> Result<(), Closed> { pub async fn binary(&mut self, msg: impl Into<Bytes>) -> Result<(), Closed> {
self.pre_check(); self.pre_check();
@ -90,18 +81,15 @@ impl Session {
} }
} }
/// Pings the client. /// Ping the client
/// ///
/// For many applications, it will be important to send regular pings to keep track of if the /// For many applications, it will be important to send regular pings to keep track of if the
/// client has disconnected /// client has disconnected
/// ///
/// ```no_run /// ```rust,ignore
/// # use actix_ws::Session;
/// # async fn test(mut session: Session) {
/// if session.ping(b"").await.is_err() { /// if session.ping(b"").await.is_err() {
/// // session is closed /// // session is closed
/// } /// }
/// # }
/// ``` /// ```
pub async fn ping(&mut self, msg: &[u8]) -> Result<(), Closed> { pub async fn ping(&mut self, msg: &[u8]) -> Result<(), Closed> {
self.pre_check(); self.pre_check();
@ -115,18 +103,15 @@ impl Session {
} }
} }
/// Pongs the client. /// Pong the client
/// ///
/// ```no_run /// ```rust,ignore
/// # use actix_ws::{Message, Session};
/// # async fn test(mut session: Session, msg: Message) {
/// match msg { /// match msg {
/// Message::Ping(bytes) => { /// Message::Ping(bytes) => {
/// let _ = session.pong(&bytes).await; /// let _ = session.pong(&bytes).await;
/// } /// }
/// _ => (), /// _ => (),
/// } /// }
/// # }
pub async fn pong(&mut self, msg: &[u8]) -> Result<(), Closed> { pub async fn pong(&mut self, msg: &[u8]) -> Result<(), Closed> {
self.pre_check(); self.pre_check();
if let Some(inner) = self.inner.as_mut() { if let Some(inner) = self.inner.as_mut() {
@ -139,51 +124,15 @@ impl Session {
} }
} }
/// Manually controls sending continuations. /// Send a close message, and consume the session
/// ///
/// Be wary of this method. Continuations represent multiple frames that, when combined, are /// All clones will return `Err(Closed)` if used after this call
/// presented as a single message. They are useful when the entire contents of a message are
/// not available all at once. However, continuations MUST NOT be interrupted by other Text or
/// Binary messages. Control messages such as Ping, Pong, or Close are allowed to interrupt a
/// continuation.
/// ///
/// Continuations must be initialized with a First variant, and must be terminated by a Last /// ```rust,ignore
/// variant, with only Continue variants sent in between.
///
/// ```no_run
/// # use actix_ws::{Item, Session};
/// # async fn test(mut session: Session) -> Result<(), Box<dyn std::error::Error>> {
/// session.continuation(Item::FirstText("Hello".into())).await?;
/// session.continuation(Item::Continue(b", World"[..].into())).await?;
/// session.continuation(Item::Last(b"!"[..].into())).await?;
/// # Ok(())
/// # }
/// ```
pub async fn continuation(&mut self, msg: Item) -> Result<(), Closed> {
self.pre_check();
if let Some(inner) = self.inner.as_mut() {
inner
.send(Message::Continuation(msg))
.await
.map_err(|_| Closed)
} else {
Err(Closed)
}
}
/// Sends a close message, and consumes the session.
///
/// All clones will return `Err(Closed)` if used after this call.
///
/// ```no_run
/// # use actix_ws::{Closed, Session};
/// # async fn test(mut session: Session) -> Result<(), Closed> {
/// session.close(None).await /// session.close(None).await
/// # }
/// ``` /// ```
pub async fn close(mut self, reason: Option<CloseReason>) -> Result<(), Closed> { pub async fn close(mut self, reason: Option<CloseReason>) -> Result<(), Closed> {
self.pre_check(); self.pre_check();
if let Some(inner) = self.inner.take() { if let Some(inner) = self.inner.take() {
self.closed.store(true, Ordering::Relaxed); self.closed.store(true, Ordering::Relaxed);
inner.send(Message::Close(reason)).await.map_err(|_| Closed) inner.send(Message::Close(reason)).await.map_err(|_| Closed)

111
justfile
View File

@ -1,135 +1,52 @@
# depends on: # depends on:
# - https://crates.io/crates/fd-find # - https://crates.io/crates/fd-find
# - https://crates.io/crates/cargo-check-external-types # - https://crates.io/crates/cargo-check-external-types
_list: _list:
@just --list @just --list
toolchain := "" # Format workspace.
msrv := ```
cargo metadata --format-version=1 \
| jq -r 'first(.packages[] | select(.source == null and .rust_version)) | .rust_version' \
| sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/'
```
msrv_rustup := "+" + msrv
# Run Clippy over workspace.
[group("lint")]
clippy:
cargo {{ toolchain }} clippy --workspace --all-targets --all-features
# Format project.
[group("lint")]
fmt: update-readmes fmt: update-readmes
cargo +nightly fmt cargo +nightly fmt
fd --type=file --hidden --extension=yml --extension=md --exec-batch npx -y prettier --write npx -y prettier --write $(fd --hidden --extension=yml --extension=md)
# 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. # Update READMEs from crate root documentation.
[group("lint")]
update-readmes: update-readmes:
cd ./actix-cors && cargo rdme --force cd ./actix-cors && cargo rdme --force
cd ./actix-identity && cargo rdme --force
cd ./actix-session && cargo rdme --force cd ./actix-session && cargo rdme --force
fd README.md --exec-batch npx -y prettier --write cd ./actix-identity && cargo rdme --force
npx -y prettier --write $(fd README.md)
# 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")]
test-all: test test-docs
# Test workspace and collect coverage info.
[private]
test-coverage:
cargo {{ toolchain }} llvm-cov nextest --no-report --all-features
cargo {{ toolchain }} llvm-cov --doc --no-report --all-features
# Test workspace and generate Codecov report.
test-coverage-codecov: test-coverage
cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json
# Test workspace and generate LCOV report.
test-coverage-lcov: test-coverage
cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info
# Test workspace docs.
[group("test")]
[group("docs")]
test-docs:
cargo {{ toolchain }} test --doc --workspace --all-features --no-fail-fast -- --nocapture
# Document crates in workspace. # Document crates in workspace.
[group("docs")] doc:
doc *args: && doc-set-workspace-crates RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features
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 }}
[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) | .targets | map(select(.doc) | .name)] | flatten'
echo ";"
) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js"
# Document crates in workspace and watch for changes. # Document crates in workspace and watch for changes.
[group("docs")]
doc-watch: doc-watch:
@just doc --open RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features --open
cargo watch -- just doc cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features
# Check for unintentional external type exposure on all crates in workspace. # Check for unintentional external type exposure on all crates in workspace.
[group("lint")] check-external-types-all toolchain="+nightly":
check-external-types-all:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
exit=0 exit=0
for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do
if ! just toolchain={{ toolchain }} check-external-types-manifest "$f"; then exit=1; fi if ! just check-external-types-manifest "$f" {{toolchain}}; then exit=1; fi
echo echo
echo echo
done done
exit $exit exit $exit
# Check for unintentional external type exposure on all crates in workspace. # Check for unintentional external type exposure on all crates in workspace.
[group("lint")] check-external-types-all-table toolchain="+nightly":
check-external-types-all-table:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do
echo echo
echo "Checking for $f" echo "Checking for $f"
just toolchain={{ toolchain }} check-external-types-manifest "$f" --output-format=markdown-table just check-external-types-manifest "$f" {{toolchain}} --output-format=markdown-table
done done
# Check for unintentional external type exposure on a crate. # Check for unintentional external type exposure on a crate.
[group("lint")] check-external-types-manifest manifest_path toolchain="+nightly" *extra_args="":
check-external-types-manifest manifest_path *extra_args="": cargo {{toolchain}} check-external-types --manifest-path "{{manifest_path}}" {{extra_args}}
cargo {{ toolchain }} check-external-types --manifest-path "{{ manifest_path }}" {{ extra_args }}

46598
lcov.info

File diff suppressed because it is too large Load Diff