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 "limitation-v0.4.0" have entirely different histories.

125 changed files with 1737 additions and 53695 deletions

View File

@ -3,41 +3,35 @@ name: bug report
about: create a bug report about: create a bug report
--- ---
Your issue may already be reported! Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one. Your issue may already be reported!
Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one.
## Expected Behavior ## Expected Behavior
<!--- If you're describing a bug, tell us what should happen --> <!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work --> <!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior ## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior --> <!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior --> <!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution ## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, --> <!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change --> <!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs) ## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to --> <!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant --> <!--- reproduce this bug. Include code to reproduce, if relevant -->
1. 1.
2. 2.
3. 3.
4. 4.
## Context ## Context
<!--- How has this issue affected you? What are you trying to accomplish? --> <!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world --> <!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment ## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in --> <!--- Include as many relevant details about the environment you experienced the bug in -->
- Rust version (output of `rustc -V`): * Rust Version (I.e, output of `rustc -V`):
- `actix-*` crate versions: * Actix-* crate(s) Version:

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Gitter channel (actix)
url: https://gitter.im/actix/actix
about: Please ask and answer questions about the actix project here.

View File

@ -2,14 +2,12 @@
<!-- Please fill out the following to make our reviews easy. --> <!-- Please fill out the following to make our reviews easy. -->
## PR Type ## PR Type
<!-- What kind of change does this PR make? --> <!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other --> <!-- Bug Fix / Feature / Refactor / Code Style / Other -->
INSERT_PR_TYPE INSERT_PR_TYPE
## PR Checklist
## PR Checklist
<!-- Check your PR fulfills the following items. --> <!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. --> <!-- For draft PRs check the boxes as you complete them. -->
@ -18,10 +16,11 @@ INSERT_PR_TYPE
- [ ] A changelog entry has been made for the appropriate packages. - [ ] A changelog entry has been made for the appropriate packages.
- [ ] Format code with the nightly rustfmt (`cargo +nightly fmt`). - [ ] Format code with the nightly rustfmt (`cargo +nightly fmt`).
## Overview
## Overview
<!-- Describe the current and new behavior. --> <!-- Describe the current and new behavior. -->
<!-- Emphasize any breaking changes. --> <!-- Emphasize any breaking changes. -->
<!-- If this PR fixes or closes an issue, reference it here. --> <!-- If this PR fixes or closes an issue, reference it here. -->
<!-- Closes #000 --> <!-- Closes #000 -->

View File

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

View File

@ -1,13 +1,8 @@
name: CI (post-merge) name: CI (post-merge)
on: on:
push: { branches: [master] } push:
branches: [master]
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build_and_test_linux_nightly: build_and_test_linux_nightly:
@ -16,8 +11,10 @@ jobs:
matrix: matrix:
target: target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
version:
- nightly
name: ${{ matrix.target.name }} / nightly name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
services: services:
@ -28,80 +25,104 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install Rust (nightly) - name: Install ${{ matrix.version }}
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install cargo-hack, cargo-ci-cache-clean - name: Install cargo-hack
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: with:
tool: cargo-hack,cargo-ci-cache-clean command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: check minimal - name: check minimal
run: cargo ci-min uses: actions-rs/cargo@v1
with: { command: ci-min }
- name: check minimal + examples - name: check minimal + examples
run: cargo ci-check-min-examples uses: actions-rs/cargo@v1
with: { command: ci-check-min-examples }
- name: check default - name: check default
run: cargo ci-check uses: actions-rs/cargo@v1
with: { command: ci-check }
- name: tests - name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test with: { command: ci-test }
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache
build_and_test_other_nightly: build_and_test_other_nightly:
strategy: strategy:
fail-fast: false fail-fast: false
# prettier-ignore
matrix: matrix:
target: target:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- nightly
name: ${{ matrix.target.name }} / nightly name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install OpenSSL - name: Install ${{ matrix.version }}
if: matrix.target.os == 'windows-latest' uses: actions-rs/toolchain@v1
shell: bash
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: nightly toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install cargo-hack and cargo-ci-cache-clean - name: Install cargo-hack
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: with:
tool: cargo-hack,cargo-ci-cache-clean command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: check minimal - name: check minimal
run: cargo ci-min uses: actions-rs/cargo@v1
with: { command: ci-min }
- name: check minimal + examples - name: check minimal + examples
run: cargo ci-check-min-examples uses: actions-rs/cargo@v1
with: { command: ci-check-min-examples }
- name: check default - name: check default
run: cargo ci-check uses: actions-rs/cargo@v1
with: { command: ci-check }
- name: tests - name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation -- --nocapture with:
command: ci-test
args: >-
--exclude=actix-redis
--exclude=actix-session
--exclude=actix-limitation
-- --nocapture
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache

View File

@ -3,17 +3,9 @@ name: CI
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
merge_group:
types: [checks_requested]
push: push:
branches: [master] branches: [master]
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build_and_test_linux: build_and_test_linux:
strategy: strategy:
@ -22,10 +14,10 @@ jobs:
target: target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
version: version:
- { name: msrv, version: 1.75.0 } - 1.59 # MSRV
- { name: stable, version: stable } - stable
name: ${{ matrix.target.name }} / ${{ matrix.version.name }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
services: services:
@ -41,113 +33,130 @@ jobs:
--entrypoint redis-server --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install Rust (${{ matrix.version.name }}) - name: Install ${{ matrix.version }}
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install cargo-hack and cargo-ci-cache-clean, just - name: Install cargo-hack
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: with:
tool: cargo-hack,cargo-ci-cache-clean,just command: generate-lockfile
- name: Cache Dependencies
- name: workaround MSRV issues uses: Swatinem/rust-cache@v1.2.0
if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv
- name: check minimal - name: check minimal
run: cargo ci-min uses: actions-rs/cargo@v1
with: { command: ci-min }
- name: check minimal + examples - name: check minimal + examples
run: cargo ci-check-min-examples uses: actions-rs/cargo@v1
with: { command: ci-check-min-examples }
- name: check default - name: check default
run: cargo ci-check uses: actions-rs/cargo@v1
with: { command: ci-check }
- name: tests - name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test with: { command: ci-test }
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache
build_and_test_other: build_and_test_other:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# prettier-ignore
target: target:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version: version:
- { name: msrv, version: 1.75.0 } - 1.59 # MSRV
- { name: stable, version: stable } - stable
name: ${{ matrix.target.name }} / ${{ matrix.version.name }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install OpenSSL - name: Install ${{ matrix.version }}
if: matrix.target.os == 'windows-latest' uses: actions-rs/toolchain@v1
shell: bash
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install cargo-hack, cargo-ci-cache-clean, just - name: Install cargo-hack
uses: taiki-e/install-action@v2.49.42 uses: taiki-e/install-action@cargo-hack
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: with:
tool: cargo-hack,cargo-ci-cache-clean,just command: generate-lockfile
- name: Cache Dependencies
- name: workaround MSRV issues uses: Swatinem/rust-cache@v1.2.0
if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv
- name: check minimal - name: check minimal
run: cargo ci-min uses: actions-rs/cargo@v1
with: { command: ci-min }
- name: check minimal + examples - name: check minimal + examples
run: cargo ci-check-min-examples uses: actions-rs/cargo@v1
with: { command: ci-check-min-examples }
- name: check default - name: check default
run: cargo ci-check uses: actions-rs/cargo@v1
with: { command: ci-check }
- name: tests - name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation with:
command: ci-test
args: >-
--exclude=actix-redis
--exclude=actix-session
--exclude=actix-limitation
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache
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@v3
- name: Install Rust (nightly) - name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Install just - name: Generate Cargo.lock
uses: taiki-e/install-action@v2.49.42 uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: doc tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with: with:
tool: just command: ci-doctest
args: -- --nocapture
- name: Test docs
run: just test-docs
- name: Build docs
run: just doc

View File

@ -1,15 +1,11 @@
# disabled because `cargo tarpaulin` currently segfaults
name: Coverage name: Coverage
on: on:
push: push:
branches: [master] branches: [master]
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -22,26 +18,25 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install Rust - name: Install stable
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: stable-x86_64-unknown-linux-gnu
components: llvm-tools-preview profile: minimal
override: true
- name: Install just, cargo-llvm-cov, cargo-nextest - name: Generate Cargo.lock
uses: taiki-e/install-action@v2.49.42 uses: actions-rs/cargo@v1
with: with: { command: generate-lockfile }
tool: just,cargo-llvm-cov,cargo-nextest - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Generate code coverage
run: just test-coverage-codecov
- name: Generate coverage file
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --workspace --out Xml --verbose
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v5.4.0 uses: codecov/codecov-action@v1
with: with: { file: cobertura.xml }
files: codecov.json
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,48 +1,39 @@
name: Lint name: Lint
on: [pull_request] on:
pull_request:
permissions: types: [opened, synchronize, reopened]
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install Rust (nightly) - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: nightly
components: rustfmt components: rustfmt
- name: Check with rustfmt - name: Check with rustfmt
run: cargo fmt --all -- --check uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy: clippy:
permissions:
contents: read
checks: write # to add clippy checks to PR diffs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install Rust - name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable
components: clippy components: clippy
override: true
- name: Check with Clippy - name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1 uses: actions-rs/clippy-check@v1
with: with:
reporter: github-pr-check token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }} args: --workspace --tests --all-features
clippy_flags: >-
--workspace --all-features --tests --examples --bins --
-A unknown_lints -D clippy::todo -D clippy::dbg_macro

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

@ -0,0 +1,35 @@
name: Upload Documentation
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Build Docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --workspace --all-features --no-deps
- 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@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: target/doc

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

View File

@ -1,5 +0,0 @@
overrides:
- files: "*.md"
options:
proseWrap: never
printWidth: 9999

3292
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,29 +5,18 @@ 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",
"actix-ws",
] ]
[workspace.package]
repository = "https://github.com/actix/actix-extras"
homepage = "https://actix.rs"
license = "MIT OR Apache-2.0"
edition = "2021"
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

@ -186,7 +186,8 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2017-NOW Actix team Copyright 2017-NOW Nikolay Kim
Copyright 2017-NOW svartalf and Actix team
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -1,4 +1,5 @@
Copyright (c) 2023 Actix team Copyright (c) 2017 Nikolay Kim
Copyright (c) 2017 svartalf and Actix team
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@ -2,27 +2,23 @@
> A collection of additional crates supporting [Actix Web]. > A collection of additional crates supporting [Actix Web].
<!-- prettier-ignore-start -->
[![CI](https://github.com/actix/actix-extras/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-extras/actions/workflows/ci.yml) [![CI](https://github.com/actix/actix-extras/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-extras/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-extras/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-extras) [![codecov](https://codecov.io/gh/actix/actix-extras/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-extras)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/5Ux4QGChWc) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/5Ux4QGChWc)
[![Dependency Status](https://deps.rs/repo/github/actix/actix-extras/status.svg)](https://deps.rs/repo/github/actix/actix-extras) [![Dependency Status](https://deps.rs/repo/github/actix/actix-extras/status.svg)](https://deps.rs/repo/github/actix/actix-extras)
<!-- prettier-ignore-end -->
## Crates by @actix ## Crates by @actix
| Crate | | | | Crate | | |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [actix-cors] | [![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors) [![dependency status](https://deps.rs/crate/actix-cors/latest/status.svg)](https://deps.rs/crate/actix-cors) | Cross-Origin Resource Sharing (CORS) controls. | | [actix-cors] | [![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors) [![dependency status](https://deps.rs/crate/actix-cors/0.6.1/status.svg)](https://deps.rs/crate/actix-cors/0.6.1) | Cross-Origin Resource Sharing (CORS) controls. |
| [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/0.4.0/status.svg)](https://deps.rs/crate/actix-identity/0.4.0) | 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/0.3.0/status.svg)](https://deps.rs/crate/actix-limitation/0.3.0) | 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/0.8.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.8.0) | Protobuf payload extractor. |
| [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-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/0.12.0/status.svg)](https://deps.rs/crate/actix-redis/0.12.0) | Actor-based Redis client. |
| [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-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/0.7.1/status.svg)](https://deps.rs/crate/actix-session/0.7.1) | Session management. |
| [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-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/0.6.0/status.svg)](https://deps.rs/crate/actix-settings/0.6.0) | Easily manage Actix Web's settings from a TOML file and environment variables. |
| [actix-ws] | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)][actix-ws] [![dependency status](https://deps.rs/crate/actix-ws/latest/status.svg)](https://deps.rs/crate/actix-ws) | WebSockets for Actix Web, without actors. | | [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/0.8.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.8.0) | HTTP authentication schemes. |
--- ---
@ -31,26 +27,21 @@
These crates are provided by the community. 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/0.16.4/status.svg)](https://deps.rs/crate/actix-web-lab/0.16.4) | 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/0.1.4/status.svg)](https://deps.rs/crate/actix-multipart-extract/0.1.4) | Better multipart form support for Actix Web. |
| [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-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/0.6.2/status.svg)](https://deps.rs/crate/actix-form-data/0.6.2) | Rate-limiting backed by form-data. |
| [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-governor] | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)][actix-governor] [![dependency status](https://deps.rs/crate/actix-governor/0.3.0/status.svg)](https://deps.rs/crate/actix-governor/0.3.0) | Rate-limiting backed by governor. |
| [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-casbin] | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)][actix-casbin] [![dependency status](https://deps.rs/crate/actix-casbin/0.4.2/status.svg)](https://deps.rs/crate/actix-casbin/0.4.2) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
| [actix-web-static-files] | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)][actix-web-static-files] [![dependency status](https://deps.rs/crate/actix-web-static-files/latest/status.svg)](https://deps.rs/crate/actix-web-static-files) | Static files as embedded resources. | | [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/0.3.1/status.svg)](https://deps.rs/crate/actix-ip-filter/0.3.1) | IP address filter. Supports glob patterns. |
| [actix-web-grants] | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)][actix-web-grants] [![dependency status](https://deps.rs/crate/actix-web-grants/latest/status.svg)](https://deps.rs/crate/actix-web-grants) | Extension for validating user authorities. | | [actix-web-static-files] | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)][actix-web-static-files] [![dependency status](https://deps.rs/crate/actix-web-static-files/4.0.0/status.svg)](https://deps.rs/crate/actix-web-static-files/4.0.0) | Static files as embedded resources. |
| [aliri_actix] | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)][aliri_actix] [![dependency status](https://deps.rs/crate/aliri_actix/latest/status.svg)](https://deps.rs/crate/aliri_actix) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. | | [actix-web-grants] | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)][actix-web-grants] [![dependency status](https://deps.rs/crate/actix-web-grants/3.0.1/status.svg)](https://deps.rs/crate/actix-web-grants/3.0.1) | Extension for validating user authorities. |
| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)][actix-web-flash-messages] [![dependency status](https://deps.rs/crate/actix-web-flash-messages/latest/status.svg)](https://deps.rs/crate/actix-web-flash-messages) | Support for flash messages/one-time notifications in `actix-web`. | | [aliri_actix] | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)][aliri_actix] [![dependency status](https://deps.rs/crate/aliri_actix/0.7.0/status.svg)](https://deps.rs/crate/aliri_actix/0.7.0) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
| [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. | | [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)][actix-web-flash-messages] [![dependency status](https://deps.rs/crate/actix-web-flash-messages/0.4.1/status.svg)](https://deps.rs/crate/actix-web-flash-messages/0.4.1) | Support for flash messages/one-time notifications in `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. | | [awmp] | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)][awmp] [![dependency status](https://deps.rs/crate/awmp/0.8.1/status.svg)](https://deps.rs/crate/awmp/0.8.1) | An easy to use wrapper around multipart fields 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. | | [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/0.6.0/status.svg)](https://deps.rs/crate/tracing-actix-web/0.6.0) | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
| [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-ws] | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)][actix-ws] [![dependency status](https://deps.rs/crate/actix-ws/0.2.5/status.svg)](https://deps.rs/crate/actix-ws/0.2.5) | Actor-less WebSockets for the Actix Runtime. |
| [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-hash] | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)][actix-hash] [![dependency status](https://deps.rs/crate/actix-hash/0.4.0/status.svg)](https://deps.rs/crate/actix-hash/0.4.0) | Hashing utilities 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`. |
| [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 +54,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
@ -80,11 +72,3 @@ To add a crate to this list, submit a pull request.
[tracing-actix-web]: https://crates.io/crates/tracing-actix-web [tracing-actix-web]: https://crates.io/crates/tracing-actix-web
[actix-ws]: https://crates.io/crates/actix-ws [actix-ws]: https://crates.io/crates/actix-ws
[actix-hash]: https://crates.io/crates/actix-hash [actix-hash]: https://crates.io/crates/actix-hash
[actix-bincode]: https://crates.io/crates/actix-bincode
[sentinel-actix]: https://crates.io/crates/sentinel-actix
[actix-telepathy]: https://crates.io/crates/actix-telepathy
[actix-web-validation]: https://crates.io/crates/actix-web-validation
[actix-telepathy]: https://crates.io/crates/actix-telepathy
[apistos]: https://crates.io/crates/apistos
[actix-jwt-cookies]: https://crates.io/crates/actix-jwt-cookies
[actix-ws-broadcaster]: https://crates.io/crates/actix-ws-broadcaster

View File

@ -1,88 +1,65 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
## 0.7.1
- Implement `PartialEq` for `Cors` allowing for better testing.
## 0.7.0
- `Cors` is now marked `#[must_use]`.
- Default for `Cors::block_on_origin_mismatch` is now false.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.5
- Fix `Vary` header when Private Network Access is enabled.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.6.4
- Add `Cors::allow_private_network_access()` behind an unstable flag (`draft-private-network-access`).
## 0.6.3
- Add `Cors::block_on_origin_mismatch()` option for controlling if requests are pre-emptively rejected.
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.6.2
- Fix `expose_any_header` to return list of response headers. ## 0.6.2 - 2022-08-07
- Fix `expose_any_header` to return list of response headers. [#273]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.6.1 [#273]: https://github.com/actix/actix-extras/pull/273
- Do not consider requests without a `Access-Control-Request-Method` as preflight.
## 0.6.0 ## 0.6.1 - 2022-03-07
- Do not consider requests without a `Access-Control-Request-Method` as preflight. [#226]
[#226]: https://github.com/actix/actix-extras/pull/226
## 0.6.0 - 2022-02-25
- Update `actix-web` dependency to 4.0. - Update `actix-web` dependency to 4.0.
<details> ## 0.6.0-beta.10 - 2022-02-07
<summary>0.6.0 pre-releases</summary>
## 0.6.0-beta.10
- Ensure that preflight responses contain a `Vary` header. [#224] - Ensure that preflight responses contain a `Vary` header. [#224]
[#224]: https://github.com/actix/actix-extras/pull/224 [#224]: https://github.com/actix/actix-extras/pull/224
## 0.6.0-beta.9
## 0.6.0-beta.9 - 2022-02-07
- Relax body type bounds on middleware impl. [#223] - Relax body type bounds on middleware impl. [#223]
- Update `actix-web` dependency to `4.0.0-rc.1`. - Update `actix-web` dependency to `4.0.0-rc.1`.
[#223]: https://github.com/actix/actix-extras/pull/223 [#223]: https://github.com/actix/actix-extras/pull/223
## 0.6.0-beta.8
## 0.6.0-beta.8 - 2021-12-29
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
## 0.6.0-beta.7
## 0.6.0-beta.7 - 2021-12-18
- Update `actix-web` dependency to `4.0.0-beta.15`. [#216] - Update `actix-web` dependency to `4.0.0-beta.15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216 [#216]: https://github.com/actix/actix-extras/pull/216
## 0.6.0-beta.6
## 0.6.0-beta.6 - 2021-12-13
- Fix panic when wrapping routes with dynamic segments in their paths. [#213] - Fix panic when wrapping routes with dynamic segments in their paths. [#213]
[#213]: https://github.com/actix/actix-extras/pull/213 [#213]: https://github.com/actix/actix-extras/pull/213
## 0.6.0-beta.5 _(YANKED)_
## 0.6.0-beta.5 - 2021-12-12 _(YANKED)_
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209] - Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#209]: https://github.com/actix/actix-extras/pull/209 [#209]: https://github.com/actix/actix-extras/pull/209
## 0.6.0-beta.4
## 0.6.0-beta.4 - 2021-11-22
- No significant changes since `0.6.0-beta.3`. - No significant changes since `0.6.0-beta.3`.
## 0.6.0-beta.3
## 0.6.0-beta.3 - 2021-10-21
- Make `Cors` middleware generic over body type [#195] - Make `Cors` middleware generic over body type [#195]
- Fix `expose_any_header` behavior. [#204] - Fix `expose_any_header` behavior. [#204]
- Update `actix-web` dependency to v4.0.0-beta.10. [#203] - Update `actix-web` dependency to v4.0.0-beta.10. [#203]
@ -92,90 +69,90 @@
[#203]: https://github.com/actix/actix-extras/pull/203 [#203]: https://github.com/actix/actix-extras/pull/203
[#204]: https://github.com/actix/actix-extras/pull/204 [#204]: https://github.com/actix/actix-extras/pull/204
## 0.6.0-beta.2
## 0.6.0-beta.2 - 2021-06-27
- No notable changes. - No notable changes.
## 0.6.0-beta.1
## 0.6.0-beta.1 - 2021-04-02
- Update `actix-web` dependency to 4.0.0 beta. - Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0. - Minimum supported Rust version (MSRV) is now 1.46.0.
</details>
## 0.5.4
## 0.5.4 - 2020-12-31
- Fix `expose_any_header` method, now set the correct field. [#143] - Fix `expose_any_header` method, now set the correct field. [#143]
[#143]: https://github.com/actix/actix-extras/pull/143 [#143]: https://github.com/actix/actix-extras/pull/143
## 0.5.3
## 0.5.3 - 2020-11-19
- Fix version spec for `derive_more` dependency. - Fix version spec for `derive_more` dependency.
## 0.5.2
## 0.5.2 - 2020-11-15
- Ensure `tinyvec` is using the correct features. - Ensure `tinyvec` is using the correct features.
- Bump `futures-util` minimum version to `0.3.7` to avoid `RUSTSEC-2020-0059`. - Bump `futures-util` minimum version to `0.3.7` to avoid `RUSTSEC-2020-0059`.
## 0.5.1
## 0.5.1 - 2020-11-05
- Fix `allow_any_header` method, now set the correct field. [#121] - Fix `allow_any_header` method, now set the correct field. [#121]
[#121]: https://github.com/actix/actix-extras/pull/121 [#121]: https://github.com/actix/actix-extras/pull/121
## 0.5.0
## 0.5.0 - 2020-10-19
- Disallow `*` in `Cors::allowed_origin`. [#114]. - Disallow `*` in `Cors::allowed_origin`. [#114].
- Hide `CorsMiddleware` from docs. [#118]. - Hide `CorsMiddleware` from docs. [#118].
- `CorsFactory` is removed. [#119] - `CorsFactory` is removed. [#119]
- The `impl Default` constructor is now overly-restrictive. [#119] - The `impl Default` constructor is now overly-restrictive. [#119]
- Added `Cors::permissive()` constructor that allows anything. [#119] - Added `Cors::permissive()` constructor that allows anything. [#119]
- Adds methods for each property to reset to a permissive state. (`allow_any_origin`, `expose_any_header`, etc.) [#119] - Adds methods for each property to reset to a permissive state. (`allow_any_origin`,
`expose_any_header`, etc.) [#119]
- Errors are now propagated with `Transform::InitError` instead of panicking. [#119] - Errors are now propagated with `Transform::InitError` instead of panicking. [#119]
- Fixes bug where allowed origin functions are not called if `allowed_origins` is All. [#119] - Fixes bug where allowed origin functions are not called if `allowed_origins` is All. [#119]
- `AllOrSome` is no longer public. [#119] - `AllOrSome` is no longer public. [#119]
- Functions used for `allowed_origin_fn` now receive the Origin HeaderValue as the first parameter. [#120] - Functions used for `allowed_origin_fn` now receive the Origin HeaderValue as the
first parameter. [#120]
[#114]: https://github.com/actix/actix-extras/pull/114 [#114]: https://github.com/actix/actix-extras/pull/114
[#118]: https://github.com/actix/actix-extras/pull/118 [#118]: https://github.com/actix/actix-extras/pull/118
[#119]: https://github.com/actix/actix-extras/pull/119 [#119]: https://github.com/actix/actix-extras/pull/119
[#120]: https://github.com/actix/actix-extras/pull/120 [#120]: https://github.com/actix/actix-extras/pull/120
## 0.4.1
## 0.4.1 - 2020-10-07
- Allow closures to be used with `allowed_origin_fn`. [#110] - Allow closures to be used with `allowed_origin_fn`. [#110]
[#110]: https://github.com/actix/actix-extras/pull/110 [#110]: https://github.com/actix/actix-extras/pull/110
## 0.4.0
## 0.4.0 - 2020-09-27
- Implement `allowed_origin_fn` builder method. [#93] - Implement `allowed_origin_fn` builder method. [#93]
- Use `TryInto` instead of `TryFrom` where applicable. [#106] - Use `TryInto` instead of `TryFrom` where applicable. [#106]
[#93]: https://github.com/actix/actix-extras/pull/93 [#93]: https://github.com/actix/actix-extras/pull/93
[#106]: https://github.com/actix/actix-extras/pull/106 [#106]: https://github.com/actix/actix-extras/pull/106
## 0.3.0
## 0.3.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0. - Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0. - Minimum supported Rust version (MSRV) is now 1.42.0.
- Implement the Debug trait on all public types. - Implement the Debug trait on all public types.
## 0.3.0-alpha.1
## 0.3.0-alpha.1 - 2020-03-11
- Minimize `futures-*` dependencies - Minimize `futures-*` dependencies
- Update `actix-web` dependency to 3.0.0-alpha.1 - Update `actix-web` dependency to 3.0.0-alpha.1
## 0.2.0 - 2019-12-20
## 0.2.0 - 2019-12-20
- Release - Release
## 0.2.0-alpha.3 - 2019-12-07
## 0.2.0-alpha.3 - 2019-12-07
- Migrate to actix-web 2.0.0 - Migrate to actix-web 2.0.0
- Bump `derive_more` crate version to 0.99.0 - Bump `derive_more` crate version to 0.99.0
## 0.1.0 - 2019-06-15
## 0.1.0 - 2019-06-15
- Move cors middleware to separate crate - Move cors middleware to separate crate

View File

@ -1,39 +1,32 @@
[package] [package]
name = "actix-cors" name = "actix-cors"
version = "0.7.1" version = "0.6.2"
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.git"
license.workspace = true license = "MIT OR Apache-2.0"
edition.workspace = true edition = "2018"
rust-version.workspace = true
[package.metadata.docs.rs] [lib]
rustdoc-args = ["--cfg", "docsrs"] name = "actix_cors"
all-features = true path = "src/lib.rs"
[features]
draft-private-network-access = []
[dependencies] [dependencies]
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.5"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false }
log = "0.4" log = "0.4"
once_cell = "1" once_cell = "1"
smallvec = "1" smallvec = "1.6.1"
[dev-dependencies] [dev-dependencies]
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.9"
regex = "1.4" regex = "1.4"
[lints]
workspace = true

View File

@ -1,72 +1,14 @@
# actix-cors # actix-cors
<!-- prettier-ignore-start --> > Cross-Origin Resource Sharing (CORS) controls for Actix Web.
[![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.6.2)](https://docs.rs/actix-cors/0.6.2)
![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-cors)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-cors.svg) [![Dependency Status](https://deps.rs/crate/actix-cors/0.6.2/status.svg)](https://deps.rs/crate/actix-cors/0.6.2)
<br />
[![Dependency Status](https://deps.rs/crate/actix-cors/0.7.1/status.svg)](https://deps.rs/crate/actix-cors/0.7.1)
[![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)
<!-- prettier-ignore-end -->
<!-- cargo-rdme start -->
Cross-Origin Resource Sharing (CORS) controls for Actix Web.
This middleware can be applied to both applications and resources. Once built, a [`Cors`] builder can be used as an argument for Actix Web's `App::wrap()`, `Scope::wrap()`, or `Resource::wrap()` methods.
This CORS middleware automatically handles `OPTIONS` preflight requests.
## Crate Features
- `draft-private-network-access`: ⚠️ Unstable. Adds opt-in support for the [Private Network Access] spec extensions. This feature is unstable since it will follow breaking changes in the draft spec until it is finalized.
## Example
```rust
use actix_cors::Cors;
use actix_web::{get, http, web, App, HttpRequest, HttpResponse, HttpServer};
#[get("/index.html")]
async fn index(req: HttpRequest) -> &'static str {
"<p>Hello World!</p>"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let cors = Cors::default()
.allowed_origin("https://www.rust-lang.org")
.allowed_origin_fn(|origin, _req_head| {
origin.as_bytes().ends_with(b".rust-lang.org")
})
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.wrap(cors)
.service(index)
})
.bind(("127.0.0.1", 8080))?
.run()
.await;
Ok(())
}
```
[Private Network Access]: https://wicg.github.io/private-network-access
<!-- cargo-rdme end -->
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-cors) - [API Documentation](https://docs.rs/actix-cors)
- [Example Project](https://github.com/actix/examples/tree/master/cors) - [Example Project](https://github.com/actix/examples/tree/master/cors)
- Minimum Supported Rust Version (MSRV): 1.75 - Minimum Supported Rust Version (MSRV): 1.57

View File

@ -39,8 +39,6 @@ async fn main() -> std::io::Result<()> {
.allowed_header(header::CONTENT_TYPE) .allowed_header(header::CONTENT_TYPE)
// set list of headers that are safe to expose // set list of headers that are safe to expose
.expose_headers(&[header::CONTENT_DISPOSITION]) .expose_headers(&[header::CONTENT_DISPOSITION])
// allow cURL/HTTPie from working without providing Origin headers
.block_on_origin_mismatch(false)
// set preflight cache TTL // set preflight cache TTL
.max_age(3600), .max_age(3600),
) )

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, rc::Rc}; use std::{collections::HashSet, convert::TryInto, iter::FromIterator, rc::Rc};
use actix_utils::future::{self, Ready}; use actix_utils::future::{self, Ready};
use actix_web::{ use actix_web::{
@ -52,20 +52,13 @@ static ALL_METHODS_SET: Lazy<HashSet<Method>> = Lazy::new(|| {
/// The alternative [`Cors::permissive()`] constructor is available for local development, allowing /// The alternative [`Cors::permissive()`] constructor is available for local development, allowing
/// all origins and headers, etc. **The permissive constructor should not be used in production.** /// all origins and headers, etc. **The permissive constructor should not be used in production.**
/// ///
/// # Behavior
///
/// In all cases, behavior for this crate follows the [Fetch Standard CORS protocol]. See that
/// document for information on exact semantics for configuration options and combinations.
///
/// # Errors /// # Errors
///
/// Errors surface in the middleware initialization phase. This means that, if you have logs enabled /// Errors surface in the middleware initialization phase. This means that, if you have logs enabled
/// in Actix Web (using `env_logger` or other crate that exposes logs from the `log` crate), error /// in Actix Web (using `env_logger` or other crate that exposes logs from the `log` crate), error
/// messages will outline what is wrong with the CORS configuration in the server logs and the /// messages will outline what is wrong with the CORS configuration in the server logs and the
/// server will fail to start up or serve requests. /// server will fail to start up or serve requests.
/// ///
/// # Example /// # Example
///
/// ``` /// ```
/// use actix_cors::Cors; /// use actix_cors::Cors;
/// use actix_web::http::header; /// use actix_web::http::header;
@ -79,18 +72,14 @@ static ALL_METHODS_SET: Lazy<HashSet<Method>> = Lazy::new(|| {
/// ///
/// // `cors` can now be used in `App::wrap`. /// // `cors` can now be used in `App::wrap`.
/// ``` /// ```
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
#[derive(Debug)] #[derive(Debug)]
#[must_use]
pub struct Cors { pub struct Cors {
inner: Rc<Inner>, inner: Rc<Inner>,
error: Option<Either<HttpError, CorsError>>, error: Option<Either<HttpError, CorsError>>,
} }
impl Cors { impl Cors {
/// Constructs a very permissive set of defaults for quick development. (Not recommended for /// A very permissive set of default for quick development. Not recommended for production use.
/// production use.)
/// ///
/// *All* origins, methods, request headers and exposed headers allowed. Credentials supported. /// *All* origins, methods, request headers and exposed headers allowed. Credentials supported.
/// Max age 1 hour. Does not send wildcard. /// Max age 1 hour. Does not send wildcard.
@ -112,10 +101,7 @@ impl Cors {
preflight: true, preflight: true,
send_wildcard: false, send_wildcard: false,
supports_credentials: true, supports_credentials: true,
#[cfg(feature = "draft-private-network-access")]
allow_private_network_access: false,
vary_header: true, vary_header: true,
block_on_origin_mismatch: false,
}; };
Cors { Cors {
@ -135,12 +121,12 @@ impl Cors {
self self
} }
/// Adds an origin that is allowed to make requests. /// Add an origin that is allowed to make requests.
/// ///
/// This method allows specifying a finite set of origins to verify the value of the `Origin` /// By default, requests from all origins are accepted by CORS logic. This method allows to
/// request header. These are `origin-or-null` types in the [Fetch Standard]. /// specify a finite set of origins to verify the value of the `Origin` request header.
/// ///
/// By default, no origins are accepted. /// These are `origin-or-null` types in the [Fetch Standard].
/// ///
/// When this list is set, the client's `Origin` request header will be checked in a /// When this list is set, the client's `Origin` request header will be checked in a
/// case-sensitive manner. /// case-sensitive manner.
@ -188,7 +174,7 @@ impl Cors {
self self
} }
/// Determinates allowed origins by processing requests which didn't match any origins specified /// Determinate allowed origins by processing requests which didn't match any origins specified
/// in the `allowed_origin`. /// in the `allowed_origin`.
/// ///
/// The function will receive two parameters, the Origin header value, and the `RequestHead` of /// The function will receive two parameters, the Origin header value, and the `RequestHead` of
@ -214,17 +200,20 @@ 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
} }
/// Sets a list of methods which allowed origins can perform. /// Set a list of methods which allowed origins can perform.
/// ///
/// These will be sent in the `Access-Control-Allow-Methods` response header. /// These will be sent in the `Access-Control-Allow-Methods` response header as specified in
/// the [Fetch Standard CORS protocol].
/// ///
/// This defaults to an empty set. /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]`
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors
where where
U: IntoIterator<Item = M>, U: IntoIterator<Item = M>,
@ -287,13 +276,16 @@ impl Cors {
self self
} }
/// Sets a list of request header field names which can be used when this resource is accessed /// Set a list of request header field names which can be used when this resource is accessed by
/// by allowed origins. /// allowed origins.
/// ///
/// If `All` is set, whatever is requested by the client in `Access-Control-Request-Headers` /// If `All` is set, whatever is requested by the client in `Access-Control-Request-Headers`
/// will be echoed back in the `Access-Control-Allow-Headers` header. /// will be echoed back in the `Access-Control-Allow-Headers` header as specified in
/// the [Fetch Standard CORS protocol].
/// ///
/// This defaults to an empty set. /// Defaults to `All`.
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors
where where
U: IntoIterator<Item = H>, U: IntoIterator<Item = H>,
@ -334,11 +326,13 @@ impl Cors {
self self
} }
/// Sets a list of headers which are safe to expose to the API of a CORS API specification. /// Set a list of headers which are safe to expose to the API of a CORS API specification.
/// /// This corresponds to the `Access-Control-Expose-Headers` response header as specified in
/// This corresponds to the `Access-Control-Expose-Headers` response header. /// the [Fetch Standard CORS protocol].
/// ///
/// This defaults to an empty set. /// This defaults to an empty set.
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
pub fn expose_headers<U, H>(mut self, headers: U) -> Cors pub fn expose_headers<U, H>(mut self, headers: U) -> Cors
where where
U: IntoIterator<Item = H>, U: IntoIterator<Item = H>,
@ -367,76 +361,63 @@ impl Cors {
self self
} }
/// Sets a maximum time (in seconds) for which this CORS request may be cached. /// Set a maximum time (in seconds) for which this CORS request may be cached. This value is set
/// /// as the `Access-Control-Max-Age` header as specified in the [Fetch Standard CORS protocol].
/// This value is set as the `Access-Control-Max-Age` header.
/// ///
/// Pass a number (of seconds) or use None to disable sending max age header. /// Pass a number (of seconds) or use None to disable sending max age header.
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
pub fn max_age(mut self, max_age: impl Into<Option<usize>>) -> Cors { pub fn max_age(mut self, max_age: impl Into<Option<usize>>) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.max_age = max_age.into(); cors.max_age = max_age.into()
} }
self self
} }
/// Configures use of wildcard (`*`) origin in responses when appropriate. /// Set to use wildcard origins.
/// ///
/// If send wildcard is set and the `allowed_origins` parameter is `All`, a wildcard /// If send wildcard is set and the `allowed_origins` parameter is `All`, a wildcard
/// `Access-Control-Allow-Origin` response header is sent, rather than the requests /// `Access-Control-Allow-Origin` response header is sent, rather than the requests
/// `Origin` header. /// `Origin` header.
/// ///
/// This option **CANNOT** be used in conjunction with a [credential /// This **CANNOT** be used in conjunction with `allowed_origins` set to `All` and
/// supported](Self::supports_credentials()) configuration. Doing so will result in an error /// `allow_credentials` set to `true`. Depending on the mode of usage, this will either result
/// during server startup. /// in an `CorsError::CredentialsWithWildcardOrigin` error during actix launch or runtime.
/// ///
/// Defaults to disabled. /// Defaults to `false`.
pub fn send_wildcard(mut self) -> Cors { pub fn send_wildcard(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.send_wildcard = true; cors.send_wildcard = true
} }
self self
} }
/// Allows users to make authenticated requests. /// Allows users to make authenticated requests
/// ///
/// If true, injects the `Access-Control-Allow-Credentials` header in responses. This allows /// If true, injects the `Access-Control-Allow-Credentials` header in responses. This allows
/// cookies and credentials to be submitted across domains. /// cookies and credentials to be submitted across domains as specified in
/// the [Fetch Standard CORS protocol].
/// ///
/// This option **CANNOT** be used in conjunction with option cannot be used in conjunction /// This option cannot be used in conjunction with an `allowed_origin` set to `All` and
/// with [wildcard origins](Self::send_wildcard()) configured. Doing so will result in an error /// `send_wildcards` set to `true`.
/// during server startup.
///
/// Defaults to disabled.
pub fn supports_credentials(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.supports_credentials = true;
}
self
}
/// Allow private network access.
///
/// If true, injects the `Access-Control-Allow-Private-Network: true` header in responses if the
/// request contained the `Access-Control-Request-Private-Network: true` header.
///
/// For more information on this behavior, see the draft [Private Network Access] spec.
/// ///
/// Defaults to `false`. /// Defaults to `false`.
/// ///
/// [Private Network Access]: https://wicg.github.io/private-network-access /// A server initialization error will occur if credentials are allowed, but the Origin is set
#[cfg(feature = "draft-private-network-access")] /// to send wildcards (`*`); this is not allowed by the CORS protocol.
pub fn allow_private_network_access(mut self) -> Cors { ///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
pub fn supports_credentials(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.allow_private_network_access = true; cors.supports_credentials = true
} }
self self
} }
/// Disables `Vary` header support. /// Disable `Vary` header support.
/// ///
/// When enabled the header `Vary: Origin` will be returned as per the Fetch Standard /// When enabled the header `Vary: Origin` will be returned as per the Fetch Standard
/// implementation guidelines. /// implementation guidelines.
@ -448,39 +429,21 @@ impl Cors {
/// By default, `Vary` header support is enabled. /// By default, `Vary` header support is enabled.
pub fn disable_vary_header(mut self) -> Cors { pub fn disable_vary_header(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.vary_header = false; cors.vary_header = false
} }
self self
} }
/// Disables preflight request handling. /// Disable support for preflight requests.
/// ///
/// When enabled CORS middleware automatically handles `OPTIONS` requests. This is useful for /// When enabled CORS middleware automatically handles `OPTIONS` requests.
/// application level middleware. /// This is useful for application level middleware.
/// ///
/// By default, preflight support is enabled. /// By default *preflight* support is enabled.
pub fn disable_preflight(mut self) -> Cors { pub fn disable_preflight(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) { if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.preflight = false; cors.preflight = false
}
self
}
/// Configures whether requests should be pre-emptively blocked on mismatched origin.
///
/// If `true`, a 400 Bad Request is returned immediately when a request fails origin validation.
///
/// If `false`, the request will be processed as normal but relevant CORS headers will not be
/// appended to the response. In this case, the browser is trusted to validate CORS headers and
/// and block requests based on pre-flight requests. Use this setting to allow cURL and other
/// non-browser HTTP clients to function as normal, no matter what `Origin` the request has.
///
/// Defaults to false.
pub fn block_on_origin_mismatch(mut self, block: bool) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.block_on_origin_mismatch = block;
} }
self self
@ -510,10 +473,7 @@ impl Default for Cors {
preflight: true, preflight: true,
send_wildcard: false, send_wildcard: false,
supports_credentials: false, supports_credentials: false,
#[cfg(feature = "draft-private-network-access")]
allow_private_network_access: false,
vary_header: true, vary_header: true,
block_on_origin_mismatch: false,
}; };
Cors { Cors {
@ -608,27 +568,14 @@ 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, TryInto};
use actix_web::{ use actix_web::{
body, body,
dev::fn_service, dev::{fn_service, Transform},
http::StatusCode, http::{header::HeaderName, StatusCode},
test::{self, TestRequest}, test::{self, TestRequest},
HttpResponse, HttpResponse,
}; };
@ -659,9 +606,8 @@ mod test {
.insert_header(("Origin", "https://www.example.com")) .insert_header(("Origin", "https://www.example.com"))
.to_srv_request(); .to_srv_request();
let res = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(!res.headers().contains_key("Access-Control-Allow-Origin"));
} }
#[actix_web::test] #[actix_web::test]
@ -692,11 +638,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

@ -1,4 +1,4 @@
use std::{collections::HashSet, fmt, rc::Rc}; use std::{collections::HashSet, convert::TryFrom, convert::TryInto, fmt, rc::Rc};
use actix_web::{ use actix_web::{
dev::RequestHead, dev::RequestHead,
@ -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]>,
@ -65,22 +59,17 @@ pub(crate) struct Inner {
pub(crate) preflight: bool, pub(crate) preflight: bool,
pub(crate) send_wildcard: bool, pub(crate) send_wildcard: bool,
pub(crate) supports_credentials: bool, pub(crate) supports_credentials: bool,
#[cfg(feature = "draft-private-network-access")]
pub(crate) allow_private_network_access: bool,
pub(crate) vary_header: bool, pub(crate) vary_header: bool,
pub(crate) block_on_origin_mismatch: bool,
} }
static EMPTY_ORIGIN_SET: Lazy<HashSet<HeaderValue>> = Lazy::new(HashSet::new); static EMPTY_ORIGIN_SET: Lazy<HashSet<HeaderValue>> = Lazy::new(HashSet::new);
impl Inner { impl Inner {
/// The bool returned in Ok(_) position indicates whether the `Access-Control-Allow-Origin` pub(crate) fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> {
/// header should be added to the response or not.
pub(crate) fn validate_origin(&self, req: &RequestHead) -> Result<bool, CorsError> {
// return early if all origins are allowed or get ref to allowed origins set // return early if all origins are allowed or get ref to allowed origins set
#[allow(clippy::mutable_key_type)] #[allow(clippy::mutable_key_type)]
let allowed_origins = match &self.allowed_origins { let allowed_origins = match &self.allowed_origins {
AllOrSome::All if self.allowed_origins_fns.is_empty() => return Ok(true), AllOrSome::All if self.allowed_origins_fns.is_empty() => return Ok(()),
AllOrSome::Some(allowed_origins) => allowed_origins, AllOrSome::Some(allowed_origins) => allowed_origins,
// only function origin validators are defined // only function origin validators are defined
_ => &EMPTY_ORIGIN_SET, _ => &EMPTY_ORIGIN_SET,
@ -91,11 +80,9 @@ impl Inner {
// origin header exists and is a string // origin header exists and is a string
Some(origin) => { Some(origin) => {
if allowed_origins.contains(origin) || self.validate_origin_fns(origin, req) { if allowed_origins.contains(origin) || self.validate_origin_fns(origin, req) {
Ok(true) Ok(())
} else if self.block_on_origin_mismatch {
Err(CorsError::OriginNotAllowed)
} else { } else {
Ok(false) Err(CorsError::OriginNotAllowed)
} }
} }
@ -222,20 +209,8 @@ pub(crate) fn add_vary_header(headers: &mut HeaderMap) {
let mut val: Vec<u8> = Vec::with_capacity(hdr.len() + 71); let mut val: Vec<u8> = Vec::with_capacity(hdr.len() + 71);
val.extend(hdr.as_bytes()); val.extend(hdr.as_bytes());
val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers"); val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
#[cfg(feature = "draft-private-network-access")]
val.extend(b", Access-Control-Request-Private-Network");
val.try_into().unwrap() val.try_into().unwrap()
} }
#[cfg(feature = "draft-private-network-access")]
None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, \
Access-Control-Request-Private-Network",
),
#[cfg(not(feature = "draft-private-network-access"))]
None => HeaderValue::from_static( None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
), ),
@ -267,7 +242,6 @@ mod test {
async fn test_validate_not_allowed_origin() { async fn test_validate_not_allowed_origin() {
let cors = Cors::default() let cors = Cors::default()
.allowed_origin("https://www.example.com") .allowed_origin("https://www.example.com")
.block_on_origin_mismatch(true)
.new_transform(test::ok_service()) .new_transform(test::ok_service())
.await .await
.unwrap(); .unwrap();

View File

@ -1,16 +1,11 @@
//! Cross-Origin Resource Sharing (CORS) controls for Actix Web. //! Cross-Origin Resource Sharing (CORS) controls for Actix Web.
//! //!
//! This middleware can be applied to both applications and resources. Once built, a [`Cors`] //! This middleware can be applied to both applications and resources. Once built, a
//! builder can be used as an argument for Actix Web's `App::wrap()`, `Scope::wrap()`, or //! [`Cors`] builder can be used as an argument for Actix Web's `App::wrap()`,
//! `Resource::wrap()` methods. //! `Scope::wrap()`, or `Resource::wrap()` methods.
//! //!
//! This CORS middleware automatically handles `OPTIONS` preflight requests. //! This CORS middleware automatically handles `OPTIONS` preflight requests.
//! //!
//! # Crate Features
//! - `draft-private-network-access`: ⚠️ Unstable. Adds opt-in support for the [Private Network
//! Access] spec extensions. This feature is unstable since it will follow breaking changes in the
//! draft spec until it is finalized.
//!
//! # Example //! # Example
//! ```no_run //! ```no_run
//! use actix_cors::Cors; //! use actix_cors::Cors;
@ -25,7 +20,7 @@
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| { //! HttpServer::new(|| {
//! let cors = Cors::default() //! let cors = Cors::default()
//! .allowed_origin("https://www.rust-lang.org") //! .allowed_origin("https://www.rust-lang.org/")
//! .allowed_origin_fn(|origin, _req_head| { //! .allowed_origin_fn(|origin, _req_head| {
//! origin.as_bytes().ends_with(b".rust-lang.org") //! origin.as_bytes().ends_with(b".rust-lang.org")
//! }) //! })
@ -45,14 +40,12 @@
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
//!
//! [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")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod all_or_some; mod all_or_some;
mod builder; mod builder;
@ -60,8 +53,8 @@ mod error;
mod inner; mod inner;
mod middleware; mod middleware;
use crate::{ use all_or_some::AllOrSome;
all_or_some::AllOrSome, pub use builder::Cors;
inner::{Inner, OriginFn}, pub use error::CorsError;
}; use inner::{Inner, OriginFn};
pub use crate::{builder::Cors, error::CorsError, middleware::CorsMiddleware}; pub use middleware::CorsMiddleware;

View File

@ -16,7 +16,7 @@ use log::debug;
use crate::{ use crate::{
builder::intersperse_header_values, builder::intersperse_header_values,
inner::{add_vary_header, header_value_try_into_method}, inner::{add_vary_header, header_value_try_into_method},
AllOrSome, CorsError, Inner, AllOrSome, Inner,
}; };
/// Service wrapper for Cross-Origin Resource Sharing support. /// Service wrapper for Cross-Origin Resource Sharing support.
@ -60,14 +60,9 @@ impl<S> CorsMiddleware<S> {
fn handle_preflight(&self, req: ServiceRequest) -> ServiceResponse { fn handle_preflight(&self, req: ServiceRequest) -> ServiceResponse {
let inner = Rc::clone(&self.inner); let inner = Rc::clone(&self.inner);
match inner.validate_origin(req.head()) {
Ok(true) => {}
Ok(false) => return req.error_response(CorsError::OriginNotAllowed),
Err(err) => return req.error_response(err),
};
if let Err(err) = inner if let Err(err) = inner
.validate_allowed_method(req.head()) .validate_origin(req.head())
.and_then(|_| inner.validate_allowed_method(req.head()))
.and_then(|_| inner.validate_allowed_headers(req.head())) .and_then(|_| inner.validate_allowed_headers(req.head()))
{ {
return req.error_response(err); return req.error_response(err);
@ -93,18 +88,6 @@ impl<S> CorsMiddleware<S> {
res.insert_header((header::ACCESS_CONTROL_ALLOW_HEADERS, headers.clone())); res.insert_header((header::ACCESS_CONTROL_ALLOW_HEADERS, headers.clone()));
} }
#[cfg(feature = "draft-private-network-access")]
if inner.allow_private_network_access
&& req
.headers()
.contains_key("access-control-request-private-network")
{
res.insert_header((
header::HeaderName::from_static("access-control-allow-private-network"),
HeaderValue::from_static("true"),
));
}
if inner.supports_credentials { if inner.supports_credentials {
res.insert_header(( res.insert_header((
header::ACCESS_CONTROL_ALLOW_CREDENTIALS, header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
@ -125,17 +108,11 @@ impl<S> CorsMiddleware<S> {
req.into_response(res) req.into_response(res)
} }
fn augment_response<B>( fn augment_response<B>(inner: &Inner, mut res: ServiceResponse<B>) -> ServiceResponse<B> {
inner: &Inner,
origin_allowed: bool,
mut res: ServiceResponse<B>,
) -> ServiceResponse<B> {
if origin_allowed {
if let Some(origin) = inner.access_control_allow_origin(res.request().head()) { if let Some(origin) = inner.access_control_allow_origin(res.request().head()) {
res.headers_mut() res.headers_mut()
.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin); .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin);
}; };
}
if let Some(ref expose) = inner.expose_headers_baked { if let Some(ref expose) = inner.expose_headers_baked {
log::trace!("exposing selected headers: {:?}", expose); log::trace!("exposing selected headers: {:?}", expose);
@ -149,6 +126,7 @@ impl<S> CorsMiddleware<S> {
let expose_all_request_headers = res let expose_all_request_headers = res
.headers() .headers()
.keys() .keys()
.into_iter()
.map(|name| name.as_str()) .map(|name| name.as_str())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
@ -173,19 +151,6 @@ impl<S> CorsMiddleware<S> {
); );
} }
#[cfg(feature = "draft-private-network-access")]
if inner.allow_private_network_access
&& res
.request()
.headers()
.contains_key("access-control-request-private-network")
{
res.headers_mut().insert(
header::HeaderName::from_static("access-control-allow-private-network"),
HeaderValue::from_static("true"),
);
}
if inner.vary_header { if inner.vary_header {
add_vary_header(res.headers_mut()); add_vary_header(res.headers_mut());
} }
@ -217,10 +182,8 @@ where
} }
// only check actual requests with a origin header // only check actual requests with a origin header
let origin_allowed = match (origin, self.inner.validate_origin(req.head())) { if origin.is_some() {
(None, _) => false, if let Err(err) = self.inner.validate_origin(req.head()) {
(_, Ok(origin_allowed)) => origin_allowed,
(_, Err(err)) => {
debug!("origin validation failed; inner service is not called"); debug!("origin validation failed; inner service is not called");
let mut res = req.error_response(err); let mut res = req.error_response(err);
@ -230,14 +193,14 @@ where
return ok(res.map_into_right_body()).boxed_local(); return ok(res.map_into_right_body()).boxed_local();
} }
}; }
let inner = Rc::clone(&self.inner); let inner = Rc::clone(&self.inner);
let fut = self.service.call(req); let fut = self.service.call(req);
Box::pin(async move { Box::pin(async move {
let res = fut.await; let res = fut.await;
Ok(Self::augment_response(&inner, origin_allowed, res?).map_into_left_body()) Ok(Self::augment_response(&inner, res?).map_into_left_body())
}) })
} }
} }

View File

@ -1,7 +1,8 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_utils::future::ok; use actix_utils::future::ok;
use actix_web::dev::fn_service;
use actix_web::{ use actix_web::{
dev::{fn_service, ServiceRequest, Transform}, dev::{ServiceRequest, Transform},
http::{ http::{
header::{self, HeaderValue}, header::{self, HeaderValue},
Method, StatusCode, Method, StatusCode,
@ -264,16 +265,10 @@ async fn test_response() {
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN) .get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes) .map(HeaderValue::as_bytes)
); );
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes), resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]), Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]),
); );
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"[..]),
);
#[allow(clippy::needless_collect)] #[allow(clippy::needless_collect)]
{ {
@ -317,18 +312,9 @@ async fn test_response() {
.method(Method::OPTIONS) .method(Method::OPTIONS)
.to_srv_request(); .to_srv_request();
let resp = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
resp.headers() resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
.get(header::VARY) Some(&b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]),
.map(HeaderValue::as_bytes)
.unwrap(),
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes).unwrap(),
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
); );
let cors = Cors::default() let cors = Cors::default()
@ -369,55 +355,6 @@ async fn test_validate_origin() {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_web::test]
async fn test_blocks_mismatched_origin_by_default() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.test"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
}
#[actix_web::test]
async fn test_mismatched_origin_block_turned_off() {
let cors = Cors::default()
.allow_any_method()
.allowed_origin("https://www.example.com")
.block_on_origin_mismatch(false)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://wrong.com"))
.insert_header(("Access-Control-Request-Method", "POST"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN), None);
let req = TestRequest::get()
.insert_header(("Origin", "https://wrong.com"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN), None);
}
#[actix_web::test] #[actix_web::test]
async fn test_no_origin_response() { async fn test_no_origin_response() {
let cors = Cors::permissive() let cors = Cors::permissive()
@ -479,7 +416,6 @@ async fn vary_header_on_all_handled_responses() {
assert!(resp assert!(resp
.headers() .headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)); .contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
resp.headers() resp.headers()
.get(header::VARY) .get(header::VARY)
@ -488,15 +424,6 @@ async fn vary_header_on_all_handled_responses() {
.unwrap(), .unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
); );
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
// follow-up regular request // follow-up regular request
let req = TestRequest::default() let req = TestRequest::default()
@ -505,7 +432,6 @@ async fn vary_header_on_all_handled_responses() {
.to_srv_request(); .to_srv_request();
let resp = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
resp.headers() resp.headers()
.get(header::VARY) .get(header::VARY)
@ -514,15 +440,6 @@ async fn vary_header_on_all_handled_responses() {
.unwrap(), .unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
); );
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
let cors = Cors::default() let cors = Cors::default()
.allow_any_method() .allow_any_method()
@ -530,44 +447,26 @@ async fn vary_header_on_all_handled_responses() {
.await .await
.unwrap(); .unwrap();
// regular request OK with no CORS response headers // regular request bad origin
let req = TestRequest::default() let req = TestRequest::default()
.method(Method::PUT) .method(Method::PUT)
.insert_header((header::ORIGIN, "https://www.example.com")) .insert_header((header::ORIGIN, "https://www.example.com"))
.to_srv_request(); .to_srv_request();
let res = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
res.headers() resp.headers()
.get(header::VARY) .get(header::VARY)
.expect("response should have Vary header") .expect("response should have Vary header")
.to_str() .to_str()
.unwrap(), .unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
); );
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
res.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
// regular request no origin // regular request no origin
let req = TestRequest::default().method(Method::PUT).to_srv_request(); let req = TestRequest::default().method(Method::PUT).to_srv_request();
let resp = test::call_service(&cors, req).await; let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!( assert_eq!(
resp.headers() resp.headers()
.get(header::VARY) .get(header::VARY)
@ -576,15 +475,6 @@ async fn vary_header_on_all_handled_responses() {
.unwrap(), .unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
); );
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
} }
#[actix_web::test] #[actix_web::test]
@ -641,42 +531,3 @@ async fn expose_all_request_header_values() {
assert!(cd_hdr.contains("content-disposition")); assert!(cd_hdr.contains("content-disposition"));
assert!(cd_hdr.contains("access-control-allow-origin")); assert!(cd_hdr.contains("access-control-allow-origin"));
} }
#[cfg(feature = "draft-private-network-access")]
#[actix_web::test]
async fn private_network_access() {
let cors = Cors::permissive()
.allowed_origin("https://public.site")
.allow_private_network_access()
.new_transform(fn_service(|req: ServiceRequest| async move {
let res = req.into_response(
HttpResponse::Ok()
.insert_header((header::CONTENT_DISPOSITION, "test disposition"))
.finish(),
);
Ok(res)
}))
.await
.unwrap();
let req = TestRequest::default()
.insert_header((header::ORIGIN, "https://public.site"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert!(res.headers().contains_key("access-control-allow-origin"));
let req = TestRequest::default()
.insert_header((header::ORIGIN, "https://public.site"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"))
.insert_header(("Access-Control-Request-Private-Network", "true"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert!(res.headers().contains_key("access-control-allow-origin"));
assert!(res
.headers()
.contains_key("access-control-allow-private-network"));
}

View File

@ -1,41 +1,22 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.8.0
- Update `actix-session` dependency to `0.10`.
## 0.7.1
- Add `IdentityMiddlewareBuilder::{id_key, last_visit_unix_timestamp_key, login_unix_timestamp_key}()` methods for customizing keys used in session. Defaults remain the same as before.
## 0.7.0
- Update `actix-session` dependency to `0.9`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.6.0
- Add `error` module.
- Replace use of `anyhow::Error` in return types with specific error types.
- Update `actix-session` dependency to `0.8`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.5.2
## 0.5.2 - 2022-07-19
- Fix visit deadline. [#263] - Fix visit deadline. [#263]
[#263]: https://github.com/actix/actix-extras/pull/263 [#263]: https://github.com/actix/actix-extras/pull/263
## 0.5.1
## 0.5.1 - 2022-07-11
- Remove unnecessary dependencies. [#259] - Remove unnecessary dependencies. [#259]
[#259]: https://github.com/actix/actix-extras/pull/259 [#259]: https://github.com/actix/actix-extras/pull/259
## 0.5.0
## 0.5.0 - 2022-07-11
`actix-identity` v0.5 is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead. `actix-identity` v0.5 is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead.
`actix-identity` is now designed as an additional layer on top of `actix-session` v0.7, focused on identity management. The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (e.g., it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now! `actix-identity` is now designed as an additional layer on top of `actix-session` v0.7, focused on identity management. The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (e.g., it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now!
@ -70,57 +51,57 @@ Changes:
[#246]: https://github.com/actix/actix-extras/pull/246 [#246]: https://github.com/actix/actix-extras/pull/246
## 0.4.0
## 0.4.0 - 2022-03-01
- Update `actix-web` dependency to `4`. - Update `actix-web` dependency to `4`.
## 0.4.0-beta.9
## 0.4.0-beta.9 - 2022-02-07
- Relax body type bounds on middleware impl. [#223] - Relax body type bounds on middleware impl. [#223]
- Update `actix-web` dependency to `4.0.0-rc.1`. - Update `actix-web` dependency to `4.0.0-rc.1`.
[#223]: https://github.com/actix/actix-extras/pull/223 [#223]: https://github.com/actix/actix-extras/pull/223
## 0.4.0-beta.8
## 0.4.0-beta.8 - 2022-01-21
- No significant changes since `0.4.0-beta.7`. - No significant changes since `0.4.0-beta.7`.
## 0.4.0-beta.7
## 0.4.0-beta.7 - 2021-12-29
- Update `actix-web` dependency to `4.0.0.beta-18`. [#218] - Update `actix-web` dependency to `4.0.0.beta-18`. [#218]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
[#218]: https://github.com/actix/actix-extras/pull/218 [#218]: https://github.com/actix/actix-extras/pull/218
## 0.4.0-beta.6
## 0.4.0-beta.6 - 2021-12-18
- Update `actix-web` dependency to `4.0.0.beta-15`. [#216] - Update `actix-web` dependency to `4.0.0.beta-15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216 [#216]: https://github.com/actix/actix-extras/pull/216
## 0.4.0-beta.5
## 0.4.0-beta.5 - 2021-12-12
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209] - Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#209]: https://github.com/actix/actix-extras/pull/209 [#209]: https://github.com/actix/actix-extras/pull/209
## 0.4.0-beta.4
## 0.4.0-beta.4 - 2021-11-22
- No significant changes since `0.4.0-beta.3`. - No significant changes since `0.4.0-beta.3`.
## 0.4.0-beta.3
## 0.4.0-beta.3 - 2021-10-21
- Update `actix-web` dependency to v4.0.0-beta.10. [#203] - Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
[#203]: https://github.com/actix/actix-extras/pull/203 [#203]: https://github.com/actix/actix-extras/pull/203
## 0.4.0-beta.2
## 0.4.0-beta.2 - 2021-06-27
- No notable changes. - No notable changes.
## 0.4.0-beta.1
## 0.4.0-beta.1 - 2021-04-02
- Rename `CookieIdentityPolicy::{max_age => max_age_secs}`. [#168] - Rename `CookieIdentityPolicy::{max_age => max_age_secs}`. [#168]
- Rename `CookieIdentityPolicy::{max_age_time => max_age}`. [#168] - Rename `CookieIdentityPolicy::{max_age_time => max_age}`. [#168]
- Update `actix-web` dependency to 4.0.0 beta. - Update `actix-web` dependency to 4.0.0 beta.
@ -128,31 +109,31 @@ Changes:
[#168]: https://github.com/actix/actix-extras/pull/168 [#168]: https://github.com/actix/actix-extras/pull/168
## 0.3.1
## 0.3.1 - 2020-09-20
- Add method to set `HttpOnly` flag on cookie identity. [#102] - Add method to set `HttpOnly` flag on cookie identity. [#102]
[#102]: https://github.com/actix/actix-extras/pull/102 [#102]: https://github.com/actix/actix-extras/pull/102
## 0.3.0
## 0.3.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0. - Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0. - Minimum supported Rust version (MSRV) is now 1.42.0.
## 0.3.0-alpha.1
## 0.3.0-alpha.1 - 2020-03-14
- Update the `time` dependency to 0.2.7 - Update the `time` dependency to 0.2.7
- Update the `actix-web` dependency to 3.0.0-alpha.1 - Update the `actix-web` dependency to 3.0.0-alpha.1
- Minimize `futures` dependency - Minimize `futures` dependency
## 0.2.1
## 0.2.1 - 2020-01-10
- Fix panic with already borrowed: BorrowMutError #1263 - Fix panic with already borrowed: BorrowMutError #1263
## 0.2.0 - 2019-12-20
## 0.2.0 - 2019-12-20
- Use actix-web 2.0 - Use actix-web 2.0
## 0.1.0 - 2019-06-xx
## 0.1.0 - 2019-06-xx
- Move identity middleware to separate crate - Move identity middleware to separate crate

View File

@ -1,41 +1,37 @@
[package] [package]
name = "actix-identity" name = "actix-identity"
version = "0.8.0" version = "0.5.2"
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 = "MIT OR Apache-2.0"
edition.workspace = true edition = "2018"
rust-version.workspace = true
[package.metadata.docs.rs] [lib]
rustdoc-args = ["--cfg", "docsrs"] name = "actix_identity"
all-features = true path = "src/lib.rs"
[dependencies] [dependencies]
actix-service = "2" actix-service = "2"
actix-session = "0.10" actix-session = "0.7"
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"] } anyhow = "1"
futures-core = "0.3.17" futures-core = "0.3.7"
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"] }
[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.7", features = ["redis-rs-session", "cookie-session"] }
env_logger = "0.11" env_logger = "0.9"
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

@ -2,105 +2,12 @@
> Identity management for Actix Web. > Identity management for Actix Web.
<!-- 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.5.2)](https://docs.rs/actix-identity/0.5.2)
![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.5.2/status.svg)](https://deps.rs/crate/actix-identity/0.5.2)
<!-- prettier-ignore-end --> ## Documentation & community resources
<!-- cargo-rdme start --> - [API Documentation](https://docs.rs/actix-identity)
- Minimum Supported Rust Version (MSRV): 1.57
Identity management for Actix Web.
`actix-identity` can be used to track identity of a user across multiple requests. It is built on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
## Getting started
To start using identity management in your Actix Web application you must register [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
```rust
use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
use actix_identity::IdentityMiddleware;
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
#[actix_web::main]
async fn main() {
// When using `Key::generate()` it is important to initialize outside of the
// `HttpServer::new` closure. When deployed the secret key should be read from a
// configuration file or environment variables.
let secret_key = Key::generate();
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap();
HttpServer::new(move || {
App::new()
// Install the identity framework first.
.wrap(IdentityMiddleware::default())
// The identity system is built on top of sessions. You must install the session
// middleware to leverage `actix-identity`. The session middleware must be mounted
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
// order of registration when it receives an incoming request.
.wrap(SessionMiddleware::new(
redis_store.clone(),
secret_key.clone(),
))
// Your request handlers [...]
})
}
```
User identities can be created, accessed and destroyed using the [`Identity`] extractor in your request handlers:
```rust
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
use actix_identity::Identity;
use actix_session::storage::RedisSessionStore;
#[get("/")]
async fn index(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
format!("Welcome! {}", user.id().unwrap())
} else {
"Welcome Anonymous!".to_owned()
}
}
#[post("/login")]
async fn login(request: HttpRequest) -> impl Responder {
// Some kind of authentication should happen here
// e.g. password-based, biometric, etc.
// [...]
// attach a verified user identity to the active session
Identity::login(&request.extensions(), "User1".into()).unwrap();
HttpResponse::Ok()
}
#[post("/logout")]
async fn logout(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
user.logout();
}
HttpResponse::Ok()
}
```
## Advanced configuration
By default, `actix-identity` does not automatically log out users. You can change this behaviour by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
In particular, you can automatically log out users who:
- have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]);
- logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
[`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
[`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
<!-- cargo-rdme end -->

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();
let identity_mw = IdentityMiddleware::builder()
.visit_deadline(Some(expiration))
.build(); .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

@ -9,9 +9,6 @@ pub(crate) struct Configuration {
pub(crate) on_logout: LogoutBehaviour, pub(crate) on_logout: LogoutBehaviour,
pub(crate) login_deadline: Option<Duration>, pub(crate) login_deadline: Option<Duration>,
pub(crate) visit_deadline: Option<Duration>, pub(crate) visit_deadline: Option<Duration>,
pub(crate) id_key: &'static str,
pub(crate) last_visit_unix_timestamp_key: &'static str,
pub(crate) login_unix_timestamp_key: &'static str,
} }
impl Default for Configuration { impl Default for Configuration {
@ -20,9 +17,6 @@ impl Default for Configuration {
on_logout: LogoutBehaviour::PurgeSession, on_logout: LogoutBehaviour::PurgeSession,
login_deadline: None, login_deadline: None,
visit_deadline: None, visit_deadline: None,
id_key: "actix_identity.user_id",
last_visit_unix_timestamp_key: "actix_identity.last_visited_at",
login_unix_timestamp_key: "actix_identity.logged_in_at",
} }
} }
} }
@ -64,24 +58,6 @@ impl IdentityMiddlewareBuilder {
} }
} }
/// Set a custom key to identify the user in the session.
pub fn id_key(mut self, key: &'static str) -> Self {
self.configuration.id_key = key;
self
}
/// Set a custom key to store the last visited unix timestamp.
pub fn last_visit_unix_timestamp_key(mut self, key: &'static str) -> Self {
self.configuration.last_visit_unix_timestamp_key = key;
self
}
/// Set a custom key to store the login unix timestamp.
pub fn login_unix_timestamp_key(mut self, key: &'static str) -> Self {
self.configuration.login_unix_timestamp_key = key;
self
}
/// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session. /// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session.
/// ///
/// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]). /// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]).

View File

@ -1,70 +0,0 @@
//! Failure modes of identity operations.
use actix_session::{SessionGetError, SessionInsertError};
use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError};
use derive_more::derive::{Display, Error, From};
/// Error that can occur during login attempts.
#[derive(Debug, Display, Error, From)]
#[display("{_0}")]
pub struct LoginError(SessionInsertError);
impl ResponseError for LoginError {
fn status_code(&self) -> StatusCode {
StatusCode::UNAUTHORIZED
}
}
/// Error encountered when working with a session that has expired.
#[derive(Debug, Display, Error)]
#[display("The given session has expired and is no longer valid")]
pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
/// The identity information has been lost.
///
/// Seeing this error in user code indicates a bug in actix-identity.
#[derive(Debug, Display, Error)]
#[display(
"The identity information in the current session has disappeared after having been \
successfully validated. This is likely to be a bug."
)]
#[non_exhaustive]
pub struct LostIdentityError;
/// There is no identity information attached to the current session.
#[derive(Debug, Display, Error)]
#[display("There is no identity information attached to the current session")]
#[non_exhaustive]
pub struct MissingIdentityError;
/// Errors that can occur while retrieving an identity.
#[derive(Debug, Display, Error, From)]
#[non_exhaustive]
pub enum GetIdentityError {
/// The session has expired.
#[display("{_0}")]
SessionExpiryError(SessionExpiryError),
/// No identity is found in a session.
#[display("{_0}")]
MissingIdentityError(MissingIdentityError),
/// Failed to accessing the session store.
#[display("{_0}")]
SessionGetError(SessionGetError),
/// Identity info was lost after being validated.
///
/// Seeing this error indicates a bug in actix-identity.
#[display("{_0}")]
LostIdentityError(LostIdentityError),
}
impl ResponseError for GetIdentityError {
fn status_code(&self) -> StatusCode {
match self {
Self::LostIdentityError(_) => StatusCode::INTERNAL_SERVER_ERROR,
_ => StatusCode::UNAUTHORIZED,
}
}
}

View File

@ -6,13 +6,9 @@ use actix_web::{
http::StatusCode, http::StatusCode,
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
}; };
use anyhow::{anyhow, Context};
use crate::{ use crate::config::LogoutBehaviour;
config::LogoutBehaviour,
error::{
GetIdentityError, LoginError, LostIdentityError, MissingIdentityError, SessionExpiryError,
},
};
/// A verified user identity. It can be used as a request extractor. /// A verified user identity. It can be used as a request extractor.
/// ///
@ -82,9 +78,6 @@ pub(crate) struct IdentityInner {
pub(crate) logout_behaviour: LogoutBehaviour, pub(crate) logout_behaviour: LogoutBehaviour,
pub(crate) is_login_deadline_enabled: bool, pub(crate) is_login_deadline_enabled: bool,
pub(crate) is_visit_deadline_enabled: bool, pub(crate) is_visit_deadline_enabled: bool,
pub(crate) id_key: &'static str,
pub(crate) last_visit_unix_timestamp_key: &'static str,
pub(crate) login_unix_timestamp_key: &'static str,
} }
impl IdentityInner { impl IdentityInner {
@ -102,13 +95,20 @@ impl IdentityInner {
} }
/// Retrieve the user id attached to the current session. /// Retrieve the user id attached to the current session.
fn get_identity(&self) -> Result<String, GetIdentityError> { fn get_identity(&self) -> Result<String, anyhow::Error> {
self.session self.session
.get::<String>(self.id_key)? .get::<String>(ID_KEY)
.ok_or_else(|| MissingIdentityError.into()) .context("Failed to deserialize the user identifier attached to the current session")?
.ok_or_else(|| {
anyhow!("There is no identity information attached to the current session")
})
} }
} }
pub(crate) const ID_KEY: &str = "actix_identity.user_id";
pub(crate) const LAST_VISIT_UNIX_TIMESTAMP_KEY: &str = "actix_identity.last_visited_at";
pub(crate) const LOGIN_UNIX_TIMESTAMP_KEY: &str = "actix_identity.logged_in_at";
impl Identity { impl Identity {
/// Return the user id associated to the current session. /// Return the user id associated to the current session.
/// ///
@ -126,11 +126,10 @@ impl Identity {
/// } /// }
/// } /// }
/// ``` /// ```
pub fn id(&self) -> Result<String, GetIdentityError> { pub fn id(&self) -> Result<String, anyhow::Error> {
self.0 self.0.session.get(ID_KEY)?.ok_or_else(|| {
.session anyhow!("Bug: the identity information attached to the current session has disappeared")
.get(self.0.id_key)? })
.ok_or_else(|| LostIdentityError.into())
} }
/// Attach a valid user identity to the current session. /// Attach a valid user identity to the current session.
@ -150,17 +149,15 @@ impl Identity {
/// HttpResponse::Ok() /// HttpResponse::Ok()
/// } /// }
/// ``` /// ```
pub fn login(ext: &Extensions, id: String) -> Result<Self, LoginError> { pub fn login(ext: &Extensions, id: String) -> Result<Self, anyhow::Error> {
let inner = IdentityInner::extract(ext); let inner = IdentityInner::extract(ext);
inner.session.insert(inner.id_key, id)?; inner.session.insert(ID_KEY, id)?;
let now = OffsetDateTime::now_utc().unix_timestamp(); let now = OffsetDateTime::now_utc().unix_timestamp();
if inner.is_login_deadline_enabled { if inner.is_login_deadline_enabled {
inner.session.insert(inner.login_unix_timestamp_key, now)?; inner.session.insert(LOGIN_UNIX_TIMESTAMP_KEY, now)?;
} }
if inner.is_visit_deadline_enabled { if inner.is_visit_deadline_enabled {
inner inner.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
.session
.insert(inner.last_visit_unix_timestamp_key, now)?;
} }
inner.session.renew(); inner.session.renew();
Ok(Self(inner)) Ok(Self(inner))
@ -192,48 +189,44 @@ impl Identity {
self.0.session.purge(); self.0.session.purge();
} }
LogoutBehaviour::DeleteIdentityKeys => { LogoutBehaviour::DeleteIdentityKeys => {
self.0.session.remove(self.0.id_key); self.0.session.remove(ID_KEY);
if self.0.is_login_deadline_enabled { if self.0.is_login_deadline_enabled {
self.0.session.remove(self.0.login_unix_timestamp_key); self.0.session.remove(LOGIN_UNIX_TIMESTAMP_KEY);
} }
if self.0.is_visit_deadline_enabled { if self.0.is_visit_deadline_enabled {
self.0.session.remove(self.0.last_visit_unix_timestamp_key); self.0.session.remove(LAST_VISIT_UNIX_TIMESTAMP_KEY);
} }
} }
} }
} }
pub(crate) fn extract(ext: &Extensions) -> Result<Self, GetIdentityError> { pub(crate) fn extract(ext: &Extensions) -> Result<Self, anyhow::Error> {
let inner = IdentityInner::extract(ext); let inner = IdentityInner::extract(ext);
inner.get_identity()?; inner.get_identity()?;
Ok(Self(inner)) Ok(Self(inner))
} }
pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> { pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
Ok(self
.0
.session
.get(self.0.login_unix_timestamp_key)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(SessionExpiryError)?)
}
pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> {
Ok(self
.0
.session
.get(self.0.last_visit_unix_timestamp_key)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(SessionExpiryError)?)
}
pub(crate) fn set_last_visited_at(&self) -> Result<(), LoginError> {
let now = OffsetDateTime::now_utc().unix_timestamp();
self.0 self.0
.session .session
.insert(self.0.last_visit_unix_timestamp_key, now)?; .get(LOGIN_UNIX_TIMESTAMP_KEY)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(anyhow::Error::from)
}
pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, anyhow::Error> {
self.0
.session
.get(LAST_VISIT_UNIX_TIMESTAMP_KEY)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(anyhow::Error::from)
}
pub(crate) fn set_last_visited_at(&self) -> Result<(), anyhow::Error> {
let now = OffsetDateTime::now_utc().unix_timestamp();
self.0.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
Ok(()) Ok(())
} }
} }

View File

@ -1,27 +1,27 @@
use actix_web::{dev::ServiceRequest, guard::GuardContext, HttpMessage, HttpRequest}; use actix_web::{dev::ServiceRequest, guard::GuardContext, HttpMessage, HttpRequest};
use crate::{error::GetIdentityError, Identity}; use crate::Identity;
/// Helper trait to retrieve an [`Identity`] instance from various `actix-web`'s types. /// Helper trait to retrieve an [`Identity`] instance from various `actix-web`'s types.
pub trait IdentityExt { pub trait IdentityExt {
/// Retrieve the identity attached to the current session, if available. /// Retrieve the identity attached to the current session, if available.
fn get_identity(&self) -> Result<Identity, GetIdentityError>; fn get_identity(&self) -> Result<Identity, anyhow::Error>;
} }
impl IdentityExt for HttpRequest { impl IdentityExt for HttpRequest {
fn get_identity(&self) -> Result<Identity, GetIdentityError> { fn get_identity(&self) -> Result<Identity, anyhow::Error> {
Identity::extract(&self.extensions()) Identity::extract(&self.extensions())
} }
} }
impl IdentityExt for ServiceRequest { impl IdentityExt for ServiceRequest {
fn get_identity(&self) -> Result<Identity, GetIdentityError> { fn get_identity(&self) -> Result<Identity, anyhow::Error> {
Identity::extract(&self.extensions()) Identity::extract(&self.extensions())
} }
} }
impl IdentityExt for GuardContext<'_> { impl<'a> IdentityExt for GuardContext<'a> {
fn get_identity(&self) -> Result<Identity, GetIdentityError> { fn get_identity(&self) -> Result<Identity, anyhow::Error> {
Identity::extract(&self.req_data()) Identity::extract(&self.req_data())
} }
} }

View File

@ -1,109 +1,100 @@
/*! //! Identity management for Actix Web.
Identity management for Actix Web. //!
//! `actix-identity` can be used to track identity of a user across multiple requests. It is built
`actix-identity` can be used to track identity of a user across multiple requests. It is built //! on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session). //!
//! # Getting started
# Getting started //! To start using identity management in your Actix Web application you must register
To start using identity management in your Actix Web application you must register //! [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
[`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`: //!
//! ```no_run
```no_run //! # use actix_web::web;
# use actix_web::web; //! use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
use actix_web::{cookie::Key, App, HttpServer, HttpResponse}; //! use actix_identity::IdentityMiddleware;
use actix_identity::IdentityMiddleware; //! use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_session::{storage::RedisSessionStore, SessionMiddleware}; //!
//! #[actix_web::main]
#[actix_web::main] //! async fn main() {
async fn main() { //! let secret_key = Key::generate();
// When using `Key::generate()` it is important to initialize outside of the //! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
// `HttpServer::new` closure. When deployed the secret key should be read from a //! .await
// configuration file or environment variables. //! .unwrap();
let secret_key = Key::generate(); //!
//! HttpServer::new(move || {
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379") //! App::new()
.await //! // Install the identity framework first.
.unwrap(); //! .wrap(IdentityMiddleware::default())
//! // The identity system is built on top of sessions. You must install the session
HttpServer::new(move || { //! // middleware to leverage `actix-identity`. The session middleware must be mounted
App::new() //! // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
// Install the identity framework first. //! // order of registration when it receives an incoming request.
.wrap(IdentityMiddleware::default()) //! .wrap(SessionMiddleware::new(
// The identity system is built on top of sessions. You must install the session //! redis_store.clone(),
// middleware to leverage `actix-identity`. The session middleware must be mounted //! secret_key.clone()
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE //! ))
// order of registration when it receives an incoming request. //! // Your request handlers [...]
.wrap(SessionMiddleware::new( //! # .default_service(web::to(|| HttpResponse::Ok()))
redis_store.clone(), //! })
secret_key.clone(), //! # ;
)) //! }
// Your request handlers [...] //! ```
# .default_service(web::to(|| HttpResponse::Ok())) //!
}) //! User identities can be created, accessed and destroyed using the [`Identity`] extractor in your
# ; //! request handlers:
} //!
``` //! ```no_run
//! use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
User identities can be created, accessed and destroyed using the [`Identity`] extractor in your //! use actix_identity::Identity;
request handlers: //! use actix_session::storage::RedisSessionStore;
//!
```no_run //! #[get("/")]
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage}; //! async fn index(user: Option<Identity>) -> impl Responder {
use actix_identity::Identity; //! if let Some(user) = user {
use actix_session::storage::RedisSessionStore; //! format!("Welcome! {}", user.id().unwrap())
//! } else {
#[get("/")] //! "Welcome Anonymous!".to_owned()
async fn index(user: Option<Identity>) -> impl Responder { //! }
if let Some(user) = user { //! }
format!("Welcome! {}", user.id().unwrap()) //!
} else { //! #[post("/login")]
"Welcome Anonymous!".to_owned() //! async fn login(request: HttpRequest) -> impl Responder {
} //! // Some kind of authentication should happen here
} //! // e.g. password-based, biometric, etc.
//! // [...]
#[post("/login")] //!
async fn login(request: HttpRequest) -> impl Responder { //! // attach a verified user identity to the active session
// Some kind of authentication should happen here //! Identity::login(&request.extensions(), "User1".into()).unwrap();
// e.g. password-based, biometric, etc. //!
// [...] //! HttpResponse::Ok()
//! }
// attach a verified user identity to the active session //!
Identity::login(&request.extensions(), "User1".into()).unwrap(); //! #[post("/logout")]
//! async fn logout(user: Identity) -> impl Responder {
HttpResponse::Ok() //! user.logout();
} //! HttpResponse::Ok()
//! }
#[post("/logout")] //! ```
async fn logout(user: Option<Identity>) -> impl Responder { //!
if let Some(user) = user { //! # Advanced configuration
user.logout(); //! By default, `actix-identity` does not automatically log out users. You can change this behaviour
} //! by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
HttpResponse::Ok() //!
} //! In particular, you can automatically log out users who:
``` //! - have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`];
//! - logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
# Advanced configuration //!
By default, `actix-identity` does not automatically log out users. You can change this behaviour //! [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`]. //! [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
In particular, you can automatically log out users who:
- have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]);
- logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
[`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
[`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
*/
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(missing_docs)] #![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![warn(future_incompatible)]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod config; pub mod config;
pub mod error;
mod identity; mod identity;
mod identity_ext; mod identity_ext;
mod middleware; mod middleware;
pub use self::{identity::Identity, identity_ext::IdentityExt, middleware::IdentityMiddleware}; pub use self::identity::Identity;
pub use self::identity_ext::IdentityExt;
pub use self::middleware::IdentityMiddleware;

View File

@ -114,9 +114,6 @@ where
logout_behaviour: configuration.on_logout.clone(), logout_behaviour: configuration.on_logout.clone(),
is_login_deadline_enabled: configuration.login_deadline.is_some(), is_login_deadline_enabled: configuration.login_deadline.is_some(),
is_visit_deadline_enabled: configuration.visit_deadline.is_some(), is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
id_key: configuration.id_key,
last_visit_unix_timestamp_key: configuration.last_visit_unix_timestamp_key,
login_unix_timestamp_key: configuration.login_unix_timestamp_key,
}; };
req.extensions_mut().insert(identity_inner); req.extensions_mut().insert(identity_inner);
enforce_policies(&req, &configuration); enforce_policies(&req, &configuration);

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};
@ -28,33 +28,6 @@ async fn login_works() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[actix_web::test]
async fn custom_keys_work_as_expected() {
let custom_id_key = "custom.user_id";
let custom_last_visited_key = "custom.last_visited_at";
let custom_logged_in_key = "custom.logged_in_at";
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder()
.id_key(custom_id_key)
.last_visit_unix_timestamp_key(custom_last_visited_key)
.login_unix_timestamp_key(custom_logged_in_key),
);
let user_id = user_id();
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
let response = app.get_identity_required().await;
assert!(response.status().is_success());
let response = app.post_logout().await;
assert!(response.status().is_success());
let response = app.get_identity_required().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test] #[actix_web::test]
async fn logging_in_again_replaces_the_current_identity() { async fn logging_in_again_replaces_the_current_identity() {
let app = TestApp::spawn(); let app = TestApp::spawn();

View File

@ -32,8 +32,7 @@ impl TestApp {
.listen(listener) .listen(listener)
.unwrap() .unwrap()
.run(); .run();
let _ = actix_web::rt::spawn(server);
actix_web::rt::spawn(server);
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.cookie_store(true) .cookie_store(true)
@ -136,7 +135,7 @@ async fn increment(session: Session, user: Option<Identity>) -> HttpResponse {
.get::<i32>("counter") .get::<i32>("counter")
.unwrap_or(Some(0)) .unwrap_or(Some(0))
.map_or(1, |inner| inner + 1); .map_or(1, |inner| inner + 1);
session.insert("counter", counter).unwrap(); session.insert("counter", &counter).unwrap();
HttpResponse::Ok().json(&EndpointResponse { HttpResponse::Ok().json(&EndpointResponse {
user_id, user_id,

View File

@ -1,41 +1,29 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
- Update `redis` dependency to `0.29`.
- Update `actix-session` dependency to `0.9`.
## 0.5.1
- No significant changes since `0.5.0`.
## 0.5.0
- Update `redis` dependency to `0.23`.
- Update `actix-session` dependency to `0.8`.
## 0.4.0
## 0.4.0 - 2022-09-10
- Add `Builder::key_by` for setting a custom rate limit key function. - Add `Builder::key_by` for setting a custom rate limit key function.
- Implement `Default` for `RateLimiter`. - Implement `Default` for `RateLimiter`.
- `RateLimiter` is marked `#[non_exhaustive]`; use `RateLimiter::default()` instead. - `RateLimiter` is marked `#[non_exhaustive]`; use `RateLimiter::default()` instead.
- In the middleware errors from the count function are matched and respond with `INTERNAL_SERVER_ERROR` if it's an unexpected error, instead of the default `TOO_MANY_REQUESTS`. - In the middleware errors from the count function are matched and respond with `INTERNAL_SERVER_ERROR` if it's an unexpected error, instead of the default `TOO_MANY_REQUESTS`.
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.3.0
## 0.3.0 - 2022-07-11
- `Limiter::builder` now takes an `impl Into<String>`. - `Limiter::builder` now takes an `impl Into<String>`.
- Removed lifetime from `Builder`. - Removed lifetime from `Builder`.
- Updated `actix-session` dependency to `0.7`. - Updated `actix-session` dependency to `0.7`.
## 0.2.0
## 0.2.0 - 2022-03-22
- Update Actix Web dependency to v4 ecosystem. - Update Actix Web dependency to v4 ecosystem.
- Update Tokio dependencies to v1 ecosystem. - Update Tokio dependencies to v1 ecosystem.
- Rename `Limiter::{build => builder}()`. - Rename `Limiter::{build => builder}()`.
- Rename `Builder::{finish => build}()`. - Rename `Builder::{finish => build}()`.
- Exceeding the rate limit now returns a 429 Too Many Requests response. - Exceeding the rate limit now returns a 429 Too Many Requests response.
## 0.1.4
## 0.1.4 - 2022-03-18
- Adopted into @actix org from <https://github.com/0xmad/actix-limitation>. - Adopted into @actix org from <https://github.com/0xmad/actix-limitation>.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-limitation" name = "actix-limitation"
version = "0.5.1" version = "0.4.0"
authors = [ authors = [
"0xmad <0xmad@users.noreply.github.com>", "0xmad <0xmad@users.noreply.github.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -8,14 +8,9 @@ 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 = "MIT OR Apache-2.0"
edition.workspace = true edition = "2018"
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features] [features]
default = ["session"] default = ["session"]
@ -23,21 +18,18 @@ session = ["actix-session"]
[dependencies] [dependencies]
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies"] } actix-web = { version = "4", features = ["cookies"] }
chrono = "0.4" chrono = "0.4"
derive_more = { version = "2", features = ["display", "error", "from"] } derive_more = "0.99.5"
log = "0.4" log = "0.4"
redis = { version = "0.29", default-features = false, features = ["tokio-comp"] } redis = { version = "0.21", 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.7", 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

@ -3,21 +3,17 @@
> Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web. > Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web.
> Originally based on <https://github.com/fnichol/limitation>. > Originally based on <https://github.com/fnichol/limitation>.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation) [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation)
[![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.5.1)](https://docs.rs/actix-limitation/0.5.1) [![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.4.0)](https://docs.rs/actix-limitation/0.4.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation)
[![Dependency Status](https://deps.rs/crate/actix-limitation/0.5.1/status.svg)](https://deps.rs/crate/actix-limitation/0.5.1) [![Dependency Status](https://deps.rs/crate/actix-limitation/0.4.0/status.svg)](https://deps.rs/crate/actix-limitation/0.4.0)
<!-- prettier-ignore-end -->
## Examples ## Examples
```toml ```toml
[dependencies] [dependencies]
actix-web = "4" actix-web = "4"
actix-limitation = "0.5" actix-limitation = "0.3"
``` ```
```rust ```rust

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,10 +45,10 @@
//! ``` //! ```
#![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))]
use std::{borrow::Cow, fmt, sync::Arc, time::Duration}; use std::{borrow::Cow, fmt, sync::Arc, time::Duration};
@ -60,7 +60,10 @@ mod errors;
mod middleware; mod middleware;
mod status; mod status;
pub use self::{builder::Builder, errors::Error, middleware::RateLimiter, status::Status}; pub use self::builder::Builder;
pub use self::errors::Error;
pub use self::middleware::RateLimiter;
pub use self::status::Status;
/// Default request limit. /// Default request limit.
pub const DEFAULT_REQUEST_LIMIT: usize = 5000; pub const DEFAULT_REQUEST_LIMIT: usize = 5000;
@ -137,7 +140,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

@ -53,7 +53,7 @@ where
forward_ready!(service); forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
// A misconfiguration of the Actix App will result in a **runtime** failure, so the expect // A mis-configuration of the Actix App will result in a **runtime** failure, so the expect
// method description is important context for the developer. // method description is important context for the developer.
let limiter = req let limiter = req
.app_data::<web::Data<Limiter>>() .app_data::<web::Data<Limiter>>()

View File

@ -1,4 +1,4 @@
use std::{ops::Add, time::Duration}; use std::{convert::TryInto, ops::Add, time::Duration};
use chrono::SubsecRound as _; use chrono::SubsecRound as _;
@ -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

@ -21,7 +21,7 @@ async fn test_limiter_count() -> Result<(), Error> {
for i in 0..20 { for i in 0..20 {
let status = limiter.count(id.to_string()).await?; let status = limiter.count(id.to_string()).await?;
println!("status: {status:?}"); println!("status: {:?}", status);
assert_eq!(20 - status.remaining(), i + 1); assert_eq!(20 - status.remaining(), i + 1);
} }

View File

@ -1,48 +1,39 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.11.0
- Updated `prost` dependency to `0.13`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.10.0
- Updated `prost` dependency to `0.12`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.9.0
## 0.9.0 - 2022-08-24
- Added `application/x-protobuf` as an acceptable header. - Added `application/x-protobuf` as an acceptable header.
- Updated `prost` dependency to `0.11`. - Updated `prost` dependency to `0.11`.
## 0.8.0
## 0.8.0 - 2022-06-25
- Update `prost` dependency to `0.10`. - Update `prost` dependency to `0.10`.
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 0.7.0
## 0.7.0 - 2022-03-01
- Update `actix-web` dependency to `4`. - Update `actix-web` dependency to `4`.
## 0.7.0-beta.5
## 0.7.0-beta.5 - 2022-02-03
- Update `prost` dependency to `0.9`. - Update `prost` dependency to `0.9`.
- Update `actix-web` dependency to `4.0.0-rc.1`. - Update `actix-web` dependency to `4.0.0-rc.1`.
## 0.7.0-beta.4
## 0.7.0-beta.4 - 2021-12-29
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
## 0.7.0-beta.3
## 0.7.0-beta.3 - 2021-12-12
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209] - Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#209]: https://github.com/actix/actix-extras/pull/209 [#209]: https://github.com/actix/actix-extras/pull/209
## 0.7.0-beta.2
## 0.7.0-beta.2 - 2021-10-21
- Bump `prost` version to 0.8. [#197] - Bump `prost` version to 0.8. [#197]
- Update `actix-web` dependency to v4.0.0-beta.10. [#203] - Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
@ -50,52 +41,52 @@
[#197]: https://github.com/actix/actix-extras/pull/197 [#197]: https://github.com/actix/actix-extras/pull/197
[#203]: https://github.com/actix/actix-extras/pull/203 [#203]: https://github.com/actix/actix-extras/pull/203
## 0.7.0-beta.1
## 0.7.0-beta.1 - 2021-06-27
- Bump `prost` version to 0.7. [#144] - Bump `prost` version to 0.7. [#144]
- Update `actix-web` dependency to 4.0.0 beta. - Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0. - Minimum supported Rust version (MSRV) is now 1.46.0.
[#144]: https://github.com/actix/actix-extras/pull/144 [#144]: https://github.com/actix/actix-extras/pull/144
## 0.6.0
## 0.6.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0. - Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0 to use `matches!` macro. - Minimum supported Rust version (MSRV) is now 1.42.0 to use `matches!` macro.
## 0.6.0-alpha.1
## 0.6.0-alpha.1 - 2020-07-06
- Update `actix-web` to 3.0.0-alpha.3 - Update `actix-web` to 3.0.0-alpha.3
- Minimum supported Rust version(MSRV) is now 1.40.0. - Minimum supported Rust version(MSRV) is now 1.40.0.
- Minimize `futures` dependency - Minimize `futures` dependency
## 0.5.1 - 2019-02-17
## 0.5.1 - 2019-02-17
- Move repository to actix-extras - Move repository to actix-extras
## 0.5.0 - 2019-01-24
## 0.5.0 - 2019-01-24
- Migrate to actix-web 2.0.0 and std::future - Migrate to actix-web 2.0.0 and std::future
- Update prost to 0.6 - Update prost to 0.6
- Update bytes to 0.5 - Update bytes to 0.5
## 0.4.1 - 2019-10-03
## 0.4.1 - 2019-10-03
- Upgrade prost and prost-derive to 0.5.0 - Upgrade prost and prost-derive to 0.5.0
## 0.4.0 - 2019-05-18
## 0.4.0 - 2019-05-18
- Upgrade to actix-web 1.0.0-rc - Upgrade to actix-web 1.0.0-rc
- Removed `protobuf` method for `HttpRequest` (use `ProtoBuf` extractor instead) - Removed `protobuf` method for `HttpRequest` (use `ProtoBuf` extractor instead)
## 0.3.0 - 2019-03-07
## 0.3.0 - 2019-03-07
- Upgrade to actix-web 0.7.18 - Upgrade to actix-web 0.7.18
## 0.2.0 - 2018-04-10
## 0.2.0 - 2018-04-10
- Provide protobuf extractor - Provide protobuf extractor
## 0.1.0 - 2018-03-21
## 0.1.0 - 2018-03-21
- First release - First release

View File

@ -1,31 +1,27 @@
[package] [package]
name = "actix-protobuf" name = "actix-protobuf"
version = "0.11.0" version = "0.9.0"
edition = "2018"
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 = "MIT OR Apache-2.0"
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs] [lib]
rustdoc-args = ["--cfg", "docsrs"] name = "actix_protobuf"
all-features = true path = "src/lib.rs"
[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.5"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false }
prost = { version = "0.13", default-features = false } prost = { version = "0.11", 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.11", default_features = false, features = ["prost-derive"] }
[lints]
workspace = true

View File

@ -2,14 +2,10 @@
> Protobuf payload extractor for Actix Web. > Protobuf payload extractor for Actix Web.
<!-- 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.9.0)](https://docs.rs/actix-protobuf/0.9.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.9.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.9.0)
<!-- prettier-ignore-end -->
## Documentation & Resources ## Documentation & Resources

View File

@ -1,9 +1,8 @@
//! Protobuf payload extractor for Actix Web. //! Protobuf payload extractor for Actix Web.
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![deny(rust_2018_idioms, nonstandard_style)]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![warn(future_incompatible)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{ use std::{
fmt, fmt,
@ -22,7 +21,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 +31,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 {
@ -269,14 +266,14 @@ impl ProtoBufResponseBuilder for HttpResponseBuilder {
value value
.encode(&mut body) .encode(&mut body)
.map_err(ProtoBufPayloadError::Serialize)?; .map_err(ProtoBufPayloadError::Serialize)?;
Ok(self.body(body)) Ok(self.body(body))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::{http::header, test::TestRequest}; use actix_web::http::header;
use actix_web::test::TestRequest;
use super::*; use super::*;

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

@ -0,0 +1,153 @@
# Changes
## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.12.0 - 2022-07-09
- 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 - 2022-03-15
### 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 - 2022-03-01
- Update `actix-web` dependency to `4`.
## 0.10.0-beta.6 - 2022-02-07
- Update `actix-web` dependency to `4.0.0-rc.1`.
## 0.10.0-beta.5 - 2021-12-29
- 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 - 2021-12-12
- 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 - 2021-10-21
- 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 - 2021-06-27
- No notable changes.
## 0.10.0-beta.1 - 2021-04-02
- Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0.
## 0.9.2 - 2021-03-21
- 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 - 2020-09-12
- Enforce minimum redis-async version of 0.6.3 to workaround breaking patch change.
## 0.9.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0.
- Minimize `futures` dependency.
## 0.9.0-alpha.2 - 2020-05-17
- 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 - 2020-03-28
- 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 - 2020-02-18
- 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

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

@ -0,0 +1,47 @@
[package]
name = "actix-redis"
version = "0.12.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actor-based Redis client"
license = "MIT OR Apache-2.0"
keywords = ["actix", "redis", "async"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-extras.git"
categories = ["network-programming", "asynchronous"]
edition = "2018"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[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.5"
futures-core = { version = "0.3.7", default-features = false }
redis-async = "0.13"
time = "0.3"
tokio = { version = "1.13.1", 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.9"
serde = { version = "1.0.101", features = ["derive"] }

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

@ -0,0 +1,14 @@
# actix-redis
> Actor-based Redis client.
[![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.12.0)](https://docs.rs/actix-redis/0.12.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-redis)
[![Dependency Status](https://deps.rs/crate/actix-redis/0.12.0/status.svg)](https://deps.rs/crate/actix-redis/0.12.0)
## 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

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

@ -0,0 +1,29 @@
//! Redis integration for `actix`.
#![forbid(unsafe_code)]
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
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

@ -1,51 +1,16 @@
# Changes # Changes
## Unreleased ## Unreleased - 2021-xx-xx
- 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
- Remove use of `async-trait` on `SessionStore` trait.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.8.0
- Set secure attribute when adding a session removal cookie.
- Update `redis` dependency to `0.23`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.7.2
- Set SameSite attribute when adding a session removal cookie. [#284]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
[#284]: https://github.com/actix/actix-extras/pull/284
## 0.7.1
## 0.7.1 - 2022-07-24
- Fix interaction between session state changes and renewal. [#265] - Fix interaction between session state changes and renewal. [#265]
[#265]: https://github.com/actix/actix-extras/pull/265 [#265]: https://github.com/actix/actix-extras/pull/265
## 0.7.0
## 0.7.0 - 2022-07-09
- Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233] - Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233]
- `SessionLength` is now called `SessionLifecycle`. [#233] - `SessionLength` is now called `SessionLifecycle`. [#233]
- `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233] - `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233]
@ -61,8 +26,8 @@
[#233]: https://github.com/actix/actix-extras/pull/233 [#233]: https://github.com/actix/actix-extras/pull/233
## 0.6.2
## 0.6.2 - 2022-03-25
- Implement `SessionExt` for `GuardContext`. [#234] - Implement `SessionExt` for `GuardContext`. [#234]
- `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235] - `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235]
- Do not leak internal implementation details to callers when errors occur. [#236] - Do not leak internal implementation details to callers when errors occur. [#236]
@ -71,14 +36,13 @@
[#236]: https://github.com/actix/actix-extras/pull/236 [#236]: https://github.com/actix/actix-extras/pull/236
[#235]: https://github.com/actix/actix-extras/pull/235 [#235]: https://github.com/actix/actix-extras/pull/235
## 0.6.1
## 0.6.1 - 2022-03-21
- No significant changes since `0.6.0`. - No significant changes since `0.6.0`.
## 0.6.0
## 0.6.0 - 2022-03-15
### Added ### Added
- `SessionMiddleware`, a middleware to provide support for saving/updating/deleting session state against a pluggable storage backend (see `SessionStore` trait). [#212] - `SessionMiddleware`, a middleware to provide support for saving/updating/deleting session state against a pluggable storage backend (see `SessionStore` trait). [#212]
- `CookieSessionStore`, a cookie-based backend to store session state. [#212] - `CookieSessionStore`, a cookie-based backend to store session state. [#212]
- `RedisActorSessionStore`, a Redis-based backend to store session state powered by `actix-redis`. [#212] - `RedisActorSessionStore`, a Redis-based backend to store session state powered by `actix-redis`. [#212]
@ -87,39 +51,37 @@
- Implement `SessionExt` for `ServiceResponse`. [#212] - Implement `SessionExt` for `ServiceResponse`. [#212]
### Changed ### Changed
- Rename `UserSession` to `SessionExt`. [#212] - Rename `UserSession` to `SessionExt`. [#212]
### Removed ### Removed
- `CookieSession`; replaced with `CookieSessionStore`, a storage backend for `SessionMiddleware`. [#212] - `CookieSession`; replaced with `CookieSessionStore`, a storage backend for `SessionMiddleware`. [#212]
- `Session::set_session`; use `Session::insert` to modify the session state. [#212] - `Session::set_session`; use `Session::insert` to modify the session state. [#212]
[#212]: https://github.com/actix/actix-extras/pull/212 [#212]: https://github.com/actix/actix-extras/pull/212
## 0.5.0
## 0.5.0 - 2022-03-01
- Update `actix-web` dependency to `4`. - Update `actix-web` dependency to `4`.
## 0.5.0-beta.8
## 0.5.0-beta.8 - 2022-02-07
- Update `actix-web` dependency to `4.0.0-rc.1`. - Update `actix-web` dependency to `4.0.0-rc.1`.
## 0.5.0-beta.7
## 0.5.0-beta.7 - 2021-12-29
- Update `actix-web` dependency to `4.0.0.beta-18`. [#218] - Update `actix-web` dependency to `4.0.0.beta-18`. [#218]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
[#218]: https://github.com/actix/actix-extras/pull/218 [#218]: https://github.com/actix/actix-extras/pull/218
## 0.5.0-beta.6
## 0.5.0-beta.6 - 2021-12-18
- Update `actix-web` dependency to `4.0.0.beta-15`. [#216] - Update `actix-web` dependency to `4.0.0.beta-15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216 [#216]: https://github.com/actix/actix-extras/pull/216
## 0.5.0-beta.5
## 0.5.0-beta.5 - 2021-12-12
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209] - Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
- Remove `UserSession` implementation for `RequestHead`. [#209] - Remove `UserSession` implementation for `RequestHead`. [#209]
- A session will be created in the storage backend if and only if there is some data inside the session state. This reduces the performance impact of `SessionMiddleware` on routes that do not leverage sessions. [#207] - A session will be created in the storage backend if and only if there is some data inside the session state. This reduces the performance impact of `SessionMiddleware` on routes that do not leverage sessions. [#207]
@ -127,12 +89,12 @@
[#207]: https://github.com/actix/actix-extras/pull/207 [#207]: https://github.com/actix/actix-extras/pull/207
[#209]: https://github.com/actix/actix-extras/pull/209 [#209]: https://github.com/actix/actix-extras/pull/209
## 0.5.0-beta.4
## 0.5.0-beta.4 - 2021-11-22
- No significant changes since `0.5.0-beta.3`. - No significant changes since `0.5.0-beta.3`.
## 0.5.0-beta.3
## 0.5.0-beta.3 - 2021-10-21
- Impl `Clone` for `CookieSession`. [#201] - Impl `Clone` for `CookieSession`. [#201]
- Update `actix-web` dependency to v4.0.0-beta.10. [#203] - Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
@ -140,12 +102,12 @@
[#201]: https://github.com/actix/actix-extras/pull/201 [#201]: https://github.com/actix/actix-extras/pull/201
[#203]: https://github.com/actix/actix-extras/pull/203 [#203]: https://github.com/actix/actix-extras/pull/203
## 0.5.0-beta.2
## 0.5.0-beta.2 - 2021-06-27
- No notable changes. - No notable changes.
## 0.5.0-beta.1
## 0.5.0-beta.1 - 2021-04-02
- Add `Session::entries`. [#170] - Add `Session::entries`. [#170]
- Rename `Session::{set => insert}` to match standard hash map naming. [#170] - Rename `Session::{set => insert}` to match standard hash map naming. [#170]
- Return values from `Session::remove`. [#170] - Return values from `Session::remove`. [#170]
@ -157,21 +119,21 @@
[#170]: https://github.com/actix/actix-extras/pull/170 [#170]: https://github.com/actix/actix-extras/pull/170
## 0.4.1
## 0.4.1 - 2021-03-21
- `Session::set_session` takes a `IntoIterator` instead of `Iterator`. [#105] - `Session::set_session` takes a `IntoIterator` instead of `Iterator`. [#105]
- Fix calls to `session.purge()` from paths other than the one specified in the cookie. [#129] - Fix calls to `session.purge()` from paths other than the one specified in the cookie. [#129]
[#105]: https://github.com/actix/actix-extras/pull/105 [#105]: https://github.com/actix/actix-extras/pull/105
[#129]: https://github.com/actix/actix-extras/pull/129 [#129]: https://github.com/actix/actix-extras/pull/129
## 0.4.0
## 0.4.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0. - Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0. - Minimum supported Rust version (MSRV) is now 1.42.0.
## 0.4.0-alpha.1
## 0.4.0-alpha.1 - 2020-03-14
- Update the `time` dependency to 0.2.7 - Update the `time` dependency to 0.2.7
- Update the `actix-web` dependency to 3.0.0-alpha.1 - Update the `actix-web` dependency to 3.0.0-alpha.1
- Long lasting auto-prolonged session [#1292] - Long lasting auto-prolonged session [#1292]
@ -179,62 +141,65 @@
[#1292]: https://github.com/actix/actix-web/pull/1292 [#1292]: https://github.com/actix/actix-web/pull/1292
## 0.3.0 - 2019-12-20
## 0.3.0 - 2019-12-20
- Release - Release
## 0.3.0-alpha.4 - 2019-12-xx
## 0.3.0-alpha.4 - 2019-12-xx
- Allow access to sessions also from not mutable references to the request - Allow access to sessions also from not mutable references to the request
## 0.3.0-alpha.3 - 2019-12-xx
## 0.3.0-alpha.3 - 2019-12-xx
- Add access to the session from RequestHead for use of session from guard methods - Add access to the session from RequestHead for use of session from guard methods
- Migrate to `std::future` - Migrate to `std::future`
- Migrate to `actix-web` 2.0 - Migrate to `actix-web` 2.0
## 0.2.0 - 2019-07-08
- Enhanced `actix-session` to facilitate state changes. Use `Session.renew()` at successful login to cycle a session (new key/cookie but keeps state). Use `Session.purge()` at logout to invalid a session cookie (and remove from redis cache, if applicable). ## 0.2.0 - 2019-07-08
- Enhanced ``actix-session`` to facilitate state changes. Use ``Session.renew()``
at successful login to cycle a session (new key/cookie but keeps state).
Use ``Session.purge()`` at logout to invalid a session cookie (and remove
from redis cache, if applicable).
## 0.1.1 - 2019-06-03 ## 0.1.1 - 2019-06-03
- Fix optional cookie session support - Fix optional cookie session support
## 0.1.0 - 2019-05-18
## 0.1.0 - 2019-05-18
- Use actix-web 1.0.0-rc - Use actix-web 1.0.0-rc
## 0.1.0-beta.4 - 2019-05-12
## 0.1.0-beta.4 - 2019-05-12
- Use actix-web 1.0.0-beta.4 - Use actix-web 1.0.0-beta.4
## 0.1.0-beta.2 - 2019-04-28
## 0.1.0-beta.2 - 2019-04-28
- Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest - Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest
## 0.1.0-beta.1 - 2019-04-20
## 0.1.0-beta.1 - 2019-04-20
- Update actix-web to beta.1 - Update actix-web to beta.1
- `CookieSession::max_age()` accepts value in seconds - `CookieSession::max_age()` accepts value in seconds
## 0.1.0-alpha.6 - 2019-04-14
## 0.1.0-alpha.6 - 2019-04-14
- Update actix-web alpha.6 - Update actix-web alpha.6
## 0.1.0-alpha.4 - 2019-04-08
## 0.1.0-alpha.4 - 2019-04-08
- Update actix-web - Update actix-web
## 0.1.0-alpha.3 - 2019-04-02 ## 0.1.0-alpha.3 - 2019-04-02
- Update actix-web - Update actix-web
## 0.1.0-alpha.2 - 2019-03-29
## 0.1.0-alpha.2 - 2019-03-29
- Update actix-web - Update actix-web
- Use new feature name for secure cookies - Use new feature name for secure cookies
## 0.1.0-alpha.1 - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl - Initial impl

View File

@ -1,60 +1,64 @@
[package] [package]
name = "actix-session" name = "actix-session"
version = "0.10.1" 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 = "Session management for Actix Web" description = "Session management for Actix We"
keywords = ["http", "web", "framework", "async", "session"] keywords = ["http", "web", "framework", "async", "session"]
repository.workspace = true homepage = "https://actix.rs"
homepage.workspace = true repository = "https://github.com/actix/actix-extras.git"
license.workspace = true license = "MIT OR Apache-2.0"
edition.workspace = true edition = "2018"
rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lib]
name = "actix_session"
path = "src/lib.rs"
[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"
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"] }
anyhow = "1" anyhow = "1"
derive_more = { version = "2", features = ["display", "error", "from"] } async-trait = "0.1"
rand = "0.9" derive_more = "0.99.5"
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.7", default-features = false, optional = true }
# redis-rs-session
redis = { version = "0.21", default-features = false, features = ["aio", "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.9"
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

@ -2,124 +2,13 @@
> Session management for Actix Web. > Session management for Actix Web.
<!-- 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.7.1)](https://docs.rs/actix-session/0.7.1)
![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.7.1/status.svg)](https://deps.rs/crate/actix-session/0.7.1)
<!-- prettier-ignore-end --> ## Documentation & Resources
<!-- cargo-rdme start --> - [API Documentation](https://docs.rs/actix-session)
- [Example Projects](https://github.com/actix/examples/tree/master/auth/cookie-session)
Session management for Actix Web. - Minimum Supported Rust Version (MSRV): 1.57
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 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 every single request**. That is, for example, how 'Basic' Authentication works. While it may work for a machine user (i.e. an API client), it is impractical for a person—you 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 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 **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 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.
Further reading on sessions:
- [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`] as a middleware on your `App`:
```rust
use actix_web::{web, App, HttpServer, HttpResponse, Error};
use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
use actix_web::cookie::Key;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// When using `Key::generate()` it is important to initialize outside of the
// `HttpServer::new` closure. When deployed the secret key should be read from a
// configuration file or environment variables.
let secret_key = Key::generate();
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap();
HttpServer::new(move ||
App::new()
// Add session management to your application using Redis for session state storage
.wrap(
SessionMiddleware::new(
redis_store.clone(),
secret_key.clone(),
)
)
.default_service(web::to(|| HttpResponse::Ok())))
.bind(("127.0.0.1", 8080))?
.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.
```rust
use actix_web::Error;
use actix_session::Session;
fn index(session: Session) -> Result<&'static str, Error> {
// access the session state
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {}", count);
// modify the session state
session.insert("counter", count + 1)?;
} else {
session.insert("counter", 1)?;
}
Ok("Welcome!")
}
```
## 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:
- a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature flag.
```console
cargo add actix-session --features=cookie-session
```
- a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the `redis-session` feature flag.
```console
cargo add actix-session --features=redis-session
```
Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure connection (via the `native-tls` crate):
```console
cargo add actix-session --features=redis-session-native-tls
```
If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
```console
cargo add actix-session --features=redis-session-rustls
```
You can implement your own session storage backend using the [`SessionStore`] trait.
[`SessionStore`]: storage::SessionStore
[`CookieSessionStore`]: storage::CookieSessionStore
[`RedisSessionStore`]: storage::RedisSessionStore
<!-- 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 {
@ -23,7 +21,7 @@ struct User {
impl User { impl User {
fn authenticate(credentials: Credentials) -> Result<Self, HttpResponse> { fn authenticate(credentials: Credentials) -> Result<Self, HttpResponse> {
// to do: figure out why I keep getting hacked /s // TODO: figure out why I keep getting hacked
if &credentials.password != "hunter2" { if &credentials.password != "hunter2" {
return Err(HttpResponse::Unauthorized().json("Unauthorized")); return Err(HttpResponse::Unauthorized().json("Unauthorized"));
} }
@ -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,7 +84,10 @@ 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(
RedisActorSessionStore::new("127.0.0.1:6379"),
signing_key.clone(),
)
// allow the cookie to be accessed from javascript // allow the cookie to be accessed from javascript
.cookie_http_only(false) .cookie_http_only(false)
// allow the cookie only from the current domain // allow the cookie only from the current domain

View File

@ -1,15 +1,13 @@
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> {
println!("{req:?}"); println!("{:?}", req);
// session // session
if let Some(count) = session.get::<i32>("counter")? { if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {count}"); println!("SESSION value: {}", count);
session.insert("counter", count + 1)?; session.insert("counter", count + 1)?;
} else { } else {
session.insert("counter", 1)?; session.insert("counter", 1)?;
@ -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};
@ -131,7 +131,6 @@ impl PersistentSession {
/// ///
/// A persistent session can live more than the specified TTL if the TTL is extended. /// A persistent session can live more than the specified TTL if the TTL is extended.
/// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details. /// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
#[doc(alias = "max_age", alias = "max age", alias = "expires")]
pub fn session_ttl(mut self, session_ttl: Duration) -> Self { pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
self.session_ttl = session_ttl; self.session_ttl = session_ttl;
self self

View File

@ -27,11 +27,11 @@
//! against the active [`Session`]. //! against the active [`Session`].
//! //!
//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`], //! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
//! [`RedisSessionStore`]) - you can create a custom storage backend by implementing the //! [`RedisSessionStore`], and [`RedisActorSessionStore`]) - you can create a custom storage backend
//! [`SessionStore`] trait. //! by implementing the [`SessionStore`] trait.
//! //!
//! Further reading on sessions: //! Further reading on sessions:
//! - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265); //! - [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265);
//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html). //! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
//! //!
//! # Getting started //! # Getting started
@ -40,27 +40,21 @@
//! //!
//! ```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;
//! //!
//! #[actix_web::main] //! #[actix_web::main]
//! async fn main() -> std::io::Result<()> { //! async fn main() -> std::io::Result<()> {
//! // When using `Key::generate()` it is important to initialize outside of the //! // The secret key would usually be read from a configuration file/environment variables.
//! // `HttpServer::new` closure. When deployed the secret key should be read from a
//! // configuration file or environment variables.
//! let secret_key = Key::generate(); //! let secret_key = Key::generate();
//! //! let redis_connection_string = "127.0.0.1:6379";
//! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
//! .await
//! .unwrap();
//!
//! 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 for session state storage
//! .wrap( //! .wrap(
//! SessionMiddleware::new( //! SessionMiddleware::new(
//! redis_store.clone(), //! RedisActorSessionStore::new(redis_connection_string),
//! secret_key.clone(), //! secret_key.clone()
//! ) //! )
//! ) //! )
//! .default_service(web::to(|| HttpResponse::Ok()))) //! .default_service(web::to(|| HttpResponse::Ok())))
@ -71,7 +65,7 @@
//! ``` //! ```
//! //!
//! The session state can be accessed and modified by your request handlers using the [`Session`] //! 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. //! extractor.
//! //!
//! ```no_run //! ```no_run
//! use actix_web::Error; //! use actix_web::Error;
@ -99,28 +93,37 @@
//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature //! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
//! flag. //! 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 //! - a Redis-based backend via [`actix-redis`](https://docs.rs/acitx-redis),
//! `redis-session` feature flag. //! [`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 //! - a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using
//! connection (via the `native-tls` crate): //! 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.
@ -128,12 +131,14 @@
//! [`SessionStore`]: storage::SessionStore //! [`SessionStore`]: storage::SessionStore
//! [`CookieSessionStore`]: storage::CookieSessionStore //! [`CookieSessionStore`]: storage::CookieSessionStore
//! [`RedisSessionStore`]: storage::RedisSessionStore //! [`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_cfg))]
pub mod config; pub mod config;
mod middleware; mod middleware;
@ -141,14 +146,11 @@ mod session;
mod session_ext; mod session_ext;
pub mod storage; pub mod storage;
pub use self::{ pub use self::middleware::SessionMiddleware;
middleware::SessionMiddleware, pub use self::session::{Session, SessionGetError, SessionInsertError, SessionStatus};
session::{Session, SessionGetError, SessionInsertError, SessionStatus}, pub use self::session_ext::SessionExt;
session_ext::SessionExt,
};
#[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;
@ -175,7 +177,7 @@ pub mod test_helpers {
CookieContentSecurity::Signed, CookieContentSecurity::Signed,
CookieContentSecurity::Private, CookieContentSecurity::Private,
] { ] {
println!("Using {policy:?} as cookie content security policy."); println!("Using {:?} as cookie content security policy.", policy);
acceptance_tests::basic_workflow(store_builder.clone(), *policy).await; acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy) acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
.await; .await;
@ -205,11 +207,9 @@ pub mod test_helpers {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use crate::config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy};
use crate::{ use crate::{
config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy}, storage::SessionStore, test_helpers::key, Session, SessionExt, SessionMiddleware,
storage::SessionStore,
test_helpers::key,
Session, SessionExt, SessionMiddleware,
}; };
pub(super) async fn basic_workflow<F, Store>( pub(super) async fn basic_workflow<F, Store>(
@ -239,7 +239,7 @@ pub mod test_helpers {
})) }))
.service(web::resource("/test/").to(|ses: Session| async move { .service(web::resource("/test/").to(|ses: Session| async move {
let val: usize = ses.get("counter").unwrap().unwrap(); let val: usize = ses.get("counter").unwrap().unwrap();
format!("counter: {val}") format!("counter: {}", val)
})), })),
) )
.await; .await;
@ -670,7 +670,7 @@ pub mod test_helpers {
.get::<i32>("counter") .get::<i32>("counter")
.unwrap_or(Some(0)) .unwrap_or(Some(0))
.map_or(1, |inner| inner + 1); .map_or(1, |inner| inner + 1);
session.insert("counter", counter)?; session.insert("counter", &counter)?;
Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter })) Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
} }
@ -706,9 +706,9 @@ pub mod test_helpers {
async fn logout(session: Session) -> Result<HttpResponse> { async fn logout(session: Session) -> Result<HttpResponse> {
let id: Option<String> = session.get("user_id")?; let id: Option<String> = session.get("user_id")?;
let body = if let Some(id) = id { let body = if let Some(x) = id {
session.purge(); session.purge();
format!("Logged out: {id}") format!("Logged out: {}", x)
} else { } else {
"Could not log out anonymous user".to_owned() "Could not log out anonymous user".to_owned()
}; };
@ -722,7 +722,10 @@ pub mod test_helpers {
impl ServiceResponseExt for ServiceResponse { impl ServiceResponseExt for ServiceResponse {
fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> { fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> {
self.response().cookies().find(|c| c.name() == cookie_name) self.response()
.cookies()
.into_iter()
.find(|c| c.name() == cookie_name)
} }
} }
} }

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, fmt, future::Future, pin::Pin, rc::Rc}; use std::{collections::HashMap, convert::TryInto, fmt, future::Future, pin::Pin, rc::Rc};
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use actix_web::{ use actix_web::{
@ -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,17 +59,17 @@ 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 as storage /// // Add session management to your application using Redis for session state storage
/// .wrap(SessionMiddleware::new( /// .wrap(
/// storage.clone(), /// SessionMiddleware::new(
/// secret_key.clone(), /// RedisActorSessionStore::new(redis_connection_string),
/// )) /// 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,20 +92,22 @@ 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(
/// RedisActorSessionStore::new(redis_connection_string),
/// secret_key.clone()
/// )
/// .session_lifecycle( /// .session_lifecycle(
/// PersistentSession::default().session_ttl(time::Duration::days(5)), /// PersistentSession::default()
/// .session_ttl(time::Duration::days(5))
/// ) /// )
/// .build(), /// .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
@ -442,9 +444,7 @@ fn delete_session_cookie(
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let removal_cookie = Cookie::build(config.name.clone(), "") let removal_cookie = Cookie::build(config.name.clone(), "")
.path(config.path.clone()) .path(config.path.clone())
.secure(config.secure) .http_only(config.http_only);
.http_only(config.http_only)
.same_site(config.same_site);
let mut removal_cookie = if let Some(ref domain) = config.domain { let mut removal_cookie = if let Some(ref domain) = config.domain {
removal_cookie.domain(domain) removal_cookie.domain(domain)

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);
@ -49,7 +46,7 @@ use serde::{de::DeserializeOwned, Serialize};
pub struct Session(Rc<RefCell<SessionInner>>); pub struct Session(Rc<RefCell<SessionInner>>);
/// Status of a [`Session`]. /// Status of a [`Session`].
#[derive(Debug, Clone, Default, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionStatus { pub enum SessionStatus {
/// Session state has been updated - the changes will have to be persisted to the backend. /// Session state has been updated - the changes will have to be persisted to the backend.
Changed, Changed,
@ -67,10 +64,15 @@ pub enum SessionStatus {
Renewed, Renewed,
/// The session state has not been modified since its creation/retrieval. /// The session state has not been modified since its creation/retrieval.
#[default]
Unchanged, Unchanged,
} }
impl Default for SessionStatus {
fn default() -> SessionStatus {
SessionStatus::Unchanged
}
}
#[derive(Default)] #[derive(Default)]
struct SessionInner { struct SessionInner {
state: HashMap<String, String>, state: HashMap<String, String>,
@ -100,11 +102,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 +119,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 +137,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 +150,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.
@ -299,12 +218,11 @@ impl Session {
/// ///
/// Values that match keys already existing on the session will be overwritten. Values should /// Values that match keys already existing on the session will be overwritten. Values should
/// already be JSON serialized. /// already be JSON serialized.
#[allow(clippy::needless_pass_by_ref_mut)]
pub(crate) fn set_session( pub(crate) fn set_session(
req: &mut ServiceRequest, req: &mut ServiceRequest,
data: impl IntoIterator<Item = (String, String)>, data: impl IntoIterator<Item = (String, String)>,
) { ) {
let session = Session::get_session(&mut req.extensions_mut()); let session = Session::get_session(&mut *req.extensions_mut());
let mut inner = session.0.borrow_mut(); let mut inner = session.0.borrow_mut();
inner.state.extend(data); inner.state.extend(data);
} }
@ -314,7 +232,6 @@ impl Session {
/// This is a destructive operation - the session state is removed from the request extensions /// This is a destructive operation - the session state is removed from the request extensions
/// typemap, leaving behind a new empty map. It should only be used when the session is being /// typemap, leaving behind a new empty map. It should only be used when the session is being
/// finalised (i.e. in `SessionMiddleware`). /// finalised (i.e. in `SessionMiddleware`).
#[allow(clippy::needless_pass_by_ref_mut)]
pub(crate) fn get_changes<B>( pub(crate) fn get_changes<B>(
res: &mut ServiceResponse<B>, res: &mut ServiceResponse<B>,
) -> (SessionStatus, HashMap<String, String>) { ) -> (SessionStatus, HashMap<String, String>) {
@ -368,13 +285,13 @@ impl FromRequest for Session {
#[inline] #[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(Ok(Session::get_session(&mut req.extensions_mut()))) ready(Ok(Session::get_session(&mut *req.extensions_mut())))
} }
} }
/// 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 +308,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 +322,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

@ -15,13 +15,13 @@ pub trait SessionExt {
impl SessionExt for HttpRequest { impl SessionExt for HttpRequest {
fn get_session(&self) -> Session { fn get_session(&self) -> Session {
Session::get_session(&mut self.extensions_mut()) Session::get_session(&mut *self.extensions_mut())
} }
} }
impl SessionExt for ServiceRequest { impl SessionExt for ServiceRequest {
fn get_session(&self) -> Session { fn get_session(&self) -> Session {
Session::get_session(&mut self.extensions_mut()) Session::get_session(&mut *self.extensions_mut())
} }
} }
@ -31,8 +31,8 @@ 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,3 +1,5 @@
use std::convert::TryInto;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::Error; use anyhow::Error;
@ -45,10 +47,12 @@ use crate::storage::{
/// storage backend. /// storage backend.
/// ///
/// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private /// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private
#[cfg_attr(docsrs, doc(cfg(feature = "cookie-session")))]
#[derive(Default)] #[derive(Default)]
#[non_exhaustive] #[non_exhaustive]
pub struct CookieSessionStore; pub struct CookieSessionStore;
#[async_trait::async_trait(?Send)]
impl SessionStore for CookieSessionStore { impl SessionStore for CookieSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> { async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
serde_json::from_str(session_key.as_ref()) serde_json::from_str(session_key.as_ref())
@ -66,10 +70,10 @@ impl SessionStore for CookieSessionStore {
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
.map_err(SaveError::Serialization)?; .map_err(SaveError::Serialization)?;
session_key Ok(session_key
.try_into() .try_into()
.map_err(Into::into) .map_err(Into::into)
.map_err(SaveError::Other) .map_err(SaveError::Other)?)
} }
async fn update( async fn update(

View File

@ -1,7 +1,7 @@
use std::{collections::HashMap, future::Future}; use std::collections::HashMap;
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;
@ -10,39 +10,41 @@ pub(crate) type SessionState = HashMap<String, String>;
/// The interface to retrieve and save the current session data from/to the chosen storage backend. /// The interface to retrieve and save the current session data from/to the chosen storage backend.
/// ///
/// You can provide your own custom session store backend by implementing this trait. /// You can provide your own custom session store backend by implementing this trait.
///
/// [`async-trait`](https://docs.rs/async-trait) is used for this trait's definition. Therefore, it
/// is required for implementations, too. In particular, we use the send-optional variant:
/// `#[async_trait(?Send)]`.
#[async_trait::async_trait(?Send)]
pub trait SessionStore { pub trait SessionStore {
/// Loads the session state associated to a session key. /// Loads the session state associated to a session key.
fn load( async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError>;
&self,
session_key: &SessionKey,
) -> impl Future<Output = Result<Option<SessionState>, LoadError>>;
/// Persist the session state for a newly created session. /// Persist the session state for a newly created session.
/// ///
/// Returns the corresponding session key. /// Returns the corresponding session key.
fn save( async fn save(
&self, &self,
session_state: SessionState, session_state: SessionState,
ttl: &Duration, ttl: &Duration,
) -> impl Future<Output = Result<SessionKey, SaveError>>; ) -> Result<SessionKey, SaveError>;
/// Updates the session state associated to a pre-existing session key. /// Updates the session state associated to a pre-existing session key.
fn update( async fn update(
&self, &self,
session_key: SessionKey, session_key: SessionKey,
session_state: SessionState, session_state: SessionState,
ttl: &Duration, ttl: &Duration,
) -> impl Future<Output = Result<SessionKey, UpdateError>>; ) -> Result<SessionKey, UpdateError>;
/// Updates the TTL of the session state associated to a pre-existing session key. /// Updates the TTL of the session state associated to a pre-existing session key.
fn update_ttl( async fn update_ttl(
&self, &self,
session_key: &SessionKey, session_key: &SessionKey,
ttl: &Duration, ttl: &Duration,
) -> impl Future<Output = Result<(), anyhow::Error>>; ) -> Result<(), anyhow::Error>;
/// Deletes a session from the store. /// Deletes a session from the store.
fn delete(&self, session_key: &SessionKey) -> impl Future<Output = Result<(), anyhow::Error>>; async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error>;
} }
// We cannot derive the `Error` implementation using `derive_more` for our custom errors: // We cannot derive the `Error` implementation using `derive_more` for our custom errors:
@ -53,11 +55,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 +76,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 +97,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,26 @@
//! Pluggable storage backends for session state. //! Pluggable storage backends for session state.
mod interface;
mod session_key;
pub use self::interface::{LoadError, SaveError, SessionStore, UpdateError};
pub use self::session_key::SessionKey;
#[cfg(feature = "cookie-session")] #[cfg(feature = "cookie-session")]
mod cookie; mod cookie;
mod interface;
#[cfg(feature = "redis-session")] #[cfg(feature = "redis-actor-session")]
mod redis_actor;
#[cfg(feature = "redis-rs-session")]
mod redis_rs; mod redis_rs;
mod session_key;
#[cfg(any(feature = "redis-actor-session", feature = "redis-rs-session"))]
mod utils; mod utils;
#[cfg(feature = "cookie-session")] #[cfg(feature = "cookie-session")]
pub use self::cookie::CookieSessionStore; pub use cookie::CookieSessionStore;
#[cfg(feature = "redis-session")] #[cfg(feature = "redis-actor-session")]
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder}; pub use redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
pub use self::{ #[cfg(feature = "redis-rs-session")]
interface::{LoadError, SaveError, SessionStore, UpdateError}, pub use redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
session_key::SessionKey,
utils::generate_session_key,
};

View File

@ -53,6 +53,7 @@ use crate::storage::{
/// Redis. Use [`RedisSessionStore`] if you need TLS support. /// Redis. Use [`RedisSessionStore`] if you need TLS support.
/// ///
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore /// [`RedisSessionStore`]: crate::storage::RedisSessionStore
#[cfg_attr(docsrs, doc(cfg(feature = "redis-actor-session")))]
pub struct RedisActorSessionStore { pub struct RedisActorSessionStore {
configuration: CacheConfiguration, configuration: CacheConfiguration,
addr: Addr<RedisActor>, addr: Addr<RedisActor>,
@ -92,6 +93,7 @@ impl Default for CacheConfiguration {
/// A fluent builder to construct a [`RedisActorSessionStore`] instance with custom configuration /// A fluent builder to construct a [`RedisActorSessionStore`] instance with custom configuration
/// parameters. /// parameters.
#[cfg_attr(docsrs, doc(cfg(feature = "redis-actor-session")))]
#[must_use] #[must_use]
pub struct RedisActorSessionStoreBuilder { pub struct RedisActorSessionStoreBuilder {
connection_string: String, connection_string: String,
@ -118,6 +120,7 @@ impl RedisActorSessionStoreBuilder {
} }
} }
#[async_trait::async_trait(?Send)]
impl SessionStore for RedisActorSessionStore { impl SessionStore for RedisActorSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> { async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
@ -277,6 +280,8 @@ impl SessionStore for RedisActorSessionStore {
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use actix_web::cookie::time::Duration;
use super::*; use super::*;
use crate::test_helpers::acceptance_test_suite; use crate::test_helpers::acceptance_test_suite;

View File

@ -1,8 +1,8 @@
use std::sync::Arc; use std::{convert::TryInto, sync::Arc};
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use anyhow::Error; use anyhow::{Context, 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,15 @@ 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
#[cfg_attr(docsrs, doc(cfg(feature = "redis-rs-session")))]
#[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 +81,35 @@ 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
#[cfg_attr(docsrs, doc(cfg(feature = "redis-rs-session")))]
#[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 +122,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,
@ -198,6 +134,7 @@ impl RedisSessionStoreBuilder {
} }
} }
#[async_trait::async_trait(?Send)]
impl SessionStore for RedisSessionStore { impl SessionStore for RedisSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> { async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref()); let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
@ -205,6 +142,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 +164,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 +190,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 +199,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 +215,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 +223,26 @@ 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(
.expire::<_, ()>(&cache_key, ttl.whole_seconds()) &cache_key,
ttl.whole_seconds().try_into().context(
"Failed to convert the state TTL into the number of whole seconds remaining",
)?,
)
.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(())
@ -335,16 +263,11 @@ impl RedisSessionStore {
/// This helper method catches this case (`.is_connection_dropped`) to execute a retry. The /// This helper method catches this case (`.is_connection_dropped`) to execute a retry. The
/// 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)] async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<T> {
let mut can_retry = true; let mut can_retry = true;
match self.client {
RedisSessionConn::Single(ref conn) => {
let mut conn = conn.clone();
loop { loop {
match cmd.query_async(&mut conn).await { match cmd.query_async(&mut self.client.clone()).await {
Ok(value) => return Ok(value), Ok(value) => return Ok(value),
Err(err) => { Err(err) => {
if can_retry && err.is_connection_dropped() { if can_retry && err.is_connection_dropped() {
@ -357,34 +280,7 @@ impl RedisSessionStore {
continue; continue;
} else { } else {
return Err(err.into()); return Err(err);
}
}
}
}
}
#[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,29 +293,17 @@ 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 redis::AsyncCommands;
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") RedisSessionStore::new("redis://127.0.0.1:6379")
.await .await
.unwrap() .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]
async fn test_session_workflow() { async fn test_session_workflow() {
let redis_store = redis_store().await; let redis_store = redis_store().await;
@ -437,25 +321,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(),
#[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(); .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,6 @@
use derive_more::derive::{Display, From}; use std::convert::TryFrom;
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 +9,8 @@ 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 std::convert::TryInto;
/// 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 +48,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,19 @@
use rand::distr::{Alphanumeric, SampleString as _}; use std::convert::TryInto;
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

@ -48,7 +48,7 @@ async fn cookie_storage() -> std::io::Result<()> {
let deletion_cookie = logout_response.response().cookies().next().unwrap(); let deletion_cookie = logout_response.response().cookies().next().unwrap();
assert_eq!(deletion_cookie.name(), "id"); assert_eq!(deletion_cookie.name(), "id");
assert_eq!(deletion_cookie.path().unwrap(), "/test"); assert_eq!(deletion_cookie.path().unwrap(), "/test");
assert!(deletion_cookie.secure().unwrap()); assert!(deletion_cookie.secure().is_none());
assert!(deletion_cookie.http_only().unwrap()); assert!(deletion_cookie.http_only().unwrap());
assert_eq!(deletion_cookie.max_age().unwrap(), Duration::ZERO); assert_eq!(deletion_cookie.max_age().unwrap(), Duration::ZERO);
assert_eq!(deletion_cookie.domain().unwrap(), "localhost"); assert_eq!(deletion_cookie.domain().unwrap(), "localhost");

View File

@ -1,14 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto;
use actix_session::{ use actix_session::storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError};
storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError}, use actix_session::{Session, SessionMiddleware};
Session, SessionMiddleware, use actix_web::body::MessageBody;
}; use actix_web::http::StatusCode;
use actix_web::{ use actix_web::{
body::MessageBody,
cookie::{time::Duration, Key}, cookie::{time::Duration, Key},
dev::Service, dev::Service,
http::StatusCode,
test, web, App, Responder, test, web, App, Responder,
}; };
use anyhow::Error; use anyhow::Error;
@ -44,6 +43,7 @@ async fn errors_are_opaque() {
struct MockStore; struct MockStore;
#[async_trait::async_trait(?Send)]
impl SessionStore for MockStore { impl SessionStore for MockStore {
async fn load( async fn load(
&self, &self,
@ -68,18 +68,15 @@ impl SessionStore for MockStore {
_session_state: HashMap<String, String>, _session_state: HashMap<String, String>,
_ttl: &Duration, _ttl: &Duration,
) -> Result<SessionKey, UpdateError> { ) -> Result<SessionKey, UpdateError> {
#![allow(clippy::diverging_sub_expression)] todo!()
unimplemented!()
} }
async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> { async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
#![allow(clippy::diverging_sub_expression)] todo!()
unimplemented!()
} }
async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> { async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> {
#![allow(clippy::diverging_sub_expression)] todo!()
unimplemented!()
} }
} }

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

@ -1,40 +1,17 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
## 0.8.0
- Add `openssl` crate feature for TLS settings using OpenSSL.
- Add `ApplySettings::try_apply_settings()`.
- Implement TLS logic for `ApplySettings::try_apply_settings()`.
- Add `Tls::get_ssl_acceptor_builder()` function to build `openssl::ssl::SslAcceptorBuilder`.
- Deprecate `ApplySettings::apply_settings()`.
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.7.1
- Fix doc examples.
## 0.7.0
- The `ApplySettings` trait now includes a type parameter, allowing multiple types to be implemented per configuration target.
- Implement `ApplySettings` for `ActixSettings`.
- `BasicSettings::from_default_template()` is now infallible.
- Rename `AtError => Error`. - Rename `AtError => Error`.
- Remove `AtResult` type alias. - Remove `AtResult` type alias.
- Update `toml` dependency to `0.8`. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- Remove `ioe` dependency; `std::io::Error` is now used directly.
- Remove `Clone` implementation for `Error`.
- Implement `Display` for `Error`.
- Implement std's `Error` for `Error`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.6.0 ## 0.6.0 - 2022-07-31
- Update Actix Web dependencies to v4 ecosystem. - Update Actix Web dependencies to v4 ecosystem.
- Rename `actix.ssl` settings object to `actix.tls`. - Rename `actix.ssl` settings object to `actix.tls`.
- `NoSettings` is now marked `#[non_exhaustive]`. - `NoSettings` is now marked `#[non_exhaustive]`.
## 0.5.2 ## 0.5.2 - 2022-07-31
- Adopted into @actix org from <https://github.com/jjpe/actix-settings>. - Adopted into @actix org from <https://github.com/jjpe/actix-settings>.

View File

@ -1,37 +1,24 @@
[package] [package]
name = "actix-settings" name = "actix-settings"
version = "0.8.0" version = "0.6.0"
authors = [ authors = [
"Joey Ezechiels <joey.ezechiels@gmail.com>", "Joey Ezechiels <joey.ezechiels@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
edition = "2018"
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 license = "MIT OR Apache-2.0"
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
[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 = "4"
derive_more = { version = "2", features = ["display", "error"] }
once_cell = "1.21" ioe = "0.5"
openssl = { version = "0.10", features = ["v110"], optional = true } once_cell = "1.13"
regex = "1.5" regex = "1.5.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = "0.8" toml = "0.5"
[dev-dependencies] [dev-dependencies]
actix-web = "4" env_logger = "0.9"
env_logger = "0.11"
[lints]
workspace = true

View File

@ -2,14 +2,10 @@
> Easily manage Actix Web's settings from a TOML file and environment variables. > Easily manage Actix Web's settings from a TOML file and environment variables.
<!-- 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.6.0)](https://docs.rs/actix-settings/0.6.0)
![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.6.0/status.svg)](https://deps.rs/crate/actix-settings/0.6.0)
<!-- prettier-ignore-end -->
## Documentation & Resources ## Documentation & Resources
@ -23,6 +19,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,18 @@
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};
#[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, Clone)]
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}")]
EnvVarError(VarError), EnvVarError(VarError),
/// File already exists on disk. /// File already exists on disk.
#[display("File exists: {}", _0.display())] FileExists(PathBuf),
FileExists(#[error(not(source))] PathBuf),
/// Invalid value. /// Invalid value.
#[allow(missing_docs)] #[allow(missing_docs)]
#[display("Expected {expected}, got {got} (@ {file}:{line}:{column})")]
InvalidValue { InvalidValue {
expected: &'static str, expected: &'static str,
got: String, got: String,
@ -28,28 +22,18 @@ pub enum Error {
}, },
/// I/O error. /// I/O error.
#[display("I/O error: {_0}")] IoError(ioe::IoError),
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}")]
ParseBoolError(ParseBoolError), ParseBoolError(ParseBoolError),
/// Value is not an integer. /// Value is not an integer.
#[display("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}")] ParseAddressError(String),
ParseAddressError(#[error(not(source))] String),
/// Error deserializing as TOML. /// Error deserializing as TOML.
#[display("TOML error: {_0}")]
TomlError(TomlError), TomlError(TomlError),
} }
@ -67,14 +51,13 @@ macro_rules! InvalidValue {
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Self { fn from(err: io::Error) -> Self {
Self::IoError(err) Self::IoError(ioe::IoError::from(err))
} }
} }
#[cfg(feature = "openssl")] impl From<ioe::IoError> for Error {
impl From<OpenSSLError> for Error { fn from(err: ioe::IoError) -> Self {
fn from(err: OpenSSLError) -> Self { Self::IoError(err)
Self::OpenSSLError(err)
} }
} }
@ -105,30 +88,51 @@ impl From<VarError> for Error {
impl From<Error> for io::Error { impl From<Error> for io::Error {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
match err { match err {
Error::EnvVarError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), Error::EnvVarError(var_error) => {
let msg = format!("Env var error: {}", var_error);
Error::FileExists(_) => io::Error::new(io::ErrorKind::AlreadyExists, err.to_string()), io::Error::new(io::ErrorKind::InvalidInput, msg)
Error::InvalidValue { .. } => {
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
} }
Error::IoError(io_error) => io_error, Error::FileExists(path_buf) => {
let msg = format!("File exists: {}", path_buf.display());
#[cfg(feature = "openssl")] io::Error::new(io::ErrorKind::AlreadyExists, msg)
Error::OpenSSLError(ossl_error) => io::Error::new(io::ErrorKind::Other, ossl_error),
Error::ParseBoolError(_) => {
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
} }
Error::ParseIntError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), Error::InvalidValue {
expected,
Error::ParseAddressError(_) => { ref got,
io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) file,
line,
column,
} => {
let msg = format!(
"Expected {}, got {} (@ {}:{}:{})",
expected, got, file, line, column
);
io::Error::new(io::ErrorKind::InvalidInput, msg)
} }
Error::TomlError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), Error::IoError(io_error) => io_error.into(),
Error::ParseBoolError(parse_bool_error) => {
let msg = format!("Failed to parse boolean: {}", parse_bool_error);
io::Error::new(io::ErrorKind::InvalidInput, msg)
}
Error::ParseIntError(parse_int_error) => {
let msg = format!("Failed to parse integer: {}", parse_int_error);
io::Error::new(io::ErrorKind::InvalidInput, msg)
}
Error::ParseAddressError(string) => {
let msg = format!("Failed to parse address: {}", string);
io::Error::new(io::ErrorKind::InvalidInput, msg)
}
Error::TomlError(toml_error) => {
let msg = format!("TOML error: {}", toml_error);
io::Error::new(io::ErrorKind::InvalidInput, msg)
}
} }
} }
} }

View File

@ -54,17 +54,17 @@
//! } //! }
//! }) //! })
//! // 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))]
use std::{ use std::{
env, fmt, env, fmt,
@ -89,15 +89,11 @@ mod error;
mod parse; mod parse;
mod settings; mod settings;
#[cfg(feature = "openssl")] pub use self::error::Error;
pub use self::settings::Tls; pub use self::parse::Parse;
pub use self::{ pub use self::settings::{
error::Error,
parse::Parse,
settings::{
ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode, ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode,
NumWorkers, Timeout, NumWorkers, Timeout, Tls,
},
}; };
/// Convenience type alias for `Result<T, AtError>`. /// Convenience type alias for `Result<T, AtError>`.
@ -156,11 +152,14 @@ where
} }
/// Parse an instance of `Self` straight from the default TOML template. /// Parse an instance of `Self` straight from the default TOML template.
pub fn from_default_template() -> Self { // TODO: make infallible
Self::from_template(Self::DEFAULT_TOML_TEMPLATE).unwrap() // TODO: consider "template" rename
pub fn from_default_template() -> AsResult<Self> {
Self::from_template(Self::DEFAULT_TOML_TEMPLATE)
} }
/// Parse an instance of `Self` straight from the default TOML template. /// Parse an instance of `Self` straight from the default TOML template.
// TODO: consider "template" rename
pub fn from_template(template: &str) -> AsResult<Self> { pub fn from_template(template: &str) -> AsResult<Self> {
Ok(toml::from_str(template)?) Ok(toml::from_str(template)?)
} }
@ -168,8 +167,7 @@ where
/// Writes the default TOML template to a new file, located at `filepath`. /// Writes the default TOML template to a new file, located at `filepath`.
/// ///
/// # Errors /// # Errors
/// /// Returns a [`FileExists`](crate::AtError::FileExists) error if a file already exists at that
/// Returns a [`FileExists`](crate::Error::FileExists) error if a file already exists at that
/// location. /// location.
pub fn write_toml_file<P>(filepath: P) -> AsResult<()> pub fn write_toml_file<P>(filepath: P) -> AsResult<()>
where where
@ -195,7 +193,7 @@ where
/// use actix_settings::{Settings, Mode}; /// use actix_settings::{Settings, Mode};
/// ///
/// # fn inner() -> Result<(), actix_settings::Error> { /// # fn inner() -> Result<(), actix_settings::Error> {
/// let mut settings = Settings::from_default_template(); /// let mut settings = Settings::from_default_template()?;
/// assert_eq!(settings.actix.mode, Mode::Development); /// assert_eq!(settings.actix.mode, Mode::Development);
/// ///
/// Settings::override_field(&mut settings.actix.mode, "production")?; /// Settings::override_field(&mut settings.actix.mode, "production")?;
@ -220,7 +218,7 @@ where
/// std::env::set_var("OVERRIDE__MODE", "production"); /// std::env::set_var("OVERRIDE__MODE", "production");
/// ///
/// # fn inner() -> Result<(), actix_settings::Error> { /// # fn inner() -> Result<(), actix_settings::Error> {
/// let mut settings = Settings::from_default_template(); /// let mut settings = Settings::from_default_template()?;
/// assert_eq!(settings.actix.mode, Mode::Development); /// assert_eq!(settings.actix.mode, Mode::Development);
/// ///
/// Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE")?; /// Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE")?;
@ -241,31 +239,17 @@ 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 {
/// Applies some settings object value to `self`. /// Apply a [`BasicSettings`] value to `self`.
/// ///
/// The default implementation calls [`try_apply_settings()`]. /// [`BasicSettings`]: ./struct.BasicSettings.html
/// #[must_use]
/// # Panics fn apply_settings<A>(self, settings: &BasicSettings<A>) -> Self
/// where
/// May panic if settings are invalid or cannot be applied. A: de::DeserializeOwned;
///
/// [`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 for HttpServer<F, I, S, B>
where where
F: Fn() -> I + Send + Clone + 'static, F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>, I: IntoServiceFactory<S, Request>,
@ -276,58 +260,51 @@ where
S::Future: 'static, S::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
fn apply_settings(self, settings: &ActixSettings) -> Self { fn apply_settings<A>(mut self, settings: &BasicSettings<A>) -> Self
self.try_apply_settings(settings).unwrap() where
} A: de::DeserializeOwned,
fn try_apply_settings(mut self, settings: &ActixSettings) -> AsResult<Self> {
for Address { host, port } in &settings.hosts {
#[cfg(feature = "openssl")]
{ {
if settings.tls.enabled { if settings.actix.tls.enabled {
self = self.bind_openssl( // for Address { host, port } in &settings.actix.hosts {
format!("{}:{}", host, port), // self = self.bind(format!("{}:{}", host, port))
settings.tls.get_ssl_acceptor_builder()?, // .unwrap(/*TODO*/);
)?; // }
todo!("[ApplySettings] TLS support has not been implemented yet.");
} else { } else {
self = self.bind(format!("{host}:{port}"))?; for Address { host, port } in &settings.actix.hosts {
self = self.bind(format!("{}:{}", host, port))
.unwrap(/*TODO*/);
} }
} }
#[cfg(not(feature = "openssl"))] self = match settings.actix.num_workers {
{
self = self.bind(format!("{host}:{port}"))?;
}
}
self = match settings.num_workers {
NumWorkers::Default => self, NumWorkers::Default => self,
NumWorkers::Manual(n) => self.workers(n), NumWorkers::Manual(n) => self.workers(n),
}; };
self = match settings.backlog { self = match settings.actix.backlog {
Backlog::Default => self, Backlog::Default => self,
Backlog::Manual(n) => self.backlog(n as u32), Backlog::Manual(n) => self.backlog(n as u32),
}; };
self = match settings.max_connections { self = match settings.actix.max_connections {
MaxConnections::Default => self, MaxConnections::Default => self,
MaxConnections::Manual(n) => self.max_connections(n), MaxConnections::Manual(n) => self.max_connections(n),
}; };
self = match settings.max_connection_rate { self = match settings.actix.max_connection_rate {
MaxConnectionRate::Default => self, MaxConnectionRate::Default => self,
MaxConnectionRate::Manual(n) => self.max_connection_rate(n), MaxConnectionRate::Manual(n) => self.max_connection_rate(n),
}; };
self = match settings.keep_alive { self = match settings.actix.keep_alive {
KeepAlive::Default => self, KeepAlive::Default => self,
KeepAlive::Disabled => self.keep_alive(ActixKeepAlive::Disabled), KeepAlive::Disabled => self.keep_alive(ActixKeepAlive::Disabled),
KeepAlive::Os => self.keep_alive(ActixKeepAlive::Os), KeepAlive::Os => self.keep_alive(ActixKeepAlive::Os),
KeepAlive::Seconds(n) => self.keep_alive(Duration::from_secs(n as u64)), KeepAlive::Seconds(n) => self.keep_alive(Duration::from_secs(n as u64)),
}; };
self = match settings.client_timeout { self = match settings.actix.client_timeout {
Timeout::Default => self, Timeout::Default => self,
Timeout::Milliseconds(n) => { Timeout::Milliseconds(n) => {
self.client_request_timeout(Duration::from_millis(n as u64)) self.client_request_timeout(Duration::from_millis(n as u64))
@ -335,7 +312,7 @@ where
Timeout::Seconds(n) => self.client_request_timeout(Duration::from_secs(n as u64)), Timeout::Seconds(n) => self.client_request_timeout(Duration::from_secs(n as u64)),
}; };
self = match settings.client_shutdown { self = match settings.actix.client_shutdown {
Timeout::Default => self, Timeout::Default => self,
Timeout::Milliseconds(n) => { Timeout::Milliseconds(n) => {
self.client_disconnect_timeout(Duration::from_millis(n as u64)) self.client_disconnect_timeout(Duration::from_millis(n as u64))
@ -343,34 +320,13 @@ where
Timeout::Seconds(n) => self.client_disconnect_timeout(Duration::from_secs(n as u64)), Timeout::Seconds(n) => self.client_disconnect_timeout(Duration::from_secs(n as u64)),
}; };
self = match settings.shutdown_timeout { self = match settings.actix.shutdown_timeout {
Timeout::Default => self, Timeout::Default => self,
Timeout::Milliseconds(_) => self.shutdown_timeout(1), Timeout::Milliseconds(_) => self.shutdown_timeout(1),
Timeout::Seconds(n) => self.shutdown_timeout(n as u64), Timeout::Seconds(n) => self.shutdown_timeout(n as u64),
}; };
Ok(self) self
}
}
impl<F, I, S, B, A> ApplySettings<BasicSettings<A>> for HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<WebError> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
A: de::DeserializeOwned,
{
fn apply_settings(self, settings: &BasicSettings<A>) -> Self {
self.try_apply_settings(&settings.actix).unwrap()
}
fn try_apply_settings(self, settings: &BasicSettings<A>) -> AsResult<Self> {
self.try_apply_settings(&settings.actix)
} }
} }
@ -383,13 +339,12 @@ 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]
fn override_field_hosts() { fn override_field_hosts() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!( assert_eq!(
settings.actix.hosts, settings.actix.hosts,
@ -425,7 +380,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_hosts() { fn override_field_with_env_var_hosts() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!( assert_eq!(
settings.actix.hosts, settings.actix.hosts,
@ -463,7 +418,7 @@ mod tests {
#[test] #[test]
fn override_field_mode() { fn override_field_mode() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.mode, Mode::Development); assert_eq!(settings.actix.mode, Mode::Development);
Settings::override_field(&mut settings.actix.mode, "production").unwrap(); Settings::override_field(&mut settings.actix.mode, "production").unwrap();
assert_eq!(settings.actix.mode, Mode::Production); assert_eq!(settings.actix.mode, Mode::Production);
@ -471,7 +426,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_mode() { fn override_field_with_env_var_mode() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.mode, Mode::Development); assert_eq!(settings.actix.mode, Mode::Development);
std::env::set_var("OVERRIDE__MODE", "production"); std::env::set_var("OVERRIDE__MODE", "production");
Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE").unwrap(); Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE").unwrap();
@ -480,7 +435,7 @@ mod tests {
#[test] #[test]
fn override_field_enable_compression() { fn override_field_enable_compression() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert!(settings.actix.enable_compression); assert!(settings.actix.enable_compression);
Settings::override_field(&mut settings.actix.enable_compression, "false").unwrap(); Settings::override_field(&mut settings.actix.enable_compression, "false").unwrap();
assert!(!settings.actix.enable_compression); assert!(!settings.actix.enable_compression);
@ -488,7 +443,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_enable_compression() { fn override_field_with_env_var_enable_compression() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert!(settings.actix.enable_compression); assert!(settings.actix.enable_compression);
std::env::set_var("OVERRIDE__ENABLE_COMPRESSION", "false"); std::env::set_var("OVERRIDE__ENABLE_COMPRESSION", "false");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -501,7 +456,7 @@ mod tests {
#[test] #[test]
fn override_field_enable_log() { fn override_field_enable_log() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert!(settings.actix.enable_log); assert!(settings.actix.enable_log);
Settings::override_field(&mut settings.actix.enable_log, "false").unwrap(); Settings::override_field(&mut settings.actix.enable_log, "false").unwrap();
assert!(!settings.actix.enable_log); assert!(!settings.actix.enable_log);
@ -509,7 +464,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_enable_log() { fn override_field_with_env_var_enable_log() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert!(settings.actix.enable_log); assert!(settings.actix.enable_log);
std::env::set_var("OVERRIDE__ENABLE_LOG", "false"); std::env::set_var("OVERRIDE__ENABLE_LOG", "false");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -522,7 +477,7 @@ mod tests {
#[test] #[test]
fn override_field_num_workers() { fn override_field_num_workers() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.num_workers, NumWorkers::Default); assert_eq!(settings.actix.num_workers, NumWorkers::Default);
Settings::override_field(&mut settings.actix.num_workers, "42").unwrap(); Settings::override_field(&mut settings.actix.num_workers, "42").unwrap();
assert_eq!(settings.actix.num_workers, NumWorkers::Manual(42)); assert_eq!(settings.actix.num_workers, NumWorkers::Manual(42));
@ -530,7 +485,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_num_workers() { fn override_field_with_env_var_num_workers() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.num_workers, NumWorkers::Default); assert_eq!(settings.actix.num_workers, NumWorkers::Default);
std::env::set_var("OVERRIDE__NUM_WORKERS", "42"); std::env::set_var("OVERRIDE__NUM_WORKERS", "42");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -543,7 +498,7 @@ mod tests {
#[test] #[test]
fn override_field_backlog() { fn override_field_backlog() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.backlog, Backlog::Default); assert_eq!(settings.actix.backlog, Backlog::Default);
Settings::override_field(&mut settings.actix.backlog, "42").unwrap(); Settings::override_field(&mut settings.actix.backlog, "42").unwrap();
assert_eq!(settings.actix.backlog, Backlog::Manual(42)); assert_eq!(settings.actix.backlog, Backlog::Manual(42));
@ -551,7 +506,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_backlog() { fn override_field_with_env_var_backlog() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.backlog, Backlog::Default); assert_eq!(settings.actix.backlog, Backlog::Default);
std::env::set_var("OVERRIDE__BACKLOG", "42"); std::env::set_var("OVERRIDE__BACKLOG", "42");
Settings::override_field_with_env_var(&mut settings.actix.backlog, "OVERRIDE__BACKLOG") Settings::override_field_with_env_var(&mut settings.actix.backlog, "OVERRIDE__BACKLOG")
@ -561,7 +516,7 @@ mod tests {
#[test] #[test]
fn override_field_max_connections() { fn override_field_max_connections() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.max_connections, MaxConnections::Default); assert_eq!(settings.actix.max_connections, MaxConnections::Default);
Settings::override_field(&mut settings.actix.max_connections, "42").unwrap(); Settings::override_field(&mut settings.actix.max_connections, "42").unwrap();
assert_eq!(settings.actix.max_connections, MaxConnections::Manual(42)); assert_eq!(settings.actix.max_connections, MaxConnections::Manual(42));
@ -569,7 +524,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_max_connections() { fn override_field_with_env_var_max_connections() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.max_connections, MaxConnections::Default); assert_eq!(settings.actix.max_connections, MaxConnections::Default);
std::env::set_var("OVERRIDE__MAX_CONNECTIONS", "42"); std::env::set_var("OVERRIDE__MAX_CONNECTIONS", "42");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -582,7 +537,7 @@ mod tests {
#[test] #[test]
fn override_field_max_connection_rate() { fn override_field_max_connection_rate() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!( assert_eq!(
settings.actix.max_connection_rate, settings.actix.max_connection_rate,
MaxConnectionRate::Default MaxConnectionRate::Default
@ -596,7 +551,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_max_connection_rate() { fn override_field_with_env_var_max_connection_rate() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!( assert_eq!(
settings.actix.max_connection_rate, settings.actix.max_connection_rate,
MaxConnectionRate::Default MaxConnectionRate::Default
@ -615,7 +570,7 @@ mod tests {
#[test] #[test]
fn override_field_keep_alive() { fn override_field_keep_alive() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.keep_alive, KeepAlive::Default); assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
Settings::override_field(&mut settings.actix.keep_alive, "42 seconds").unwrap(); Settings::override_field(&mut settings.actix.keep_alive, "42 seconds").unwrap();
assert_eq!(settings.actix.keep_alive, KeepAlive::Seconds(42)); assert_eq!(settings.actix.keep_alive, KeepAlive::Seconds(42));
@ -623,7 +578,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_keep_alive() { fn override_field_with_env_var_keep_alive() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.keep_alive, KeepAlive::Default); assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
std::env::set_var("OVERRIDE__KEEP_ALIVE", "42 seconds"); std::env::set_var("OVERRIDE__KEEP_ALIVE", "42 seconds");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -636,7 +591,7 @@ mod tests {
#[test] #[test]
fn override_field_client_timeout() { fn override_field_client_timeout() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.client_timeout, Timeout::Default); assert_eq!(settings.actix.client_timeout, Timeout::Default);
Settings::override_field(&mut settings.actix.client_timeout, "42 seconds").unwrap(); Settings::override_field(&mut settings.actix.client_timeout, "42 seconds").unwrap();
assert_eq!(settings.actix.client_timeout, Timeout::Seconds(42)); assert_eq!(settings.actix.client_timeout, Timeout::Seconds(42));
@ -644,7 +599,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_client_timeout() { fn override_field_with_env_var_client_timeout() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.client_timeout, Timeout::Default); assert_eq!(settings.actix.client_timeout, Timeout::Default);
std::env::set_var("OVERRIDE__CLIENT_TIMEOUT", "42 seconds"); std::env::set_var("OVERRIDE__CLIENT_TIMEOUT", "42 seconds");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -657,7 +612,7 @@ mod tests {
#[test] #[test]
fn override_field_client_shutdown() { fn override_field_client_shutdown() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.client_shutdown, Timeout::Default); assert_eq!(settings.actix.client_shutdown, Timeout::Default);
Settings::override_field(&mut settings.actix.client_shutdown, "42 seconds").unwrap(); Settings::override_field(&mut settings.actix.client_shutdown, "42 seconds").unwrap();
assert_eq!(settings.actix.client_shutdown, Timeout::Seconds(42)); assert_eq!(settings.actix.client_shutdown, Timeout::Seconds(42));
@ -665,7 +620,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_client_shutdown() { fn override_field_with_env_var_client_shutdown() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.client_shutdown, Timeout::Default); assert_eq!(settings.actix.client_shutdown, Timeout::Default);
std::env::set_var("OVERRIDE__CLIENT_SHUTDOWN", "42 seconds"); std::env::set_var("OVERRIDE__CLIENT_SHUTDOWN", "42 seconds");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -678,7 +633,7 @@ mod tests {
#[test] #[test]
fn override_field_shutdown_timeout() { fn override_field_shutdown_timeout() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default); assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
Settings::override_field(&mut settings.actix.shutdown_timeout, "42 seconds").unwrap(); Settings::override_field(&mut settings.actix.shutdown_timeout, "42 seconds").unwrap();
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42)); assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
@ -686,7 +641,7 @@ mod tests {
#[test] #[test]
fn override_field_with_env_var_shutdown_timeout() { fn override_field_with_env_var_shutdown_timeout() {
let mut settings = Settings::from_default_template(); let mut settings = Settings::from_default_template().unwrap();
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default); assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
std::env::set_var("OVERRIDE__SHUTDOWN_TIMEOUT", "42 seconds"); std::env::set_var("OVERRIDE__SHUTDOWN_TIMEOUT", "42 seconds");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -697,19 +652,17 @@ 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().unwrap();
assert!(!settings.actix.tls.enabled); assert!(!settings.actix.tls.enabled);
Settings::override_field(&mut settings.actix.tls.enabled, "true").unwrap(); Settings::override_field(&mut settings.actix.tls.enabled, "true").unwrap();
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().unwrap();
assert!(!settings.actix.tls.enabled); assert!(!settings.actix.tls.enabled);
std::env::set_var("OVERRIDE__TLS_ENABLED", "true"); std::env::set_var("OVERRIDE__TLS_ENABLED", "true");
Settings::override_field_with_env_var( Settings::override_field_with_env_var(
@ -720,10 +673,9 @@ 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().unwrap();
assert_eq!( assert_eq!(
settings.actix.tls.certificate, settings.actix.tls.certificate,
Path::new("path/to/cert/cert.pem") Path::new("path/to/cert/cert.pem")
@ -739,10 +691,9 @@ 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().unwrap();
assert_eq!( assert_eq!(
settings.actix.tls.certificate, settings.actix.tls.certificate,
Path::new("path/to/cert/cert.pem") Path::new("path/to/cert/cert.pem")
@ -762,10 +713,9 @@ 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().unwrap();
assert_eq!( assert_eq!(
settings.actix.tls.private_key, settings.actix.tls.private_key,
Path::new("path/to/cert/key.pem") Path::new("path/to/cert/key.pem")
@ -781,10 +731,9 @@ 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().unwrap();
assert_eq!( assert_eq!(
settings.actix.tls.private_key, settings.actix.tls.private_key,
Path::new("path/to/cert/key.pem") Path::new("path/to/cert/key.pem")

View File

@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr};
use crate::Error; use crate::Error;
/// A specialized `FromStr` trait that returns [`Error`] errors /// A specialized `FromStr` trait that returns [`AtError`] errors
pub trait Parse: Sized { pub trait Parse: Sized {
/// Parse `Self` from `string`. /// Parse `Self` from `string`.
fn parse(string: &str) -> Result<Self, Error>; fn parse(string: &str) -> Result<Self, Error>;

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,16 +8,17 @@ 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::address::Address;
pub use self::backlog::Backlog;
pub use self::keep_alive::KeepAlive;
pub use self::max_connection_rate::MaxConnectionRate;
pub use self::max_connections::MaxConnections;
pub use self::mode::Mode;
pub use self::num_workers::NumWorkers;
pub use self::timeout::Timeout;
pub use self::tls::Tls; pub use self::tls::Tls;
pub use self::{
address::Address, backlog::Backlog, keep_alive::KeepAlive,
max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode,
num_workers::NumWorkers, timeout::Timeout,
};
/// Settings types for Actix Web. /// Settings types for Actix Web.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
@ -29,7 +30,7 @@ pub struct ActixSettings {
/// Marker of intended deployment environment. /// Marker of intended deployment environment.
pub mode: Mode, pub mode: Mode,
/// True if the `Compress` middleware should be enabled. /// True if the [`Compress`](actix_web::middleware::Compress) middleware should be enabled.
pub enable_compression: bool, pub enable_compression: bool,
/// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled. /// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled.
@ -60,6 +61,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

@ -1,18 +1,10 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## 0.8.2
- Minimum supported Rust version (MSRV) is now 1.75.
## 0.8.1
- Implement `From<Basic>` for `BasicAuth`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.8.0
## 0.8.0 - 2022-07-21
- Removed `AuthExtractor` trait; implement `FromRequest` for your custom auth types. [#264] - Removed `AuthExtractor` trait; implement `FromRequest` for your custom auth types. [#264]
- `BasicAuth::user_id()` now returns `&str`. [#249] - `BasicAuth::user_id()` now returns `&str`. [#249]
- `BasicAuth::password()` now returns `Option<&str>`. [#249] - `BasicAuth::password()` now returns `Option<&str>`. [#249]
@ -23,75 +15,75 @@
[#249]: https://github.com/actix/actix-extras/pull/249 [#249]: https://github.com/actix/actix-extras/pull/249
[#264]: https://github.com/actix/actix-extras/pull/264 [#264]: https://github.com/actix/actix-extras/pull/264
## 0.7.0
## 0.7.0 - 2022-07-19
- Auth validator functions now need to return `(Error, ServiceRequest)` in error cases. [#260] - Auth validator functions now need to return `(Error, ServiceRequest)` in error cases. [#260]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#260]: https://github.com/actix/actix-extras/pull/260 [#260]: https://github.com/actix/actix-extras/pull/260
## 0.6.0
## 0.6.0 - 2022-03-01
- Update `actix-web` dependency to `4`. - Update `actix-web` dependency to `4`.
## 0.6.0-beta.8
## 0.6.0-beta.8 - 2022-02-07
- Relax body type bounds on middleware impl. [#223] - Relax body type bounds on middleware impl. [#223]
- Update `actix-web` dependency to `4.0.0-rc.1`. - Update `actix-web` dependency to `4.0.0-rc.1`.
[#223]: https://github.com/actix/actix-extras/pull/223 [#223]: https://github.com/actix/actix-extras/pull/223
## 0.6.0-beta.7
## 0.6.0-beta.7 - 2021-12-29
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
## 0.6.0-beta.6
## 0.6.0-beta.6 - 2021-12-18
- Update `actix-web` dependency to `4.0.0.beta-15`. [#216] - Update `actix-web` dependency to `4.0.0.beta-15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216 [#216]: https://github.com/actix/actix-extras/pull/216
## 0.6.0-beta.5
## 0.6.0-beta.5 - 2021-12-12
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209] - Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#209]: https://github.com/actix/actix-extras/pull/209 [#209]: https://github.com/actix/actix-extras/pull/209
## 0.6.0-beta.4
## 0.6.0-beta.4 - 2021-11-22
- impl `AuthExtractor` trait for `Option<T: AuthExtractor>` and `Result<T: AuthExtractor, T::Error>`. [#205] - impl `AuthExtractor` trait for `Option<T: AuthExtractor>` and `Result<T: AuthExtractor, T::Error>`. [#205]
[#205]: https://github.com/actix/actix-extras/pull/205 [#205]: https://github.com/actix/actix-extras/pull/205
## 0.6.0-beta.3
## 0.6.0-beta.3 - 2021-10-21
- Update `actix-web` dependency to v4.0.0-beta.10. [#203] - Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
[#203]: https://github.com/actix/actix-extras/pull/203 [#203]: https://github.com/actix/actix-extras/pull/203
## 0.6.0-beta.2
## 0.6.0-beta.2 - 2021-06-27
- No notable changes. - No notable changes.
## 0.6.0-beta.1
## 0.6.0-beta.1 - 2021-04-02
- Update `actix-web` dependency to 4.0.0 beta. - Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0. - Minimum supported Rust version (MSRV) is now 1.46.0.
## 0.5.1
## 0.5.1 - 2021-03-21
- Correct error handling when extracting auth details from request. [#128] - Correct error handling when extracting auth details from request. [#128]
[#128]: https://github.com/actix/actix-extras/pull/128 [#128]: https://github.com/actix/actix-extras/pull/128
## 0.5.0
## 0.5.0 - 2020-09-11
- Update `actix-web` dependency to 3.0.0. - Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0. - Minimum supported Rust version (MSRV) is now 1.42.0.
## 0.4.2
## 0.4.2 - 2020-07-08
- Update the `base64` dependency to 0.12 - Update the `base64` dependency to 0.12
- AuthenticationError's status code is preserved when converting to a ResponseError - AuthenticationError's status code is preserved when converting to a ResponseError
- Minimize `futures` dependency - Minimize `futures` dependency
@ -99,46 +91,46 @@
[#69]: https://github.com/actix/actix-web-httpauth/pull/69 [#69]: https://github.com/actix/actix-web-httpauth/pull/69
## 0.4.1
## 0.4.1 - 2020-02-19
- Move repository to actix-extras - Move repository to actix-extras
## 0.4.0
## 0.4.0 - 2020-01-14
- Depends on `actix-web = "^2.0"`, `actix-service = "^1.0"`, and `futures = "^0.3"` version now ([#14]) - Depends on `actix-web = "^2.0"`, `actix-service = "^1.0"`, and `futures = "^0.3"` version now ([#14])
- Depends on `bytes = "^0.5"` and `base64 = "^0.11"` now - Depends on `bytes = "^0.5"` and `base64 = "^0.11"` now
[#14]: https://github.com/actix/actix-web-httpauth/pull/14 [#14]: https://github.com/actix/actix-web-httpauth/pull/14
## 0.3.2 - 2019-07-19
## 0.3.2 - 2019-07-19
- Middleware accepts any `Fn` as a validator function instead of `FnMut` [#11] - Middleware accepts any `Fn` as a validator function instead of `FnMut` [#11]
[#11]: https://github.com/actix/actix-web-httpauth/pull/11 [#11]: https://github.com/actix/actix-web-httpauth/pull/11
## 0.3.1 - 2019-06-09
## 0.3.1 - 2019-06-09
- Multiple calls to the middleware would result in panic - Multiple calls to the middleware would result in panic
## 0.3.0 - 2019-06-05
## 0.3.0 - 2019-06-05
- Crate edition was changed to `2018`, same as `actix-web` - Crate edition was changed to `2018`, same as `actix-web`
- Depends on `actix-web = "^1.0"` version now - Depends on `actix-web = "^1.0"` version now
- `WWWAuthenticate` header struct was renamed into `WwwAuthenticate` - `WWWAuthenticate` header struct was renamed into `WwwAuthenticate`
- Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types - Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types
## 0.2.0 - 2019-04-26
## 0.2.0 - 2019-04-26
- `actix-web` dependency is used without default features now [#6] - `actix-web` dependency is used without default features now [#6]
- `base64` dependency version was bumped to `0.10` - `base64` dependency version was bumped to `0.10`
[#6]: https://github.com/actix/actix-web-httpauth/pull/6 [#6]: https://github.com/actix/actix-web-httpauth/pull/6
## 0.1.0 - 2018-09-08
## 0.1.0 - 2018-09-08
- Update to `actix-web = "0.7"` version - Update to `actix-web = "0.7"` version
## 0.0.4 - 2018-07-01
## 0.0.4 - 2018-07-01
- Fix possible panic at `IntoHeaderValue` implementation for `headers::authorization::Basic` - Fix possible panic at `IntoHeaderValue` implementation for `headers::authorization::Basic`
- Fix possible panic at `headers::www_authenticate::challenge::bearer::Bearer::to_bytes` call - Fix possible panic at `headers::www_authenticate::challenge::bearer::Bearer::to_bytes` call

View File

@ -1,39 +1,33 @@
[package] [package]
name = "actix-web-httpauth" name = "actix-web-httpauth"
version = "0.8.2" version = "0.8.0"
description = "HTTP authentication schemes for Actix Web"
categories = ["web-programming"]
keywords = ["http", "web", "framework", "authentication", "security"]
authors = [ authors = [
"svartalf <self@svartalf.info>", "svartalf <self@svartalf.info>",
"Yuki Okushi <huyuumi.dev@gmail.com>", "Yuki Okushi <huyuumi.dev@gmail.com>",
] ]
repository.workspace = true description = "HTTP authentication schemes for Actix Web"
homepage.workspace = true keywords = ["http", "web", "framework", "authentication", "security"]
license.workspace = true homepage = "https://actix.rs"
edition.workspace = true repository = "https://github.com/actix/actix-extras.git"
rust-version.workspace = true categories = ["web-programming::http-server"]
license = "MIT OR Apache-2.0"
edition = "2018"
[package.metadata.docs.rs] [lib]
rustdoc-args = ["--cfg", "docsrs"] name = "actix_web_httpauth"
all-features = true path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4.1", default-features = false } actix-web = { version = "4.1", default_features = false }
base64 = "0.22" base64 = "0.13"
futures-core = "0.3.17" futures-core = "0.3.7"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
log = "0.4" log = "0.4"
pin-project-lite = "0.2.7" pin-project-lite = "0.2.7"
[dev-dependencies] [dev-dependencies]
actix-cors = "0.7" actix-cors = "0.6"
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

@ -2,14 +2,10 @@
> HTTP authentication schemes for [Actix Web](https://actix.rs). > HTTP authentication schemes for [Actix Web](https://actix.rs).
<!-- 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.0)](https://docs.rs/actix-web-httpauth/0.8.0)
![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.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.8.0)
<!-- prettier-ignore-end -->
## Documentation & Resources ## Documentation & Resources

View File

@ -1,57 +1,26 @@
use actix_web::{ use actix_web::dev::ServiceRequest;
dev::ServiceRequest, error, get, middleware::Logger, App, Error, HttpServer, Responder, use actix_web::{middleware, web, App, Error, HttpServer};
}; use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; use actix_web_httpauth::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

@ -6,7 +6,7 @@ async fn ok_validator(
req: ServiceRequest, req: ServiceRequest,
credentials: BearerAuth, credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> { ) -> Result<ServiceRequest, (Error, ServiceRequest)> {
eprintln!("{credentials:?}"); eprintln!("{:?}", credentials);
Ok(req) Ok(req)
} }

View File

@ -89,12 +89,6 @@ impl BasicAuth {
} }
} }
impl From<Basic> for BasicAuth {
fn from(basic: Basic) -> Self {
Self(basic)
}
}
impl FromRequest for BasicAuth { impl FromRequest for BasicAuth {
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<Challenge>; type Error = AuthenticationError<Challenge>;

View File

@ -102,7 +102,7 @@ impl FromRequest for BearerAuth {
let bearer = req let bearer = req
.app_data::<Config>() .app_data::<Config>()
.map(|config| config.0.clone()) .map(|config| config.0.clone())
.unwrap_or_default(); .unwrap_or_else(Default::default);
AuthenticationError::new(bearer) AuthenticationError::new(bearer)
}), }),

Some files were not shown because too many files have changed in this diff Show More