├── .cargo └── config.toml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci-post-merge.yml │ ├── ci.yml │ ├── coverage.yml │ └── lint.yml ├── .gitignore ├── .prettierrc.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── actix-cors ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── cors.rs ├── src │ ├── all_or_some.rs │ ├── builder.rs │ ├── error.rs │ ├── inner.rs │ ├── lib.rs │ └── middleware.rs └── tests │ └── tests.rs ├── actix-identity ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ └── identity.rs ├── src │ ├── config.rs │ ├── error.rs │ ├── identity.rs │ ├── identity_ext.rs │ ├── lib.rs │ └── middleware.rs └── tests │ └── integration │ ├── fixtures.rs │ ├── integration.rs │ ├── main.rs │ └── test_app.rs ├── actix-limitation ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── builder.rs │ ├── errors.rs │ ├── lib.rs │ ├── middleware.rs │ └── status.rs └── tests │ └── tests.rs ├── actix-protobuf ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src │ └── lib.rs ├── actix-session ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── authentication.rs │ └── basic.rs ├── src │ ├── config.rs │ ├── lib.rs │ ├── middleware.rs │ ├── session.rs │ ├── session_ext.rs │ └── storage │ │ ├── cookie.rs │ │ ├── interface.rs │ │ ├── mod.rs │ │ ├── redis_actor.rs │ │ ├── redis_rs.rs │ │ ├── session_key.rs │ │ └── utils.rs └── tests │ ├── middleware.rs │ ├── opaque_errors.rs │ └── session.rs ├── actix-settings ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── actix.rs │ └── config.toml └── src │ ├── defaults.toml │ ├── error.rs │ ├── lib.rs │ ├── parse.rs │ └── settings │ ├── address.rs │ ├── backlog.rs │ ├── keep_alive.rs │ ├── max_connection_rate.rs │ ├── max_connections.rs │ ├── mod.rs │ ├── mode.rs │ ├── num_workers.rs │ ├── timeout.rs │ └── tls.rs ├── actix-web-httpauth ├── CHANGES.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── middleware-closure.rs │ ├── middleware.rs │ └── with-cors.rs └── src │ ├── extractors │ ├── basic.rs │ ├── bearer.rs │ ├── config.rs │ ├── errors.rs │ └── mod.rs │ ├── headers │ ├── authorization │ │ ├── errors.rs │ │ ├── header.rs │ │ ├── mod.rs │ │ └── scheme │ │ │ ├── basic.rs │ │ │ ├── bearer.rs │ │ │ └── mod.rs │ ├── mod.rs │ └── www_authenticate │ │ ├── challenge │ │ ├── basic.rs │ │ ├── bearer │ │ │ ├── builder.rs │ │ │ ├── challenge.rs │ │ │ ├── errors.rs │ │ │ └── mod.rs │ │ └── mod.rs │ │ ├── header.rs │ │ └── mod.rs │ ├── lib.rs │ ├── middleware.rs │ └── utils.rs ├── actix-ws ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── chat.html │ └── chat.rs └── src │ ├── aggregated.rs │ ├── lib.rs │ ├── session.rs │ └── stream.rs ├── codecov.yml ├── justfile ├── lcov.info └── rustfmt.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo" 3 | ci-min = "hack check --workspace --no-default-features" 4 | ci-check-min-examples = "hack check --workspace --no-default-features --examples" 5 | ci-check = "check --workspace --tests --examples --bins" 6 | ci-test = "test --workspace --lib --tests --all-features --examples --bins --no-fail-fast" 7 | ci-doctest = "test --workspace --doc --all-features --no-fail-fast" 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.yml] 12 | indent_size = 2 13 | 14 | [*.md] 15 | indent_size = 2 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: bug report 3 | about: create a bug report 4 | --- 5 | 6 | Your issue may already be reported! Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one. 7 | 8 | ## Expected Behavior 9 | 10 | 11 | 12 | 13 | ## Current Behavior 14 | 15 | 16 | 17 | 18 | ## Possible Solution 19 | 20 | 21 | 22 | 23 | ## Steps to Reproduce (for bugs) 24 | 25 | 26 | 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 4. 32 | 33 | ## Context 34 | 35 | 36 | 37 | 38 | ## Your Environment 39 | 40 | 41 | 42 | - Rust version (output of `rustc -V`): 43 | - `actix-*` crate versions: 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## PR Type 5 | 6 | 7 | 8 | 9 | INSERT_PR_TYPE 10 | 11 | ## PR Checklist 12 | 13 | 14 | 15 | 16 | - [ ] Tests for the changes have been added / updated. 17 | - [ ] Documentation comments have been added / updated. 18 | - [ ] A changelog entry has been made for the appropriate packages. 19 | - [ ] Format code with the nightly rustfmt (`cargo +nightly fmt`). 20 | 21 | ## Overview 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/ci-post-merge.yml: -------------------------------------------------------------------------------- 1 | name: CI (post-merge) 2 | 3 | on: 4 | push: { branches: [master] } 5 | 6 | permissions: { contents: read } 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build_and_test_linux_nightly: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | target: 18 | - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } 19 | 20 | name: ${{ matrix.target.name }} / nightly 21 | runs-on: ${{ matrix.target.os }} 22 | 23 | services: 24 | redis: 25 | image: redis:5.0.7 26 | ports: 27 | - 6379:6379 28 | options: --entrypoint redis-server 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Install Rust (nightly) 34 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 35 | with: 36 | toolchain: nightly 37 | 38 | - name: Install cargo-hack, cargo-ci-cache-clean 39 | uses: taiki-e/install-action@v2.50.4 40 | with: 41 | tool: cargo-hack,cargo-ci-cache-clean 42 | 43 | - name: check minimal 44 | run: cargo ci-min 45 | 46 | - name: check minimal + examples 47 | run: cargo ci-check-min-examples 48 | 49 | - name: check default 50 | run: cargo ci-check 51 | 52 | - name: tests 53 | timeout-minutes: 40 54 | run: cargo ci-test 55 | 56 | - name: CI cache clean 57 | run: cargo-ci-cache-clean 58 | 59 | build_and_test_other_nightly: 60 | strategy: 61 | fail-fast: false 62 | # prettier-ignore 63 | matrix: 64 | target: 65 | - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } 66 | - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } 67 | 68 | name: ${{ matrix.target.name }} / nightly 69 | runs-on: ${{ matrix.target.os }} 70 | 71 | steps: 72 | - uses: actions/checkout@v4 73 | 74 | - name: Install OpenSSL 75 | if: matrix.target.os == 'windows-latest' 76 | shell: bash 77 | run: | 78 | set -e 79 | choco install openssl --version=1.1.1.2100 -y --no-progress 80 | echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV 81 | echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV 82 | 83 | - name: Install Rust (nightly) 84 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 85 | with: 86 | toolchain: nightly 87 | 88 | - name: Install cargo-hack and cargo-ci-cache-clean 89 | uses: taiki-e/install-action@v2.50.4 90 | with: 91 | tool: cargo-hack,cargo-ci-cache-clean 92 | 93 | - name: check minimal 94 | run: cargo ci-min 95 | 96 | - name: check minimal + examples 97 | run: cargo ci-check-min-examples 98 | 99 | - name: check default 100 | run: cargo ci-check 101 | 102 | - name: tests 103 | timeout-minutes: 40 104 | run: cargo ci-test --exclude=actix-session --exclude=actix-limitation -- --nocapture 105 | 106 | - name: CI cache clean 107 | run: cargo-ci-cache-clean 108 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | merge_group: 7 | types: [checks_requested] 8 | push: 9 | branches: [master] 10 | 11 | permissions: { contents: read } 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build_and_test_linux: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | target: 23 | - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } 24 | version: 25 | - { name: msrv, version: 1.75.0 } 26 | - { name: stable, version: stable } 27 | 28 | name: ${{ matrix.target.name }} / ${{ matrix.version.name }} 29 | runs-on: ${{ matrix.target.os }} 30 | 31 | services: 32 | redis: 33 | image: redis:6 34 | ports: 35 | - 6379:6379 36 | options: >- 37 | --health-cmd "redis-cli ping" 38 | --health-interval 10s 39 | --health-timeout 5s 40 | --health-retries 5 41 | --entrypoint redis-server 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - name: Install Rust (${{ matrix.version.name }}) 47 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 48 | with: 49 | toolchain: ${{ matrix.version.version }} 50 | 51 | - name: Install cargo-hack and cargo-ci-cache-clean, just 52 | uses: taiki-e/install-action@v2.50.4 53 | with: 54 | tool: cargo-hack,cargo-ci-cache-clean,just 55 | 56 | - name: workaround MSRV issues 57 | if: matrix.version.name == 'msrv' 58 | run: just downgrade-for-msrv 59 | 60 | - name: check minimal 61 | run: cargo ci-min 62 | 63 | - name: check minimal + examples 64 | run: cargo ci-check-min-examples 65 | 66 | - name: check default 67 | run: cargo ci-check 68 | 69 | - name: tests 70 | timeout-minutes: 40 71 | run: cargo ci-test 72 | 73 | - name: CI cache clean 74 | run: cargo-ci-cache-clean 75 | 76 | build_and_test_other: 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | # prettier-ignore 81 | target: 82 | - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } 83 | - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } 84 | version: 85 | - { name: msrv, version: 1.75.0 } 86 | - { name: stable, version: stable } 87 | 88 | name: ${{ matrix.target.name }} / ${{ matrix.version.name }} 89 | runs-on: ${{ matrix.target.os }} 90 | 91 | steps: 92 | - uses: actions/checkout@v4 93 | 94 | - name: Install OpenSSL 95 | if: matrix.target.os == 'windows-latest' 96 | shell: bash 97 | run: | 98 | set -e 99 | choco install openssl --version=1.1.1.2100 -y --no-progress 100 | echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV 101 | echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV 102 | 103 | - name: Install Rust (${{ matrix.version.name }}) 104 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 105 | with: 106 | toolchain: ${{ matrix.version.version }} 107 | 108 | - name: Install cargo-hack, cargo-ci-cache-clean, just 109 | uses: taiki-e/install-action@v2.50.4 110 | with: 111 | tool: cargo-hack,cargo-ci-cache-clean,just 112 | 113 | - name: workaround MSRV issues 114 | if: matrix.version.name == 'msrv' 115 | run: just downgrade-for-msrv 116 | 117 | - name: check minimal 118 | run: cargo ci-min 119 | 120 | - name: check minimal + examples 121 | run: cargo ci-check-min-examples 122 | 123 | - name: check default 124 | run: cargo ci-check 125 | 126 | - name: tests 127 | timeout-minutes: 40 128 | run: cargo ci-test --exclude=actix-session --exclude=actix-limitation 129 | 130 | - name: CI cache clean 131 | run: cargo-ci-cache-clean 132 | 133 | doc_tests: 134 | name: Documentation Tests 135 | runs-on: ubuntu-latest 136 | steps: 137 | - uses: actions/checkout@v4 138 | 139 | - name: Install Rust (nightly) 140 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 141 | with: 142 | toolchain: nightly 143 | 144 | - name: Install just 145 | uses: taiki-e/install-action@v2.50.4 146 | with: 147 | tool: just 148 | 149 | - name: Test docs 150 | run: just test-docs 151 | 152 | - name: Build docs 153 | run: just doc 154 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | permissions: { contents: read } 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | coverage: 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | redis: 19 | image: redis:5.0.7 20 | ports: 21 | - 6379:6379 22 | options: --entrypoint redis-server 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Install Rust 28 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 29 | with: 30 | toolchain: nightly 31 | components: llvm-tools-preview 32 | 33 | - name: Install just, cargo-llvm-cov, cargo-nextest 34 | uses: taiki-e/install-action@v2.50.4 35 | with: 36 | tool: just,cargo-llvm-cov,cargo-nextest 37 | 38 | - name: Generate code coverage 39 | run: just test-coverage-codecov 40 | 41 | - name: Upload to Codecov 42 | uses: codecov/codecov-action@v5.4.2 43 | with: 44 | files: codecov.json 45 | fail_ci_if_error: true 46 | env: 47 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | fmt: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Rust (nightly) 19 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 20 | with: 21 | toolchain: nightly 22 | components: rustfmt 23 | 24 | - name: Check with rustfmt 25 | run: cargo fmt --all -- --check 26 | 27 | clippy: 28 | permissions: 29 | contents: read 30 | checks: write # to add clippy checks to PR diffs 31 | 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Install Rust 37 | uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 38 | with: 39 | components: clippy 40 | 41 | - name: Check with Clippy 42 | uses: giraffate/clippy-action@v1.0.1 43 | with: 44 | reporter: github-pr-check 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | clippy_flags: >- 47 | --workspace --all-features --tests --examples --bins -- 48 | -A unknown_lints -D clippy::todo -D clippy::dbg_macro 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | guide/build/ 5 | /gh-pages 6 | 7 | *.so 8 | *.out 9 | *.pyc 10 | *.pid 11 | *.sock 12 | *~ 13 | .DS_Store 14 | 15 | Server.toml 16 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: "*.md" 3 | options: 4 | proseWrap: never 5 | printWidth: 9999 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "actix-cors", 5 | "actix-identity", 6 | "actix-limitation", 7 | "actix-protobuf", 8 | "actix-session", 9 | "actix-settings", 10 | "actix-web-httpauth", 11 | "actix-ws", 12 | ] 13 | 14 | [workspace.package] 15 | repository = "https://github.com/actix/actix-extras" 16 | homepage = "https://actix.rs" 17 | license = "MIT OR Apache-2.0" 18 | edition = "2021" 19 | rust-version = "1.75" 20 | 21 | [workspace.lints.rust] 22 | rust-2018-idioms = { level = "deny" } 23 | nonstandard-style = { level = "deny" } 24 | future-incompatible = { level = "deny" } 25 | 26 | [patch.crates-io] 27 | actix-cors = { path = "./actix-cors" } 28 | actix-identity = { path = "./actix-identity" } 29 | actix-limitation = { path = "./actix-limitation" } 30 | actix-protobuf = { path = "./actix-protobuf" } 31 | actix-session = { path = "./actix-session" } 32 | actix-settings = { path = "./actix-settings" } 33 | actix-web-httpauth = { path = "./actix-web-httpauth" } 34 | 35 | # uncomment to quickly test against local actix-web repo 36 | # actix-http = { path = "../actix-web/actix-http" } 37 | # actix-router = { path = "../actix-web/actix-router" } 38 | # actix-web = { path = "../actix-web" } 39 | # awc = { path = "../actix-web/awc" } 40 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Actix team 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /actix-cors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-cors" 3 | version = "0.7.1" 4 | authors = [ 5 | "Nikolay Kim ", 6 | "Rob Ede ", 7 | ] 8 | description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web" 9 | keywords = ["actix", "cors", "web", "security", "crossorigin"] 10 | repository.workspace = true 11 | homepage.workspace = true 12 | license.workspace = true 13 | edition.workspace = true 14 | rust-version.workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | all-features = true 19 | 20 | [features] 21 | draft-private-network-access = [] 22 | 23 | [dependencies] 24 | actix-utils = "3" 25 | actix-web = { version = "4", default-features = false } 26 | 27 | derive_more = { version = "2", features = ["display", "error"] } 28 | futures-util = { version = "0.3.17", default-features = false, features = ["std"] } 29 | log = "0.4" 30 | once_cell = "1" 31 | smallvec = "1" 32 | 33 | [dev-dependencies] 34 | actix-web = { version = "4", default-features = false, features = ["macros"] } 35 | env_logger = "0.11" 36 | regex = "1.4" 37 | 38 | [lints] 39 | workspace = true 40 | -------------------------------------------------------------------------------- /actix-cors/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-cors/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-cors/README.md: -------------------------------------------------------------------------------- 1 | # actix-cors 2 | 3 | 4 | 5 | [![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors) 6 | [![Documentation](https://docs.rs/actix-cors/badge.svg?version=0.7.1)](https://docs.rs/actix-cors/0.7.1) 7 | ![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) 8 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-cors.svg) 9 |
10 | [![Dependency Status](https://deps.rs/crate/actix-cors/0.7.1/status.svg)](https://deps.rs/crate/actix-cors/0.7.1) 11 | [![Download](https://img.shields.io/crates/d/actix-cors.svg)](https://crates.io/crates/actix-cors) 12 | [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) 13 | 14 | 15 | 16 | 17 | 18 | Cross-Origin Resource Sharing (CORS) controls for Actix Web. 19 | 20 | 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. 21 | 22 | This CORS middleware automatically handles `OPTIONS` preflight requests. 23 | 24 | ## Crate Features 25 | 26 | - `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. 27 | 28 | ## Example 29 | 30 | ```rust 31 | use actix_cors::Cors; 32 | use actix_web::{get, http, web, App, HttpRequest, HttpResponse, HttpServer}; 33 | 34 | #[get("/index.html")] 35 | async fn index(req: HttpRequest) -> &'static str { 36 | "

Hello World!

" 37 | } 38 | 39 | #[actix_web::main] 40 | async fn main() -> std::io::Result<()> { 41 | HttpServer::new(|| { 42 | let cors = Cors::default() 43 | .allowed_origin("https://www.rust-lang.org") 44 | .allowed_origin_fn(|origin, _req_head| { 45 | origin.as_bytes().ends_with(b".rust-lang.org") 46 | }) 47 | .allowed_methods(vec!["GET", "POST"]) 48 | .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) 49 | .allowed_header(http::header::CONTENT_TYPE) 50 | .max_age(3600); 51 | 52 | App::new() 53 | .wrap(cors) 54 | .service(index) 55 | }) 56 | .bind(("127.0.0.1", 8080))? 57 | .run() 58 | .await; 59 | 60 | Ok(()) 61 | } 62 | ``` 63 | 64 | [Private Network Access]: https://wicg.github.io/private-network-access 65 | 66 | 67 | 68 | ## Documentation & Resources 69 | 70 | - [API Documentation](https://docs.rs/actix-cors) 71 | - [Example Project](https://github.com/actix/examples/tree/master/cors) 72 | - Minimum Supported Rust Version (MSRV): 1.75 73 | -------------------------------------------------------------------------------- /actix-cors/examples/cors.rs: -------------------------------------------------------------------------------- 1 | use actix_cors::Cors; 2 | use actix_web::{http::header, middleware::Logger, web, App, HttpServer}; 3 | 4 | #[actix_web::main] 5 | async fn main() -> std::io::Result<()> { 6 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 7 | 8 | log::info!("starting HTTP server at http://localhost:8080"); 9 | 10 | HttpServer::new(move || { 11 | App::new() 12 | // `permissive` is a wide-open development config 13 | // .wrap(Cors::permissive()) 14 | .wrap( 15 | // default settings are overly restrictive to reduce chance of 16 | // misconfiguration leading to security concerns 17 | Cors::default() 18 | // add specific origin to allowed origin list 19 | .allowed_origin("http://project.local:8080") 20 | // allow any port on localhost 21 | .allowed_origin_fn(|origin, _req_head| { 22 | origin.as_bytes().starts_with(b"http://localhost") 23 | 24 | // manual alternative: 25 | // unwrapping is acceptable on the origin header since this function is 26 | // only called when it exists 27 | // req_head 28 | // .headers() 29 | // .get(header::ORIGIN) 30 | // .unwrap() 31 | // .as_bytes() 32 | // .starts_with(b"http://localhost") 33 | }) 34 | // set allowed methods list 35 | .allowed_methods(vec!["GET", "POST"]) 36 | // set allowed request header list 37 | .allowed_headers(&[header::AUTHORIZATION, header::ACCEPT]) 38 | // add header to allowed list 39 | .allowed_header(header::CONTENT_TYPE) 40 | // set list of headers that are safe to expose 41 | .expose_headers(&[header::CONTENT_DISPOSITION]) 42 | // allow cURL/HTTPie from working without providing Origin headers 43 | .block_on_origin_mismatch(false) 44 | // set preflight cache TTL 45 | .max_age(3600), 46 | ) 47 | .wrap(Logger::default()) 48 | .default_service(web::to(|| async { "Hello, cross-origin world!" })) 49 | }) 50 | .workers(1) 51 | .bind(("127.0.0.1", 8080))? 52 | .run() 53 | .await 54 | } 55 | -------------------------------------------------------------------------------- /actix-cors/src/all_or_some.rs: -------------------------------------------------------------------------------- 1 | /// An enum signifying that some of type `T` is allowed, or `All` (anything is allowed). 2 | #[derive(Debug, Clone, PartialEq, Eq)] 3 | pub enum AllOrSome { 4 | /// Everything is allowed. Usually equivalent to the `*` value. 5 | All, 6 | 7 | /// Only some of `T` is allowed 8 | Some(T), 9 | } 10 | 11 | /// Default as `AllOrSome::All`. 12 | impl Default for AllOrSome { 13 | fn default() -> Self { 14 | AllOrSome::All 15 | } 16 | } 17 | 18 | impl AllOrSome { 19 | /// Returns whether this is an `All` variant. 20 | pub fn is_all(&self) -> bool { 21 | matches!(self, AllOrSome::All) 22 | } 23 | 24 | /// Returns whether this is a `Some` variant. 25 | #[allow(dead_code)] 26 | pub fn is_some(&self) -> bool { 27 | !self.is_all() 28 | } 29 | 30 | /// Provides a shared reference to `T` if variant is `Some`. 31 | pub fn as_ref(&self) -> Option<&T> { 32 | match *self { 33 | AllOrSome::All => None, 34 | AllOrSome::Some(ref t) => Some(t), 35 | } 36 | } 37 | 38 | /// Provides a mutable reference to `T` if variant is `Some`. 39 | pub fn as_mut(&mut self) -> Option<&mut T> { 40 | match *self { 41 | AllOrSome::All => None, 42 | AllOrSome::Some(ref mut t) => Some(t), 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | #[test] 49 | fn tests() { 50 | assert!(AllOrSome::<()>::All.is_all()); 51 | assert!(!AllOrSome::<()>::All.is_some()); 52 | 53 | assert!(!AllOrSome::Some(()).is_all()); 54 | assert!(AllOrSome::Some(()).is_some()); 55 | } 56 | -------------------------------------------------------------------------------- /actix-cors/src/error.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{http::StatusCode, HttpResponse, ResponseError}; 2 | use derive_more::derive::{Display, Error}; 3 | 4 | /// Errors that can occur when processing CORS guarded requests. 5 | #[derive(Debug, Clone, Display, Error)] 6 | #[non_exhaustive] 7 | pub enum CorsError { 8 | /// Allowed origin argument must not be wildcard (`*`). 9 | #[display("`allowed_origin` argument must not be wildcard (`*`)")] 10 | WildcardOrigin, 11 | 12 | /// Request header `Origin` is required but was not provided. 13 | #[display("Request header `Origin` is required but was not provided")] 14 | MissingOrigin, 15 | 16 | /// Request header `Access-Control-Request-Method` is required but is missing. 17 | #[display("Request header `Access-Control-Request-Method` is required but is missing")] 18 | MissingRequestMethod, 19 | 20 | /// Request header `Access-Control-Request-Method` has an invalid value. 21 | #[display("Request header `Access-Control-Request-Method` has an invalid value")] 22 | BadRequestMethod, 23 | 24 | /// Request header `Access-Control-Request-Headers` has an invalid value. 25 | #[display("Request header `Access-Control-Request-Headers` has an invalid value")] 26 | BadRequestHeaders, 27 | 28 | /// Origin is not allowed to make this request. 29 | #[display("Origin is not allowed to make this request")] 30 | OriginNotAllowed, 31 | 32 | /// Request method is not allowed. 33 | #[display("Requested method is not allowed")] 34 | MethodNotAllowed, 35 | 36 | /// One or more request headers are not allowed. 37 | #[display("One or more request headers are not allowed")] 38 | HeadersNotAllowed, 39 | } 40 | 41 | impl ResponseError for CorsError { 42 | fn status_code(&self) -> StatusCode { 43 | StatusCode::BAD_REQUEST 44 | } 45 | 46 | fn error_response(&self) -> HttpResponse { 47 | HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /actix-cors/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Cross-Origin Resource Sharing (CORS) controls for Actix Web. 2 | //! 3 | //! This middleware can be applied to both applications and resources. Once built, a [`Cors`] 4 | //! builder can be used as an argument for Actix Web's `App::wrap()`, `Scope::wrap()`, or 5 | //! `Resource::wrap()` methods. 6 | //! 7 | //! This CORS middleware automatically handles `OPTIONS` preflight requests. 8 | //! 9 | //! # Crate Features 10 | //! - `draft-private-network-access`: ⚠️ Unstable. Adds opt-in support for the [Private Network 11 | //! Access] spec extensions. This feature is unstable since it will follow breaking changes in the 12 | //! draft spec until it is finalized. 13 | //! 14 | //! # Example 15 | //! ```no_run 16 | //! use actix_cors::Cors; 17 | //! use actix_web::{get, http, web, App, HttpRequest, HttpResponse, HttpServer}; 18 | //! 19 | //! #[get("/index.html")] 20 | //! async fn index(req: HttpRequest) -> &'static str { 21 | //! "

Hello World!

" 22 | //! } 23 | //! 24 | //! #[actix_web::main] 25 | //! async fn main() -> std::io::Result<()> { 26 | //! HttpServer::new(|| { 27 | //! let cors = Cors::default() 28 | //! .allowed_origin("https://www.rust-lang.org") 29 | //! .allowed_origin_fn(|origin, _req_head| { 30 | //! origin.as_bytes().ends_with(b".rust-lang.org") 31 | //! }) 32 | //! .allowed_methods(vec!["GET", "POST"]) 33 | //! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) 34 | //! .allowed_header(http::header::CONTENT_TYPE) 35 | //! .max_age(3600); 36 | //! 37 | //! App::new() 38 | //! .wrap(cors) 39 | //! .service(index) 40 | //! }) 41 | //! .bind(("127.0.0.1", 8080))? 42 | //! .run() 43 | //! .await; 44 | //! 45 | //! Ok(()) 46 | //! } 47 | //! ``` 48 | //! 49 | //! [Private Network Access]: https://wicg.github.io/private-network-access 50 | 51 | #![forbid(unsafe_code)] 52 | #![warn(future_incompatible, missing_docs, missing_debug_implementations)] 53 | #![doc(html_logo_url = "https://actix.rs/img/logo.png")] 54 | #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] 55 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 56 | 57 | mod all_or_some; 58 | mod builder; 59 | mod error; 60 | mod inner; 61 | mod middleware; 62 | 63 | use crate::{ 64 | all_or_some::AllOrSome, 65 | inner::{Inner, OriginFn}, 66 | }; 67 | pub use crate::{builder::Cors, error::CorsError, middleware::CorsMiddleware}; 68 | -------------------------------------------------------------------------------- /actix-identity/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-identity" 3 | version = "0.8.0" 4 | authors = [ 5 | "Nikolay Kim ", 6 | "Luca Palmieri ", 7 | ] 8 | description = "Identity management for Actix Web" 9 | keywords = ["actix", "auth", "identity", "web", "security"] 10 | repository.workspace = true 11 | homepage.workspace = true 12 | license.workspace = true 13 | edition.workspace = true 14 | rust-version.workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | all-features = true 19 | 20 | [dependencies] 21 | actix-service = "2" 22 | actix-session = "0.10" 23 | actix-utils = "3" 24 | actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] } 25 | 26 | derive_more = { version = "2", features = ["display", "error", "from"] } 27 | futures-core = "0.3.17" 28 | serde = { version = "1", features = ["derive"] } 29 | tracing = { version = "0.1.30", default-features = false, features = ["log"] } 30 | 31 | [dev-dependencies] 32 | actix-http = "3" 33 | actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] } 34 | actix-session = { version = "0.10", features = ["redis-session", "cookie-session"] } 35 | 36 | env_logger = "0.11" 37 | reqwest = { version = "0.12", default-features = false, features = ["cookies", "json"] } 38 | uuid = { version = "1", features = ["v4"] } 39 | 40 | [lints] 41 | workspace = true 42 | -------------------------------------------------------------------------------- /actix-identity/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-identity/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-identity/README.md: -------------------------------------------------------------------------------- 1 | # actix-identity 2 | 3 | > Identity management for Actix Web. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity) 8 | [![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.8.0)](https://docs.rs/actix-identity/0.8.0) 9 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity) 10 | [![Dependency Status](https://deps.rs/crate/actix-identity/0.8.0/status.svg)](https://deps.rs/crate/actix-identity/0.8.0) 11 | 12 | 13 | 14 | 15 | 16 | Identity management for Actix Web. 17 | 18 | `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). 19 | 20 | ## Getting started 21 | 22 | To start using identity management in your Actix Web application you must register [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`: 23 | 24 | ```rust 25 | use actix_web::{cookie::Key, App, HttpServer, HttpResponse}; 26 | use actix_identity::IdentityMiddleware; 27 | use actix_session::{storage::RedisSessionStore, SessionMiddleware}; 28 | 29 | #[actix_web::main] 30 | async fn main() { 31 | // When using `Key::generate()` it is important to initialize outside of the 32 | // `HttpServer::new` closure. When deployed the secret key should be read from a 33 | // configuration file or environment variables. 34 | let secret_key = Key::generate(); 35 | 36 | let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379") 37 | .await 38 | .unwrap(); 39 | 40 | HttpServer::new(move || { 41 | App::new() 42 | // Install the identity framework first. 43 | .wrap(IdentityMiddleware::default()) 44 | // The identity system is built on top of sessions. You must install the session 45 | // middleware to leverage `actix-identity`. The session middleware must be mounted 46 | // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE 47 | // order of registration when it receives an incoming request. 48 | .wrap(SessionMiddleware::new( 49 | redis_store.clone(), 50 | secret_key.clone(), 51 | )) 52 | // Your request handlers [...] 53 | }) 54 | } 55 | ``` 56 | 57 | User identities can be created, accessed and destroyed using the [`Identity`] extractor in your request handlers: 58 | 59 | ```rust 60 | use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage}; 61 | use actix_identity::Identity; 62 | use actix_session::storage::RedisSessionStore; 63 | 64 | #[get("/")] 65 | async fn index(user: Option) -> impl Responder { 66 | if let Some(user) = user { 67 | format!("Welcome! {}", user.id().unwrap()) 68 | } else { 69 | "Welcome Anonymous!".to_owned() 70 | } 71 | } 72 | 73 | #[post("/login")] 74 | async fn login(request: HttpRequest) -> impl Responder { 75 | // Some kind of authentication should happen here 76 | // e.g. password-based, biometric, etc. 77 | // [...] 78 | 79 | // attach a verified user identity to the active session 80 | Identity::login(&request.extensions(), "User1".into()).unwrap(); 81 | 82 | HttpResponse::Ok() 83 | } 84 | 85 | #[post("/logout")] 86 | async fn logout(user: Option) -> impl Responder { 87 | if let Some(user) = user { 88 | user.logout(); 89 | } 90 | HttpResponse::Ok() 91 | } 92 | ``` 93 | 94 | ## Advanced configuration 95 | 96 | By default, `actix-identity` does not automatically log out users. You can change this behaviour by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`]. 97 | 98 | In particular, you can automatically log out users who: 99 | 100 | - have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]); 101 | - logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]). 102 | 103 | [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline 104 | [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline 105 | 106 | 107 | -------------------------------------------------------------------------------- /actix-identity/examples/identity.rs: -------------------------------------------------------------------------------- 1 | //! A rudimentary example of how to set up and use `actix-identity`. 2 | //! 3 | //! ```bash 4 | //! # using HTTPie (https://httpie.io/cli) 5 | //! 6 | //! # outputs "Welcome Anonymous!" message 7 | //! http -v --session=identity GET localhost:8080/ 8 | //! 9 | //! # log in using fake details, ensuring that --session is used to persist cookies 10 | //! http -v --session=identity POST localhost:8080/login user_id=foo 11 | //! 12 | //! # outputs "Welcome User1" message 13 | //! http -v --session=identity GET localhost:8080/ 14 | //! ``` 15 | 16 | use std::{io, time::Duration}; 17 | 18 | use actix_identity::{Identity, IdentityMiddleware}; 19 | use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware}; 20 | use actix_web::{ 21 | cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse, 22 | HttpServer, Responder, 23 | }; 24 | 25 | #[actix_web::main] 26 | async fn main() -> io::Result<()> { 27 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 28 | 29 | let secret_key = Key::generate(); 30 | 31 | let expiration = Duration::from_secs(24 * 60 * 60); 32 | 33 | HttpServer::new(move || { 34 | let session_mw = 35 | SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) 36 | // disable secure cookie for local testing 37 | .cookie_secure(false) 38 | // Set a ttl for the cookie if the identity should live longer than the user session 39 | .session_lifecycle( 40 | PersistentSession::default().session_ttl(expiration.try_into().unwrap()), 41 | ) 42 | .build(); 43 | let identity_mw = IdentityMiddleware::builder() 44 | .visit_deadline(Some(expiration)) 45 | .build(); 46 | 47 | App::new() 48 | // Install the identity framework first. 49 | .wrap(identity_mw) 50 | // The identity system is built on top of sessions. You must install the session 51 | // middleware to leverage `actix-identity`. The session middleware must be mounted 52 | // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE 53 | // order of registration when it receives an incoming request. 54 | .wrap(session_mw) 55 | .wrap(Logger::default()) 56 | .service(index) 57 | .service(login) 58 | .service(logout) 59 | }) 60 | .bind(("127.0.0.1", 8080)) 61 | .unwrap() 62 | .workers(2) 63 | .run() 64 | .await 65 | } 66 | 67 | #[get("/")] 68 | async fn index(user: Option) -> impl Responder { 69 | if let Some(user) = user { 70 | format!("Welcome! {}", user.id().unwrap()) 71 | } else { 72 | "Welcome Anonymous!".to_owned() 73 | } 74 | } 75 | 76 | #[post("/login")] 77 | async fn login(request: HttpRequest) -> impl Responder { 78 | // Some kind of authentication should happen here - 79 | // e.g. password-based, biometric, etc. 80 | // [...] 81 | 82 | // Attached a verified user identity to the active 83 | // session. 84 | Identity::login(&request.extensions(), "User1".into()).unwrap(); 85 | 86 | HttpResponse::Ok() 87 | } 88 | 89 | #[post("/logout")] 90 | async fn logout(user: Identity) -> impl Responder { 91 | user.logout(); 92 | HttpResponse::NoContent() 93 | } 94 | -------------------------------------------------------------------------------- /actix-identity/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration options to tune the behaviour of [`IdentityMiddleware`]. 2 | 3 | use std::time::Duration; 4 | 5 | use crate::IdentityMiddleware; 6 | 7 | #[derive(Debug, Clone)] 8 | pub(crate) struct Configuration { 9 | pub(crate) on_logout: LogoutBehaviour, 10 | pub(crate) login_deadline: Option, 11 | pub(crate) visit_deadline: Option, 12 | pub(crate) id_key: &'static str, 13 | pub(crate) last_visit_unix_timestamp_key: &'static str, 14 | pub(crate) login_unix_timestamp_key: &'static str, 15 | } 16 | 17 | impl Default for Configuration { 18 | fn default() -> Self { 19 | Self { 20 | on_logout: LogoutBehaviour::PurgeSession, 21 | login_deadline: None, 22 | visit_deadline: None, 23 | id_key: "actix_identity.user_id", 24 | last_visit_unix_timestamp_key: "actix_identity.last_visited_at", 25 | login_unix_timestamp_key: "actix_identity.logged_in_at", 26 | } 27 | } 28 | } 29 | 30 | /// `LogoutBehaviour` controls what actions are going to be performed when [`Identity::logout`] is 31 | /// invoked. 32 | /// 33 | /// [`Identity::logout`]: crate::Identity::logout 34 | #[derive(Debug, Clone)] 35 | #[non_exhaustive] 36 | pub enum LogoutBehaviour { 37 | /// When [`Identity::logout`](crate::Identity::logout) is called, purge the current session. 38 | /// 39 | /// This behaviour might be desirable when you have stored additional information in the 40 | /// session state that are tied to the user's identity and should not be retained after logout. 41 | PurgeSession, 42 | 43 | /// When [`Identity::logout`](crate::Identity::logout) is called, remove the identity 44 | /// information from the current session state. The session itself is not destroyed. 45 | /// 46 | /// This behaviour might be desirable when you have stored information in the session state that 47 | /// is not tied to the user's identity and should be retained after logout. 48 | DeleteIdentityKeys, 49 | } 50 | 51 | /// A fluent builder to construct an [`IdentityMiddleware`] instance with custom configuration 52 | /// parameters. 53 | /// 54 | /// Use [`IdentityMiddleware::builder`] to get started! 55 | #[derive(Debug, Clone)] 56 | pub struct IdentityMiddlewareBuilder { 57 | configuration: Configuration, 58 | } 59 | 60 | impl IdentityMiddlewareBuilder { 61 | pub(crate) fn new() -> Self { 62 | Self { 63 | configuration: Configuration::default(), 64 | } 65 | } 66 | 67 | /// Set a custom key to identify the user in the session. 68 | pub fn id_key(mut self, key: &'static str) -> Self { 69 | self.configuration.id_key = key; 70 | self 71 | } 72 | 73 | /// Set a custom key to store the last visited unix timestamp. 74 | pub fn last_visit_unix_timestamp_key(mut self, key: &'static str) -> Self { 75 | self.configuration.last_visit_unix_timestamp_key = key; 76 | self 77 | } 78 | 79 | /// Set a custom key to store the login unix timestamp. 80 | pub fn login_unix_timestamp_key(mut self, key: &'static str) -> Self { 81 | self.configuration.login_unix_timestamp_key = key; 82 | self 83 | } 84 | 85 | /// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session. 86 | /// 87 | /// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]). 88 | pub fn logout_behaviour(mut self, logout_behaviour: LogoutBehaviour) -> Self { 89 | self.configuration.on_logout = logout_behaviour; 90 | self 91 | } 92 | 93 | /// Automatically logs out users after a certain amount of time has passed since they logged in, 94 | /// regardless of their activity pattern. 95 | /// 96 | /// If set to: 97 | /// - `None`: login deadline is disabled. 98 | /// - `Some(duration)`: login deadline is enabled and users will be logged out after `duration` 99 | /// has passed since their login. 100 | /// 101 | /// By default, login deadline is disabled. 102 | pub fn login_deadline(mut self, deadline: Option) -> Self { 103 | self.configuration.login_deadline = deadline; 104 | self 105 | } 106 | 107 | /// Automatically logs out users after a certain amount of time has passed since their last 108 | /// visit. 109 | /// 110 | /// If set to: 111 | /// - `None`: visit deadline is disabled. 112 | /// - `Some(duration)`: visit deadline is enabled and users will be logged out after `duration` 113 | /// has passed since their last visit. 114 | /// 115 | /// By default, visit deadline is disabled. 116 | pub fn visit_deadline(mut self, deadline: Option) -> Self { 117 | self.configuration.visit_deadline = deadline; 118 | self 119 | } 120 | 121 | /// Finalises the builder and returns an [`IdentityMiddleware`] instance. 122 | pub fn build(self) -> IdentityMiddleware { 123 | IdentityMiddleware::new(self.configuration) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /actix-identity/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Failure modes of identity operations. 2 | 3 | use actix_session::{SessionGetError, SessionInsertError}; 4 | use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError}; 5 | use derive_more::derive::{Display, Error, From}; 6 | 7 | /// Error that can occur during login attempts. 8 | #[derive(Debug, Display, Error, From)] 9 | #[display("{_0}")] 10 | pub struct LoginError(SessionInsertError); 11 | 12 | impl ResponseError for LoginError { 13 | fn status_code(&self) -> StatusCode { 14 | StatusCode::UNAUTHORIZED 15 | } 16 | } 17 | 18 | /// Error encountered when working with a session that has expired. 19 | #[derive(Debug, Display, Error)] 20 | #[display("The given session has expired and is no longer valid")] 21 | pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange); 22 | 23 | /// The identity information has been lost. 24 | /// 25 | /// Seeing this error in user code indicates a bug in actix-identity. 26 | #[derive(Debug, Display, Error)] 27 | #[display( 28 | "The identity information in the current session has disappeared after having been \ 29 | successfully validated. This is likely to be a bug." 30 | )] 31 | #[non_exhaustive] 32 | pub struct LostIdentityError; 33 | 34 | /// There is no identity information attached to the current session. 35 | #[derive(Debug, Display, Error)] 36 | #[display("There is no identity information attached to the current session")] 37 | #[non_exhaustive] 38 | pub struct MissingIdentityError; 39 | 40 | /// Errors that can occur while retrieving an identity. 41 | #[derive(Debug, Display, Error, From)] 42 | #[non_exhaustive] 43 | pub enum GetIdentityError { 44 | /// The session has expired. 45 | #[display("{_0}")] 46 | SessionExpiryError(SessionExpiryError), 47 | 48 | /// No identity is found in a session. 49 | #[display("{_0}")] 50 | MissingIdentityError(MissingIdentityError), 51 | 52 | /// Failed to accessing the session store. 53 | #[display("{_0}")] 54 | SessionGetError(SessionGetError), 55 | 56 | /// Identity info was lost after being validated. 57 | /// 58 | /// Seeing this error indicates a bug in actix-identity. 59 | #[display("{_0}")] 60 | LostIdentityError(LostIdentityError), 61 | } 62 | 63 | impl ResponseError for GetIdentityError { 64 | fn status_code(&self) -> StatusCode { 65 | match self { 66 | Self::LostIdentityError(_) => StatusCode::INTERNAL_SERVER_ERROR, 67 | _ => StatusCode::UNAUTHORIZED, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /actix-identity/src/identity_ext.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{dev::ServiceRequest, guard::GuardContext, HttpMessage, HttpRequest}; 2 | 3 | use crate::{error::GetIdentityError, Identity}; 4 | 5 | /// Helper trait to retrieve an [`Identity`] instance from various `actix-web`'s types. 6 | pub trait IdentityExt { 7 | /// Retrieve the identity attached to the current session, if available. 8 | fn get_identity(&self) -> Result; 9 | } 10 | 11 | impl IdentityExt for HttpRequest { 12 | fn get_identity(&self) -> Result { 13 | Identity::extract(&self.extensions()) 14 | } 15 | } 16 | 17 | impl IdentityExt for ServiceRequest { 18 | fn get_identity(&self) -> Result { 19 | Identity::extract(&self.extensions()) 20 | } 21 | } 22 | 23 | impl IdentityExt for GuardContext<'_> { 24 | fn get_identity(&self) -> Result { 25 | Identity::extract(&self.req_data()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /actix-identity/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Identity management for Actix Web. 3 | 4 | `actix-identity` can be used to track identity of a user across multiple requests. It is built 5 | on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session). 6 | 7 | # Getting started 8 | To start using identity management in your Actix Web application you must register 9 | [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`: 10 | 11 | ```no_run 12 | # use actix_web::web; 13 | use actix_web::{cookie::Key, App, HttpServer, HttpResponse}; 14 | use actix_identity::IdentityMiddleware; 15 | use actix_session::{storage::RedisSessionStore, SessionMiddleware}; 16 | 17 | #[actix_web::main] 18 | async fn main() { 19 | // When using `Key::generate()` it is important to initialize outside of the 20 | // `HttpServer::new` closure. When deployed the secret key should be read from a 21 | // configuration file or environment variables. 22 | let secret_key = Key::generate(); 23 | 24 | let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379") 25 | .await 26 | .unwrap(); 27 | 28 | HttpServer::new(move || { 29 | App::new() 30 | // Install the identity framework first. 31 | .wrap(IdentityMiddleware::default()) 32 | // The identity system is built on top of sessions. You must install the session 33 | // middleware to leverage `actix-identity`. The session middleware must be mounted 34 | // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE 35 | // order of registration when it receives an incoming request. 36 | .wrap(SessionMiddleware::new( 37 | redis_store.clone(), 38 | secret_key.clone(), 39 | )) 40 | // Your request handlers [...] 41 | # .default_service(web::to(|| HttpResponse::Ok())) 42 | }) 43 | # ; 44 | } 45 | ``` 46 | 47 | User identities can be created, accessed and destroyed using the [`Identity`] extractor in your 48 | request handlers: 49 | 50 | ```no_run 51 | use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage}; 52 | use actix_identity::Identity; 53 | use actix_session::storage::RedisSessionStore; 54 | 55 | #[get("/")] 56 | async fn index(user: Option) -> impl Responder { 57 | if let Some(user) = user { 58 | format!("Welcome! {}", user.id().unwrap()) 59 | } else { 60 | "Welcome Anonymous!".to_owned() 61 | } 62 | } 63 | 64 | #[post("/login")] 65 | async fn login(request: HttpRequest) -> impl Responder { 66 | // Some kind of authentication should happen here 67 | // e.g. password-based, biometric, etc. 68 | // [...] 69 | 70 | // attach a verified user identity to the active session 71 | Identity::login(&request.extensions(), "User1".into()).unwrap(); 72 | 73 | HttpResponse::Ok() 74 | } 75 | 76 | #[post("/logout")] 77 | async fn logout(user: Option) -> impl Responder { 78 | if let Some(user) = user { 79 | user.logout(); 80 | } 81 | HttpResponse::Ok() 82 | } 83 | ``` 84 | 85 | # Advanced configuration 86 | By default, `actix-identity` does not automatically log out users. You can change this behaviour 87 | by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`]. 88 | 89 | In particular, you can automatically log out users who: 90 | - have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]); 91 | - logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]). 92 | 93 | [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline 94 | [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline 95 | */ 96 | 97 | #![forbid(unsafe_code)] 98 | #![deny(missing_docs)] 99 | #![doc(html_logo_url = "https://actix.rs/img/logo.png")] 100 | #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] 101 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 102 | 103 | pub mod config; 104 | pub mod error; 105 | mod identity; 106 | mod identity_ext; 107 | mod middleware; 108 | 109 | pub use self::{identity::Identity, identity_ext::IdentityExt, middleware::IdentityMiddleware}; 110 | -------------------------------------------------------------------------------- /actix-identity/tests/integration/fixtures.rs: -------------------------------------------------------------------------------- 1 | use actix_session::{storage::CookieSessionStore, SessionMiddleware}; 2 | use actix_web::cookie::Key; 3 | use uuid::Uuid; 4 | 5 | pub fn store() -> CookieSessionStore { 6 | CookieSessionStore::default() 7 | } 8 | 9 | pub fn user_id() -> String { 10 | Uuid::new_v4().to_string() 11 | } 12 | 13 | pub fn session_middleware() -> SessionMiddleware { 14 | SessionMiddleware::builder(store(), Key::generate()) 15 | .cookie_domain(Some("localhost".into())) 16 | .build() 17 | } 18 | -------------------------------------------------------------------------------- /actix-identity/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | pub mod fixtures; 2 | mod integration; 3 | pub mod test_app; 4 | -------------------------------------------------------------------------------- /actix-limitation/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | - Update `redis` dependency to `0.29`. 6 | - Update `actix-session` dependency to `0.9`. 7 | 8 | ## 0.5.1 9 | 10 | - No significant changes since `0.5.0`. 11 | 12 | ## 0.5.0 13 | 14 | - Update `redis` dependency to `0.23`. 15 | - Update `actix-session` dependency to `0.8`. 16 | 17 | ## 0.4.0 18 | 19 | - Add `Builder::key_by` for setting a custom rate limit key function. 20 | - Implement `Default` for `RateLimiter`. 21 | - `RateLimiter` is marked `#[non_exhaustive]`; use `RateLimiter::default()` instead. 22 | - 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`. 23 | - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. 24 | 25 | ## 0.3.0 26 | 27 | - `Limiter::builder` now takes an `impl Into`. 28 | - Removed lifetime from `Builder`. 29 | - Updated `actix-session` dependency to `0.7`. 30 | 31 | ## 0.2.0 32 | 33 | - Update Actix Web dependency to v4 ecosystem. 34 | - Update Tokio dependencies to v1 ecosystem. 35 | - Rename `Limiter::{build => builder}()`. 36 | - Rename `Builder::{finish => build}()`. 37 | - Exceeding the rate limit now returns a 429 Too Many Requests response. 38 | 39 | ## 0.1.4 40 | 41 | - Adopted into @actix org from . 42 | -------------------------------------------------------------------------------- /actix-limitation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-limitation" 3 | version = "0.5.1" 4 | authors = [ 5 | "0xmad <0xmad@users.noreply.github.com>", 6 | "Rob Ede ", 7 | ] 8 | description = "Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web" 9 | keywords = ["actix-web", "rate-api", "rate-limit", "limitation"] 10 | categories = ["asynchronous", "web-programming"] 11 | repository = "https://github.com/actix/actix-extras" 12 | license.workspace = true 13 | edition.workspace = true 14 | rust-version.workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | all-features = true 19 | 20 | [features] 21 | default = ["session"] 22 | session = ["actix-session"] 23 | 24 | [dependencies] 25 | actix-utils = "3" 26 | actix-web = { version = "4", default-features = false, features = ["cookies"] } 27 | 28 | chrono = "0.4" 29 | derive_more = { version = "2", features = ["display", "error", "from"] } 30 | log = "0.4" 31 | redis = { version = "0.29", default-features = false, features = ["tokio-comp"] } 32 | time = "0.3" 33 | 34 | # session 35 | actix-session = { version = "0.10", optional = true } 36 | 37 | [dev-dependencies] 38 | actix-web = "4" 39 | static_assertions = "1" 40 | uuid = { version = "1", features = ["v4"] } 41 | 42 | [lints] 43 | workspace = true 44 | -------------------------------------------------------------------------------- /actix-limitation/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-limitation/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-limitation/README.md: -------------------------------------------------------------------------------- 1 | # actix-limitation 2 | 3 | > Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web. 4 | > Originally based on . 5 | 6 | 7 | 8 | [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation) 9 | [![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.5.1)](https://docs.rs/actix-limitation/0.5.1) 10 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation) 11 | [![Dependency Status](https://deps.rs/crate/actix-limitation/0.5.1/status.svg)](https://deps.rs/crate/actix-limitation/0.5.1) 12 | 13 | 14 | 15 | ## Examples 16 | 17 | ```toml 18 | [dependencies] 19 | actix-web = "4" 20 | actix-limitation = "0.5" 21 | ``` 22 | 23 | ```rust 24 | use actix_limitation::{Limiter, RateLimiter}; 25 | use actix_session::SessionExt as _; 26 | use actix_web::{dev::ServiceRequest, get, web, App, HttpServer, Responder}; 27 | use std::{sync::Arc, time::Duration}; 28 | 29 | #[get("/{id}/{name}")] 30 | async fn index(info: web::Path<(u32, String)>) -> impl Responder { 31 | format!("Hello {}! id:{}", info.1, info.0) 32 | } 33 | 34 | #[actix_web::main] 35 | async fn main() -> std::io::Result<()> { 36 | let limiter = web::Data::new( 37 | Limiter::builder("redis://127.0.0.1") 38 | .key_by(|req: &ServiceRequest| { 39 | req.get_session() 40 | .get(&"session-id") 41 | .unwrap_or_else(|_| req.cookie(&"rate-api-id").map(|c| c.to_string())) 42 | }) 43 | .limit(5000) 44 | .period(Duration::from_secs(3600)) // 60 minutes 45 | .build() 46 | .unwrap(), 47 | ); 48 | HttpServer::new(move || { 49 | App::new() 50 | .wrap(RateLimiter::default()) 51 | .app_data(limiter.clone()) 52 | .service(index) 53 | }) 54 | .bind(("127.0.0.1", 8080))? 55 | .run() 56 | .await 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /actix-limitation/src/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::derive::{Display, Error, From}; 2 | 3 | use crate::status::Status; 4 | 5 | /// Failure modes of the rate limiter. 6 | #[derive(Debug, Display, Error, From)] 7 | pub enum Error { 8 | /// Redis client failed to connect or run a query. 9 | #[display("Redis client failed to connect or run a query")] 10 | Client(redis::RedisError), 11 | 12 | /// Limit is exceeded for a key. 13 | #[display("Limit is exceeded for a key")] 14 | #[from(ignore)] 15 | LimitExceeded(#[error(not(source))] Status), 16 | 17 | /// Time conversion failed. 18 | #[display("Time conversion failed")] 19 | Time(time::error::ComponentRange), 20 | 21 | /// Generic error. 22 | #[display("Generic error")] 23 | #[from(ignore)] 24 | Other(#[error(not(source))] String), 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | static_assertions::assert_impl_all! { 32 | Error: 33 | From, 34 | From, 35 | } 36 | 37 | static_assertions::assert_not_impl_any! { 38 | Error: 39 | From, 40 | From, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /actix-limitation/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, pin::Pin, rc::Rc}; 2 | 3 | use actix_utils::future::{ok, Ready}; 4 | use actix_web::{ 5 | body::EitherBody, 6 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, 7 | http::StatusCode, 8 | web, Error, HttpResponse, 9 | }; 10 | 11 | use crate::{Error as LimitationError, Limiter}; 12 | 13 | /// Rate limit middleware. 14 | #[derive(Debug, Default)] 15 | #[non_exhaustive] 16 | pub struct RateLimiter; 17 | 18 | impl Transform for RateLimiter 19 | where 20 | S: Service, Error = Error> + 'static, 21 | S::Future: 'static, 22 | B: 'static, 23 | { 24 | type Response = ServiceResponse>; 25 | type Error = Error; 26 | type Transform = RateLimiterMiddleware; 27 | type InitError = (); 28 | type Future = Ready>; 29 | 30 | fn new_transform(&self, service: S) -> Self::Future { 31 | ok(RateLimiterMiddleware { 32 | service: Rc::new(service), 33 | }) 34 | } 35 | } 36 | 37 | /// Rate limit middleware service. 38 | #[derive(Debug)] 39 | pub struct RateLimiterMiddleware { 40 | service: Rc, 41 | } 42 | 43 | impl Service for RateLimiterMiddleware 44 | where 45 | S: Service, Error = Error> + 'static, 46 | S::Future: 'static, 47 | B: 'static, 48 | { 49 | type Response = ServiceResponse>; 50 | type Error = Error; 51 | type Future = Pin>>>; 52 | 53 | forward_ready!(service); 54 | 55 | fn call(&self, req: ServiceRequest) -> Self::Future { 56 | // A misconfiguration of the Actix App will result in a **runtime** failure, so the expect 57 | // method description is important context for the developer. 58 | let limiter = req 59 | .app_data::>() 60 | .expect("web::Data should be set in app data for RateLimiter middleware") 61 | .clone(); 62 | 63 | let key = (limiter.get_key_fn)(&req); 64 | let service = Rc::clone(&self.service); 65 | 66 | let key = match key { 67 | Some(key) => key, 68 | None => { 69 | return Box::pin(async move { 70 | service 71 | .call(req) 72 | .await 73 | .map(ServiceResponse::map_into_left_body) 74 | }); 75 | } 76 | }; 77 | 78 | Box::pin(async move { 79 | let status = limiter.count(key.to_string()).await; 80 | 81 | if let Err(err) = status { 82 | match err { 83 | LimitationError::LimitExceeded(_) => { 84 | log::warn!("Rate limit exceed error for {}", key); 85 | 86 | Ok(req.into_response( 87 | HttpResponse::new(StatusCode::TOO_MANY_REQUESTS).map_into_right_body(), 88 | )) 89 | } 90 | LimitationError::Client(e) => { 91 | log::error!("Client request failed, redis error: {}", e); 92 | 93 | Ok(req.into_response( 94 | HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) 95 | .map_into_right_body(), 96 | )) 97 | } 98 | _ => { 99 | log::error!("Count failed: {}", err); 100 | 101 | Ok(req.into_response( 102 | HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) 103 | .map_into_right_body(), 104 | )) 105 | } 106 | } 107 | } else { 108 | service 109 | .call(req) 110 | .await 111 | .map(ServiceResponse::map_into_left_body) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /actix-limitation/src/status.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Add, time::Duration}; 2 | 3 | use chrono::SubsecRound as _; 4 | 5 | use crate::Error as LimitationError; 6 | 7 | /// A report for a given key containing the limit status. 8 | #[derive(Debug, Clone)] 9 | pub struct Status { 10 | pub(crate) limit: usize, 11 | pub(crate) remaining: usize, 12 | pub(crate) reset_epoch_utc: usize, 13 | } 14 | 15 | impl Status { 16 | /// Constructs status limit status from parts. 17 | #[must_use] 18 | pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self { 19 | let remaining = limit.saturating_sub(count); 20 | 21 | Status { 22 | limit, 23 | remaining, 24 | reset_epoch_utc, 25 | } 26 | } 27 | 28 | /// Returns the maximum number of requests allowed in the current period. 29 | #[must_use] 30 | pub fn limit(&self) -> usize { 31 | self.limit 32 | } 33 | 34 | /// Returns how many requests are left in the current period. 35 | #[must_use] 36 | pub fn remaining(&self) -> usize { 37 | self.remaining 38 | } 39 | 40 | /// Returns a UNIX timestamp in UTC approximately when the next period will begin. 41 | #[must_use] 42 | pub fn reset_epoch_utc(&self) -> usize { 43 | self.reset_epoch_utc 44 | } 45 | 46 | pub(crate) fn epoch_utc_plus(duration: Duration) -> Result { 47 | match chrono::Duration::from_std(duration) { 48 | Ok(value) => Ok(chrono::Utc::now() 49 | .add(value) 50 | .round_subsecs(0) 51 | .timestamp() 52 | .try_into() 53 | .unwrap_or(0)), 54 | 55 | Err(_) => Err(LimitationError::Other( 56 | "Source duration value is out of range for the target type".to_string(), 57 | )), 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn test_create_status() { 68 | let status = Status { 69 | limit: 100, 70 | remaining: 0, 71 | reset_epoch_utc: 1000, 72 | }; 73 | 74 | assert_eq!(status.limit(), 100); 75 | assert_eq!(status.remaining(), 0); 76 | assert_eq!(status.reset_epoch_utc(), 1000); 77 | } 78 | 79 | #[test] 80 | fn test_build_status() { 81 | let count = 200; 82 | let limit = 100; 83 | let status = Status::new(count, limit, 2000); 84 | assert_eq!(status.limit(), limit); 85 | assert_eq!(status.remaining(), 0); 86 | assert_eq!(status.reset_epoch_utc(), 2000); 87 | } 88 | 89 | #[test] 90 | fn test_build_status_limit() { 91 | let limit = 100; 92 | let status = Status::new(0, limit, 2000); 93 | assert_eq!(status.limit(), limit); 94 | assert_eq!(status.remaining(), limit); 95 | assert_eq!(status.reset_epoch_utc(), 2000); 96 | } 97 | 98 | #[test] 99 | fn test_epoch_utc_plus_zero() { 100 | let duration = Duration::from_secs(0); 101 | let seconds = Status::epoch_utc_plus(duration).unwrap(); 102 | assert!(seconds as u64 >= duration.as_secs()); 103 | } 104 | 105 | #[test] 106 | fn test_epoch_utc_plus() { 107 | let duration = Duration::from_secs(10); 108 | let seconds = Status::epoch_utc_plus(duration).unwrap(); 109 | assert!(seconds as u64 >= duration.as_secs() + 10); 110 | } 111 | 112 | #[test] 113 | #[should_panic = "Source duration value is out of range for the target type"] 114 | fn test_epoch_utc_plus_overflow() { 115 | let duration = Duration::from_secs(10000000000000000000); 116 | Status::epoch_utc_plus(duration).unwrap(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /actix-limitation/tests/tests.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use actix_limitation::{Error, Limiter, RateLimiter}; 4 | use actix_web::{dev::ServiceRequest, http::StatusCode, test, web, App, HttpRequest, HttpResponse}; 5 | use uuid::Uuid; 6 | 7 | #[test] 8 | #[should_panic = "Redis URL did not parse"] 9 | async fn test_create_limiter_error() { 10 | Limiter::builder("127.0.0.1").build().unwrap(); 11 | } 12 | 13 | #[actix_web::test] 14 | async fn test_limiter_count() -> Result<(), Error> { 15 | let limiter = Limiter::builder("redis://127.0.0.1:6379/2") 16 | .limit(20) 17 | .build() 18 | .unwrap(); 19 | 20 | let id = Uuid::new_v4(); 21 | 22 | for i in 0..20 { 23 | let status = limiter.count(id.to_string()).await?; 24 | println!("status: {status:?}"); 25 | assert_eq!(20 - status.remaining(), i + 1); 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | #[actix_web::test] 32 | async fn test_limiter_count_error() -> Result<(), Error> { 33 | let limiter = Limiter::builder("redis://127.0.0.1:6379/3") 34 | .limit(25) 35 | .build() 36 | .unwrap(); 37 | 38 | let id = Uuid::new_v4(); 39 | for i in 0..25 { 40 | let status = limiter.count(id.to_string()).await?; 41 | assert_eq!(25 - status.remaining(), i + 1); 42 | } 43 | 44 | match limiter.count(id.to_string()).await.unwrap_err() { 45 | Error::LimitExceeded(status) => assert_eq!(status.remaining(), 0), 46 | _ => panic!("error should be LimitExceeded variant"), 47 | }; 48 | 49 | let id = Uuid::new_v4(); 50 | for i in 0..25 { 51 | let status = limiter.count(id.to_string()).await?; 52 | assert_eq!(25 - status.remaining(), i + 1); 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | #[actix_web::test] 59 | async fn test_limiter_key_by() -> Result<(), Error> { 60 | let cooldown_period = Duration::from_secs(1); 61 | let limiter = Limiter::builder("redis://127.0.0.1:6379/3") 62 | .limit(2) 63 | .period(cooldown_period) 64 | .key_by(|_: &ServiceRequest| Some("fix_key".to_string())) 65 | .build() 66 | .unwrap(); 67 | 68 | let app = test::init_service( 69 | App::new() 70 | .wrap(RateLimiter::default()) 71 | .app_data(web::Data::new(limiter)) 72 | .route( 73 | "/", 74 | web::get().to(|_: HttpRequest| async { HttpResponse::Ok().body("ok") }), 75 | ), 76 | ) 77 | .await; 78 | for _ in 1..2 { 79 | for index in 1..4 { 80 | let req = test::TestRequest::default().to_request(); 81 | let resp = test::call_service(&app, req).await; 82 | if index <= 2 { 83 | assert!(resp.status().is_success()); 84 | } else { 85 | assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS); 86 | } 87 | } 88 | std::thread::sleep(cooldown_period); 89 | } 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /actix-protobuf/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | ## 0.11.0 6 | 7 | - Updated `prost` dependency to `0.13`. 8 | - Minimum supported Rust version (MSRV) is now 1.75. 9 | 10 | ## 0.10.0 11 | 12 | - Updated `prost` dependency to `0.12`. 13 | - Minimum supported Rust version (MSRV) is now 1.68. 14 | 15 | ## 0.9.0 16 | 17 | - Added `application/x-protobuf` as an acceptable header. 18 | - Updated `prost` dependency to `0.11`. 19 | 20 | ## 0.8.0 21 | 22 | - Update `prost` dependency to `0.10`. 23 | - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. 24 | 25 | ## 0.7.0 26 | 27 | - Update `actix-web` dependency to `4`. 28 | 29 | ## 0.7.0-beta.5 30 | 31 | - Update `prost` dependency to `0.9`. 32 | - Update `actix-web` dependency to `4.0.0-rc.1`. 33 | 34 | ## 0.7.0-beta.4 35 | 36 | - Minimum supported Rust version (MSRV) is now 1.54. 37 | 38 | ## 0.7.0-beta.3 39 | 40 | - Update `actix-web` dependency to `4.0.0.beta-14`. [#209] 41 | 42 | [#209]: https://github.com/actix/actix-extras/pull/209 43 | 44 | ## 0.7.0-beta.2 45 | 46 | - Bump `prost` version to 0.8. [#197] 47 | - Update `actix-web` dependency to v4.0.0-beta.10. [#203] 48 | - Minimum supported Rust version (MSRV) is now 1.52. 49 | 50 | [#197]: https://github.com/actix/actix-extras/pull/197 51 | [#203]: https://github.com/actix/actix-extras/pull/203 52 | 53 | ## 0.7.0-beta.1 54 | 55 | - Bump `prost` version to 0.7. [#144] 56 | - Update `actix-web` dependency to 4.0.0 beta. 57 | - Minimum supported Rust version (MSRV) is now 1.46.0. 58 | 59 | [#144]: https://github.com/actix/actix-extras/pull/144 60 | 61 | ## 0.6.0 62 | 63 | - Update `actix-web` dependency to 3.0.0. 64 | - Minimum supported Rust version (MSRV) is now 1.42.0 to use `matches!` macro. 65 | 66 | ## 0.6.0-alpha.1 67 | 68 | - Update `actix-web` to 3.0.0-alpha.3 69 | - Minimum supported Rust version(MSRV) is now 1.40.0. 70 | - Minimize `futures` dependency 71 | 72 | ## 0.5.1 - 2019-02-17 73 | 74 | - Move repository to actix-extras 75 | 76 | ## 0.5.0 - 2019-01-24 77 | 78 | - Migrate to actix-web 2.0.0 and std::future 79 | - Update prost to 0.6 80 | - Update bytes to 0.5 81 | 82 | ## 0.4.1 - 2019-10-03 83 | 84 | - Upgrade prost and prost-derive to 0.5.0 85 | 86 | ## 0.4.0 - 2019-05-18 87 | 88 | - Upgrade to actix-web 1.0.0-rc 89 | - Removed `protobuf` method for `HttpRequest` (use `ProtoBuf` extractor instead) 90 | 91 | ## 0.3.0 - 2019-03-07 92 | 93 | - Upgrade to actix-web 0.7.18 94 | 95 | ## 0.2.0 - 2018-04-10 96 | 97 | - Provide protobuf extractor 98 | 99 | ## 0.1.0 - 2018-03-21 100 | 101 | - First release 102 | -------------------------------------------------------------------------------- /actix-protobuf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-protobuf" 3 | version = "0.11.0" 4 | authors = [ 5 | "kingxsp ", 6 | "Yuki Okushi ", 7 | ] 8 | description = "Protobuf payload extractor for Actix Web" 9 | keywords = ["actix", "web", "protobuf", "protocol", "rpc"] 10 | repository.workspace = true 11 | homepage.workspace = true 12 | license.workspace = true 13 | edition.workspace = true 14 | rust-version.workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | all-features = true 19 | 20 | [dependencies] 21 | actix-web = { version = "4", default-features = false } 22 | derive_more = { version = "2", features = ["display"] } 23 | futures-util = { version = "0.3.17", default-features = false, features = ["std"] } 24 | prost = { version = "0.13", default-features = false } 25 | 26 | [dev-dependencies] 27 | actix-web = { version = "4", default-features = false, features = ["macros"] } 28 | prost = { version = "0.13", default-features = false, features = ["prost-derive"] } 29 | 30 | [lints] 31 | workspace = true 32 | -------------------------------------------------------------------------------- /actix-protobuf/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-protobuf/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-protobuf/README.md: -------------------------------------------------------------------------------- 1 | # actix-protobuf 2 | 3 | > Protobuf payload extractor for Actix Web. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf) 8 | [![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.11.0)](https://docs.rs/actix-protobuf/0.11.0) 9 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-protobuf) 10 | [![Dependency Status](https://deps.rs/crate/actix-protobuf/0.11.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.11.0) 11 | 12 | 13 | 14 | ## Documentation & Resources 15 | 16 | - [API Documentation](https://docs.rs/actix-protobuf) 17 | - [Example Project](https://github.com/actix/examples/tree/master/protobuf) 18 | - Minimum Supported Rust Version (MSRV): 1.57 19 | 20 | ## Example 21 | 22 | ```rust,ignore 23 | use actix_protobuf::*; 24 | use actix_web::*; 25 | 26 | #[derive(Clone, PartialEq, Message)] 27 | pub struct MyObj { 28 | #[prost(int32, tag = "1")] 29 | pub number: i32, 30 | 31 | #[prost(string, tag = "2")] 32 | pub name: String, 33 | } 34 | 35 | async fn index(msg: ProtoBuf) -> Result { 36 | println!("model: {:?}", msg); 37 | HttpResponse::Ok().protobuf(msg.0) // <- send response 38 | } 39 | ``` 40 | 41 | See [here](https://github.com/actix/examples/tree/master/protobuf) for the complete example. 42 | 43 | ## License 44 | 45 | This project is licensed under either of 46 | 47 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)) 48 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT)) 49 | 50 | at your option. 51 | -------------------------------------------------------------------------------- /actix-session/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-session" 3 | version = "0.10.1" 4 | authors = [ 5 | "Nikolay Kim ", 6 | "Luca Palmieri ", 7 | ] 8 | description = "Session management for Actix Web" 9 | keywords = ["http", "web", "framework", "async", "session"] 10 | repository.workspace = true 11 | homepage.workspace = true 12 | license.workspace = true 13 | edition.workspace = true 14 | rust-version.workspace = true 15 | 16 | [package.metadata.docs.rs] 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | all-features = true 19 | 20 | [features] 21 | default = [] 22 | cookie-session = [] 23 | redis-session = ["dep:redis"] 24 | redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"] 25 | redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"] 26 | redis-pool = ["dep:deadpool-redis"] 27 | 28 | [dependencies] 29 | actix-service = "2" 30 | actix-utils = "3" 31 | actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] } 32 | 33 | anyhow = "1" 34 | derive_more = { version = "2", features = ["display", "error", "from"] } 35 | rand = "0.9" 36 | serde = { version = "1" } 37 | serde_json = { version = "1" } 38 | tracing = { version = "0.1.30", default-features = false, features = ["log"] } 39 | 40 | # redis-session 41 | redis = { version = "0.29", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true } 42 | deadpool-redis = { version = "0.20", optional = true } 43 | 44 | [dev-dependencies] 45 | actix-session = { path = ".", features = ["cookie-session", "redis-session"] } 46 | actix-test = "0.1" 47 | actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] } 48 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 49 | tracing = "0.1.30" 50 | 51 | [lints] 52 | workspace = true 53 | 54 | [[example]] 55 | name = "basic" 56 | required-features = ["redis-session"] 57 | 58 | [[example]] 59 | name = "authentication" 60 | required-features = ["redis-session"] 61 | -------------------------------------------------------------------------------- /actix-session/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-session/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-session/examples/authentication.rs: -------------------------------------------------------------------------------- 1 | use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware}; 2 | use actix_web::{ 3 | cookie::{Key, SameSite}, 4 | error::InternalError, 5 | middleware, web, App, Error, HttpResponse, HttpServer, Responder, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use tracing::level_filters::LevelFilter; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | #[derive(Deserialize)] 12 | struct Credentials { 13 | username: String, 14 | password: String, 15 | } 16 | 17 | #[derive(Serialize)] 18 | struct User { 19 | id: i64, 20 | username: String, 21 | password: String, 22 | } 23 | 24 | impl User { 25 | fn authenticate(credentials: Credentials) -> Result { 26 | // to do: figure out why I keep getting hacked /s 27 | if &credentials.password != "hunter2" { 28 | return Err(HttpResponse::Unauthorized().json("Unauthorized")); 29 | } 30 | 31 | Ok(User { 32 | id: 42, 33 | username: credentials.username, 34 | password: credentials.password, 35 | }) 36 | } 37 | } 38 | 39 | pub fn validate_session(session: &Session) -> Result { 40 | let user_id: Option = session.get("user_id").unwrap_or(None); 41 | 42 | match user_id { 43 | Some(id) => { 44 | // keep the user's session alive 45 | session.renew(); 46 | Ok(id) 47 | } 48 | None => Err(HttpResponse::Unauthorized().json("Unauthorized")), 49 | } 50 | } 51 | 52 | async fn login( 53 | credentials: web::Json, 54 | session: Session, 55 | ) -> Result { 56 | let credentials = credentials.into_inner(); 57 | 58 | match User::authenticate(credentials) { 59 | Ok(user) => session.insert("user_id", user.id).unwrap(), 60 | Err(err) => return Err(InternalError::from_response("", err).into()), 61 | }; 62 | 63 | Ok("Welcome!") 64 | } 65 | 66 | /// some protected resource 67 | async fn secret(session: Session) -> Result { 68 | // only allow access to this resource if the user has an active session 69 | validate_session(&session).map_err(|err| InternalError::from_response("", err))?; 70 | 71 | Ok("secret revealed") 72 | } 73 | 74 | #[actix_web::main] 75 | async fn main() -> std::io::Result<()> { 76 | tracing_subscriber::fmt() 77 | .with_env_filter( 78 | EnvFilter::builder() 79 | .with_default_directive(LevelFilter::INFO.into()) 80 | .from_env_lossy(), 81 | ) 82 | .init(); 83 | 84 | // The signing key would usually be read from a configuration file/environment variables. 85 | let signing_key = Key::generate(); 86 | 87 | tracing::info!("setting up Redis session storage"); 88 | let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap(); 89 | 90 | tracing::info!("starting HTTP server at http://localhost:8080"); 91 | 92 | HttpServer::new(move || { 93 | App::new() 94 | // enable logger 95 | .wrap(middleware::Logger::default()) 96 | // cookie session middleware 97 | .wrap( 98 | SessionMiddleware::builder(storage.clone(), signing_key.clone()) 99 | // allow the cookie to be accessed from javascript 100 | .cookie_http_only(false) 101 | // allow the cookie only from the current domain 102 | .cookie_same_site(SameSite::Strict) 103 | .build(), 104 | ) 105 | .route("/login", web::post().to(login)) 106 | .route("/secret", web::get().to(secret)) 107 | }) 108 | .bind(("127.0.0.1", 8080))? 109 | .run() 110 | .await 111 | } 112 | -------------------------------------------------------------------------------- /actix-session/examples/basic.rs: -------------------------------------------------------------------------------- 1 | use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware}; 2 | use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder}; 3 | use tracing::level_filters::LevelFilter; 4 | use tracing_subscriber::EnvFilter; 5 | 6 | /// simple handler 7 | async fn index(req: HttpRequest, session: Session) -> Result { 8 | println!("{req:?}"); 9 | 10 | // session 11 | if let Some(count) = session.get::("counter")? { 12 | println!("SESSION value: {count}"); 13 | session.insert("counter", count + 1)?; 14 | } else { 15 | session.insert("counter", 1)?; 16 | } 17 | 18 | Ok("Welcome!") 19 | } 20 | 21 | #[actix_web::main] 22 | async fn main() -> std::io::Result<()> { 23 | tracing_subscriber::fmt() 24 | .with_env_filter( 25 | EnvFilter::builder() 26 | .with_default_directive(LevelFilter::INFO.into()) 27 | .from_env_lossy(), 28 | ) 29 | .init(); 30 | 31 | // The signing key would usually be read from a configuration file/environment variables. 32 | let signing_key = Key::generate(); 33 | 34 | tracing::info!("setting up Redis session storage"); 35 | let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap(); 36 | 37 | tracing::info!("starting HTTP server at http://localhost:8080"); 38 | 39 | HttpServer::new(move || { 40 | App::new() 41 | // enable logger 42 | .wrap(middleware::Logger::default()) 43 | // cookie session middleware 44 | .wrap(SessionMiddleware::new(storage.clone(), signing_key.clone())) 45 | // register simple route, handle all methods 46 | .service(web::resource("/").to(index)) 47 | }) 48 | .bind(("127.0.0.1", 8080))? 49 | .run() 50 | .await 51 | } 52 | -------------------------------------------------------------------------------- /actix-session/src/session_ext.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | dev::{ServiceRequest, ServiceResponse}, 3 | guard::GuardContext, 4 | HttpMessage, HttpRequest, 5 | }; 6 | 7 | use crate::Session; 8 | 9 | /// Extract a [`Session`] object from various `actix-web` types (e.g. `HttpRequest`, 10 | /// `ServiceRequest`, `ServiceResponse`). 11 | pub trait SessionExt { 12 | /// Extract a [`Session`] object. 13 | fn get_session(&self) -> Session; 14 | } 15 | 16 | impl SessionExt for HttpRequest { 17 | fn get_session(&self) -> Session { 18 | Session::get_session(&mut self.extensions_mut()) 19 | } 20 | } 21 | 22 | impl SessionExt for ServiceRequest { 23 | fn get_session(&self) -> Session { 24 | Session::get_session(&mut self.extensions_mut()) 25 | } 26 | } 27 | 28 | impl SessionExt for ServiceResponse { 29 | fn get_session(&self) -> Session { 30 | self.request().get_session() 31 | } 32 | } 33 | 34 | impl SessionExt for GuardContext<'_> { 35 | fn get_session(&self) -> Session { 36 | Session::get_session(&mut self.req_data_mut()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /actix-session/src/storage/cookie.rs: -------------------------------------------------------------------------------- 1 | use actix_web::cookie::time::Duration; 2 | use anyhow::Error; 3 | 4 | use super::SessionKey; 5 | use crate::storage::{ 6 | interface::{LoadError, SaveError, SessionState, UpdateError}, 7 | SessionStore, 8 | }; 9 | 10 | /// Use the session key, stored in the session cookie, as storage backend for the session state. 11 | /// 12 | /// ```no_run 13 | /// use actix_web::{cookie::Key, web, App, HttpServer, HttpResponse, Error}; 14 | /// use actix_session::{SessionMiddleware, storage::CookieSessionStore}; 15 | /// 16 | /// // The secret key would usually be read from a configuration file/environment variables. 17 | /// fn get_secret_key() -> Key { 18 | /// # todo!() 19 | /// // [...] 20 | /// } 21 | /// 22 | /// #[actix_web::main] 23 | /// async fn main() -> std::io::Result<()> { 24 | /// let secret_key = get_secret_key(); 25 | /// HttpServer::new(move || 26 | /// App::new() 27 | /// .wrap(SessionMiddleware::new(CookieSessionStore::default(), secret_key.clone())) 28 | /// .default_service(web::to(|| HttpResponse::Ok()))) 29 | /// .bind(("127.0.0.1", 8080))? 30 | /// .run() 31 | /// .await 32 | /// } 33 | /// ``` 34 | /// 35 | /// # Limitations 36 | /// Cookies are subject to size limits so we require session keys to be shorter than 4096 bytes. 37 | /// This translates into a limit on the maximum size of the session state when using cookies as 38 | /// storage backend. 39 | /// 40 | /// The session cookie can always be inspected by end users via the developer tools exposed by their 41 | /// browsers. We strongly recommend setting the policy to [`CookieContentSecurity::Private`] when 42 | /// using cookies as storage backend. 43 | /// 44 | /// There is no way to invalidate a session before its natural expiry when using cookies as the 45 | /// storage backend. 46 | /// 47 | /// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private 48 | #[derive(Default)] 49 | #[non_exhaustive] 50 | pub struct CookieSessionStore; 51 | 52 | impl SessionStore for CookieSessionStore { 53 | async fn load(&self, session_key: &SessionKey) -> Result, LoadError> { 54 | serde_json::from_str(session_key.as_ref()) 55 | .map(Some) 56 | .map_err(anyhow::Error::new) 57 | .map_err(LoadError::Deserialization) 58 | } 59 | 60 | async fn save( 61 | &self, 62 | session_state: SessionState, 63 | _ttl: &Duration, 64 | ) -> Result { 65 | let session_key = serde_json::to_string(&session_state) 66 | .map_err(anyhow::Error::new) 67 | .map_err(SaveError::Serialization)?; 68 | 69 | session_key 70 | .try_into() 71 | .map_err(Into::into) 72 | .map_err(SaveError::Other) 73 | } 74 | 75 | async fn update( 76 | &self, 77 | _session_key: SessionKey, 78 | session_state: SessionState, 79 | ttl: &Duration, 80 | ) -> Result { 81 | self.save(session_state, ttl) 82 | .await 83 | .map_err(|err| match err { 84 | SaveError::Serialization(err) => UpdateError::Serialization(err), 85 | SaveError::Other(err) => UpdateError::Other(err), 86 | }) 87 | } 88 | 89 | async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> { 90 | Ok(()) 91 | } 92 | 93 | async fn delete(&self, _session_key: &SessionKey) -> Result<(), anyhow::Error> { 94 | Ok(()) 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | use super::*; 101 | use crate::{storage::utils::generate_session_key, test_helpers::acceptance_test_suite}; 102 | 103 | #[actix_web::test] 104 | async fn test_session_workflow() { 105 | acceptance_test_suite(CookieSessionStore::default, false).await; 106 | } 107 | 108 | #[actix_web::test] 109 | async fn loading_a_random_session_key_returns_deserialization_error() { 110 | let store = CookieSessionStore::default(); 111 | let session_key = generate_session_key(); 112 | assert!(matches!( 113 | store.load(&session_key).await.unwrap_err(), 114 | LoadError::Deserialization(_), 115 | )); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /actix-session/src/storage/interface.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, future::Future}; 2 | 3 | use actix_web::cookie::time::Duration; 4 | use derive_more::derive::Display; 5 | 6 | use super::SessionKey; 7 | 8 | pub(crate) type SessionState = HashMap; 9 | 10 | /// The interface to retrieve and save the current session data from/to the chosen storage backend. 11 | /// 12 | /// You can provide your own custom session store backend by implementing this trait. 13 | pub trait SessionStore { 14 | /// Loads the session state associated to a session key. 15 | fn load( 16 | &self, 17 | session_key: &SessionKey, 18 | ) -> impl Future, LoadError>>; 19 | 20 | /// Persist the session state for a newly created session. 21 | /// 22 | /// Returns the corresponding session key. 23 | fn save( 24 | &self, 25 | session_state: SessionState, 26 | ttl: &Duration, 27 | ) -> impl Future>; 28 | 29 | /// Updates the session state associated to a pre-existing session key. 30 | fn update( 31 | &self, 32 | session_key: SessionKey, 33 | session_state: SessionState, 34 | ttl: &Duration, 35 | ) -> impl Future>; 36 | 37 | /// Updates the TTL of the session state associated to a pre-existing session key. 38 | fn update_ttl( 39 | &self, 40 | session_key: &SessionKey, 41 | ttl: &Duration, 42 | ) -> impl Future>; 43 | 44 | /// Deletes a session from the store. 45 | fn delete(&self, session_key: &SessionKey) -> impl Future>; 46 | } 47 | 48 | // We cannot derive the `Error` implementation using `derive_more` for our custom errors: 49 | // `derive_more`'s `#[error(source)]` attribute requires the source implement the `Error` trait, 50 | // while it's actually enough for it to be able to produce a reference to a dyn Error. 51 | 52 | /// Possible failures modes for [`SessionStore::load`]. 53 | #[derive(Debug, Display)] 54 | pub enum LoadError { 55 | /// Failed to deserialize session state. 56 | #[display("Failed to deserialize session state")] 57 | Deserialization(anyhow::Error), 58 | 59 | /// Something went wrong when retrieving the session state. 60 | #[display("Something went wrong when retrieving the session state")] 61 | Other(anyhow::Error), 62 | } 63 | 64 | impl std::error::Error for LoadError { 65 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 66 | match self { 67 | Self::Deserialization(err) => Some(err.as_ref()), 68 | Self::Other(err) => Some(err.as_ref()), 69 | } 70 | } 71 | } 72 | 73 | /// Possible failures modes for [`SessionStore::save`]. 74 | #[derive(Debug, Display)] 75 | pub enum SaveError { 76 | /// Failed to serialize session state. 77 | #[display("Failed to serialize session state")] 78 | Serialization(anyhow::Error), 79 | 80 | /// Something went wrong when persisting the session state. 81 | #[display("Something went wrong when persisting the session state")] 82 | Other(anyhow::Error), 83 | } 84 | 85 | impl std::error::Error for SaveError { 86 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 87 | match self { 88 | Self::Serialization(err) => Some(err.as_ref()), 89 | Self::Other(err) => Some(err.as_ref()), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug, Display)] 95 | /// Possible failures modes for [`SessionStore::update`]. 96 | pub enum UpdateError { 97 | /// Failed to serialize session state. 98 | #[display("Failed to serialize session state")] 99 | Serialization(anyhow::Error), 100 | 101 | /// Something went wrong when updating the session state. 102 | #[display("Something went wrong when updating the session state.")] 103 | Other(anyhow::Error), 104 | } 105 | 106 | impl std::error::Error for UpdateError { 107 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 108 | match self { 109 | Self::Serialization(err) => Some(err.as_ref()), 110 | Self::Other(err) => Some(err.as_ref()), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /actix-session/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Pluggable storage backends for session state. 2 | 3 | #[cfg(feature = "cookie-session")] 4 | mod cookie; 5 | mod interface; 6 | #[cfg(feature = "redis-session")] 7 | mod redis_rs; 8 | mod session_key; 9 | mod utils; 10 | 11 | #[cfg(feature = "cookie-session")] 12 | pub use self::cookie::CookieSessionStore; 13 | #[cfg(feature = "redis-session")] 14 | pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder}; 15 | pub use self::{ 16 | interface::{LoadError, SaveError, SessionStore, UpdateError}, 17 | session_key::SessionKey, 18 | utils::generate_session_key, 19 | }; 20 | -------------------------------------------------------------------------------- /actix-session/src/storage/session_key.rs: -------------------------------------------------------------------------------- 1 | use derive_more::derive::{Display, From}; 2 | 3 | /// A session key, the string stored in a client-side cookie to associate a user with its session 4 | /// state on the backend. 5 | /// 6 | /// # Validation 7 | /// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are 8 | /// required to be smaller than 4064 bytes. 9 | /// 10 | /// ``` 11 | /// use actix_session::storage::SessionKey; 12 | /// 13 | /// let key: String = std::iter::repeat('a').take(4065).collect(); 14 | /// let session_key: Result = key.try_into(); 15 | /// assert!(session_key.is_err()); 16 | /// ``` 17 | #[derive(Debug, PartialEq, Eq)] 18 | pub struct SessionKey(String); 19 | 20 | impl TryFrom for SessionKey { 21 | type Error = InvalidSessionKeyError; 22 | 23 | fn try_from(val: String) -> Result { 24 | if val.len() > 4064 { 25 | return Err(anyhow::anyhow!( 26 | "The session key is bigger than 4064 bytes, the upper limit on cookie content." 27 | ) 28 | .into()); 29 | } 30 | 31 | Ok(SessionKey(val)) 32 | } 33 | } 34 | 35 | impl AsRef for SessionKey { 36 | fn as_ref(&self) -> &str { 37 | &self.0 38 | } 39 | } 40 | 41 | impl From for String { 42 | fn from(key: SessionKey) -> Self { 43 | key.0 44 | } 45 | } 46 | 47 | #[derive(Debug, Display, From)] 48 | #[display("The provided string is not a valid session key")] 49 | pub struct InvalidSessionKeyError(anyhow::Error); 50 | 51 | impl std::error::Error for InvalidSessionKeyError { 52 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 53 | Some(self.0.as_ref()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /actix-session/src/storage/utils.rs: -------------------------------------------------------------------------------- 1 | use rand::distr::{Alphanumeric, SampleString as _}; 2 | 3 | use crate::storage::SessionKey; 4 | 5 | /// Session key generation routine that follows [OWASP recommendations]. 6 | /// 7 | /// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy 8 | pub fn generate_session_key() -> SessionKey { 9 | Alphanumeric 10 | .sample_string(&mut rand::rng(), 64) 11 | .try_into() 12 | .expect("generated string should be within size range for a session key") 13 | } 14 | -------------------------------------------------------------------------------- /actix-session/tests/middleware.rs: -------------------------------------------------------------------------------- 1 | use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware}; 2 | use actix_web::{ 3 | cookie::{time::Duration, Key}, 4 | test, web, App, Responder, 5 | }; 6 | 7 | async fn login(session: Session) -> impl Responder { 8 | session.insert("user_id", "id").unwrap(); 9 | "Logged in" 10 | } 11 | 12 | async fn logout(session: Session) -> impl Responder { 13 | session.purge(); 14 | "Logged out" 15 | } 16 | 17 | #[actix_web::test] 18 | async fn cookie_storage() -> std::io::Result<()> { 19 | let signing_key = Key::generate(); 20 | let app = test::init_service( 21 | App::new() 22 | .wrap( 23 | SessionMiddleware::builder(CookieSessionStore::default(), signing_key.clone()) 24 | .cookie_path("/test".to_string()) 25 | .cookie_domain(Some("localhost".to_string())) 26 | .build(), 27 | ) 28 | .route("/login", web::post().to(login)) 29 | .route("/logout", web::post().to(logout)), 30 | ) 31 | .await; 32 | 33 | let login_request = test::TestRequest::post().uri("/login").to_request(); 34 | let login_response = test::call_service(&app, login_request).await; 35 | let session_cookie = login_response.response().cookies().next().unwrap(); 36 | assert_eq!(session_cookie.name(), "id"); 37 | assert_eq!(session_cookie.path().unwrap(), "/test"); 38 | assert!(session_cookie.secure().unwrap()); 39 | assert!(session_cookie.http_only().unwrap()); 40 | assert!(session_cookie.max_age().is_none()); 41 | assert_eq!(session_cookie.domain().unwrap(), "localhost"); 42 | 43 | let logout_request = test::TestRequest::post() 44 | .cookie(session_cookie) 45 | .uri("/logout") 46 | .to_request(); 47 | let logout_response = test::call_service(&app, logout_request).await; 48 | let deletion_cookie = logout_response.response().cookies().next().unwrap(); 49 | assert_eq!(deletion_cookie.name(), "id"); 50 | assert_eq!(deletion_cookie.path().unwrap(), "/test"); 51 | assert!(deletion_cookie.secure().unwrap()); 52 | assert!(deletion_cookie.http_only().unwrap()); 53 | assert_eq!(deletion_cookie.max_age().unwrap(), Duration::ZERO); 54 | assert_eq!(deletion_cookie.domain().unwrap(), "localhost"); 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /actix-session/tests/opaque_errors.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use actix_session::{ 4 | storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError}, 5 | Session, SessionMiddleware, 6 | }; 7 | use actix_web::{ 8 | body::MessageBody, 9 | cookie::{time::Duration, Key}, 10 | dev::Service, 11 | http::StatusCode, 12 | test, web, App, Responder, 13 | }; 14 | use anyhow::Error; 15 | 16 | #[actix_web::test] 17 | async fn errors_are_opaque() { 18 | let signing_key = Key::generate(); 19 | let app = test::init_service( 20 | App::new() 21 | .wrap(SessionMiddleware::new(MockStore, signing_key.clone())) 22 | .route("/create_session", web::post().to(create_session)) 23 | .route( 24 | "/load_session_with_error", 25 | web::post().to(load_session_with_error), 26 | ), 27 | ) 28 | .await; 29 | 30 | let req = test::TestRequest::post() 31 | .uri("/create_session") 32 | .to_request(); 33 | let response = test::call_service(&app, req).await; 34 | let session_cookie = response.response().cookies().next().unwrap(); 35 | 36 | let req = test::TestRequest::post() 37 | .cookie(session_cookie) 38 | .uri("/load_session_with_error") 39 | .to_request(); 40 | let response = app.call(req).await.unwrap_err().error_response(); 41 | assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); 42 | assert!(response.into_body().try_into_bytes().unwrap().is_empty()); 43 | } 44 | 45 | struct MockStore; 46 | 47 | impl SessionStore for MockStore { 48 | async fn load( 49 | &self, 50 | _session_key: &SessionKey, 51 | ) -> Result>, LoadError> { 52 | Err(LoadError::Other(anyhow::anyhow!( 53 | "My error full of implementation details" 54 | ))) 55 | } 56 | 57 | async fn save( 58 | &self, 59 | _session_state: HashMap, 60 | _ttl: &Duration, 61 | ) -> Result { 62 | Ok("random_value".to_string().try_into().unwrap()) 63 | } 64 | 65 | async fn update( 66 | &self, 67 | _session_key: SessionKey, 68 | _session_state: HashMap, 69 | _ttl: &Duration, 70 | ) -> Result { 71 | #![allow(clippy::diverging_sub_expression)] 72 | unimplemented!() 73 | } 74 | 75 | async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> { 76 | #![allow(clippy::diverging_sub_expression)] 77 | unimplemented!() 78 | } 79 | 80 | async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> { 81 | #![allow(clippy::diverging_sub_expression)] 82 | unimplemented!() 83 | } 84 | } 85 | 86 | async fn create_session(session: Session) -> impl Responder { 87 | session.insert("user_id", "id").unwrap(); 88 | "Created" 89 | } 90 | 91 | async fn load_session_with_error(_session: Session) -> impl Responder { 92 | "Loaded" 93 | } 94 | -------------------------------------------------------------------------------- /actix-settings/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | ## 0.8.0 6 | 7 | - Add `openssl` crate feature for TLS settings using OpenSSL. 8 | - Add `ApplySettings::try_apply_settings()`. 9 | - Implement TLS logic for `ApplySettings::try_apply_settings()`. 10 | - Add `Tls::get_ssl_acceptor_builder()` function to build `openssl::ssl::SslAcceptorBuilder`. 11 | - Deprecate `ApplySettings::apply_settings()`. 12 | - Minimum supported Rust version (MSRV) is now 1.75. 13 | 14 | ## 0.7.1 15 | 16 | - Fix doc examples. 17 | 18 | ## 0.7.0 19 | 20 | - The `ApplySettings` trait now includes a type parameter, allowing multiple types to be implemented per configuration target. 21 | - Implement `ApplySettings` for `ActixSettings`. 22 | - `BasicSettings::from_default_template()` is now infallible. 23 | - Rename `AtError => Error`. 24 | - Remove `AtResult` type alias. 25 | - Update `toml` dependency to `0.8`. 26 | - Remove `ioe` dependency; `std::io::Error` is now used directly. 27 | - Remove `Clone` implementation for `Error`. 28 | - Implement `Display` for `Error`. 29 | - Implement std's `Error` for `Error`. 30 | - Minimum supported Rust version (MSRV) is now 1.68. 31 | 32 | ## 0.6.0 33 | 34 | - Update Actix Web dependencies to v4 ecosystem. 35 | - Rename `actix.ssl` settings object to `actix.tls`. 36 | - `NoSettings` is now marked `#[non_exhaustive]`. 37 | 38 | ## 0.5.2 39 | 40 | - Adopted into @actix org from . 41 | -------------------------------------------------------------------------------- /actix-settings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-settings" 3 | version = "0.8.0" 4 | authors = [ 5 | "Joey Ezechiels ", 6 | "Rob Ede ", 7 | ] 8 | description = "Easily manage Actix Web's settings from a TOML file and environment variables" 9 | repository.workspace = true 10 | homepage.workspace = true 11 | license.workspace = true 12 | edition.workspace = true 13 | rust-version.workspace = true 14 | 15 | [package.metadata.docs.rs] 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [features] 19 | openssl = ["dep:openssl", "actix-web/openssl"] 20 | 21 | [dependencies] 22 | actix-http = "3" 23 | actix-service = "2" 24 | actix-web = { version = "4", default-features = false } 25 | derive_more = { version = "2", features = ["display", "error"] } 26 | once_cell = "1.21" 27 | openssl = { version = "0.10", features = ["v110"], optional = true } 28 | regex = "1.5" 29 | serde = { version = "1", features = ["derive"] } 30 | toml = "0.8" 31 | 32 | [dev-dependencies] 33 | actix-web = "4" 34 | env_logger = "0.11" 35 | 36 | [lints] 37 | workspace = true 38 | -------------------------------------------------------------------------------- /actix-settings/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-settings/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-settings/README.md: -------------------------------------------------------------------------------- 1 | # actix-settings 2 | 3 | > Easily manage Actix Web's settings from a TOML file and environment variables. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings) 8 | [![Documentation](https://docs.rs/actix-settings/badge.svg?version=0.8.0)](https://docs.rs/actix-settings/0.8.0) 9 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-settings) 10 | [![Dependency Status](https://deps.rs/crate/actix-settings/0.8.0/status.svg)](https://deps.rs/crate/actix-settings/0.8.0) 11 | 12 | 13 | 14 | ## Documentation & Resources 15 | 16 | - [API Documentation](https://docs.rs/actix-settings) 17 | - [Usage Example][usage] 18 | - Minimum Supported Rust Version (MSRV): 1.57 19 | 20 | ### Custom Settings 21 | 22 | There is a way to extend the available settings. This can be used to combine the settings provided by Actix Web and those provided by application server built using `actix`. 23 | 24 | Have a look at [the usage example][usage] to see how. 25 | 26 | ## Special Thanks 27 | 28 | This crate was made possible by support from Accept B.V and [@jjpe]. 29 | 30 | [usage]: https://github.com/actix/actix-extras/blob/master/actix-settings/examples/actix.rs 31 | [@jjpe]: https://github.com/jjpe 32 | -------------------------------------------------------------------------------- /actix-settings/examples/actix.rs: -------------------------------------------------------------------------------- 1 | use actix_settings::{ApplySettings as _, Mode, Settings}; 2 | use actix_web::{ 3 | get, 4 | middleware::{Compress, Condition, Logger}, 5 | web, App, HttpServer, Responder, 6 | }; 7 | 8 | #[get("/")] 9 | async fn index(settings: web::Data) -> impl Responder { 10 | format!( 11 | r#"{{ 12 | "mode": "{}", 13 | "hosts": ["{}"] 14 | }}"#, 15 | match settings.actix.mode { 16 | Mode::Development => "development", 17 | Mode::Production => "production", 18 | }, 19 | settings 20 | .actix 21 | .hosts 22 | .iter() 23 | .map(|addr| { format!("{}:{}", addr.host, addr.port) }) 24 | .collect::>() 25 | .join(", "), 26 | ) 27 | .customize() 28 | .insert_header(("content-type", "application/json")) 29 | } 30 | 31 | #[actix_web::main] 32 | async fn main() -> std::io::Result<()> { 33 | let mut settings = Settings::parse_toml("./examples/config.toml") 34 | .expect("Failed to parse `Settings` from config.toml"); 35 | 36 | // If the environment variable `$APPLICATION__HOSTS` is set, 37 | // have its value override the `settings.actix.hosts` setting: 38 | Settings::override_field_with_env_var(&mut settings.actix.hosts, "APPLICATION__HOSTS")?; 39 | 40 | init_logger(&settings); 41 | 42 | HttpServer::new({ 43 | // clone settings into each worker thread 44 | let settings = settings.clone(); 45 | 46 | move || { 47 | App::new() 48 | // Include this `.wrap()` call for compression settings to take effect: 49 | .wrap(Condition::new( 50 | settings.actix.enable_compression, 51 | Compress::default(), 52 | )) 53 | .wrap(Logger::default()) 54 | // make `Settings` available to handlers 55 | .app_data(web::Data::new(settings.clone())) 56 | .service(index) 57 | } 58 | }) 59 | // apply the `Settings` to Actix Web's `HttpServer` 60 | .try_apply_settings(&settings)? 61 | .run() 62 | .await 63 | } 64 | 65 | /// Initialize the logging infrastructure. 66 | fn init_logger(settings: &Settings) { 67 | if !settings.actix.enable_log { 68 | return; 69 | } 70 | 71 | std::env::set_var( 72 | "RUST_LOG", 73 | match settings.actix.mode { 74 | Mode::Development => "actix_web=debug", 75 | Mode::Production => "actix_web=info", 76 | }, 77 | ); 78 | 79 | std::env::set_var("RUST_BACKTRACE", "1"); 80 | 81 | env_logger::init(); 82 | } 83 | -------------------------------------------------------------------------------- /actix-settings/examples/config.toml: -------------------------------------------------------------------------------- 1 | [actix] 2 | # For more info, see: https://docs.rs/actix-web/4/actix_web/struct.HttpServer.html. 3 | 4 | hosts = [ 5 | ["0.0.0.0", 8080] # This should work for both development and deployment... 6 | # # ... but other entries are possible, as well. 7 | ] 8 | mode = "development" # Either "development" or "production". 9 | enable-compression = true # Toggle compression middleware. 10 | enable-log = true # Toggle logging middleware. 11 | 12 | # The number of workers that the server should start. 13 | # By default the number of available logical cpu cores is used. 14 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 15 | num-workers = "default" 16 | 17 | # The maximum number of pending connections. This refers to the number of clients 18 | # that can be waiting to be served. Exceeding this number results in the client 19 | # getting an error when attempting to connect. It should only affect servers under 20 | # significant load. Generally set in the 64-2048 range. The default value is 2048. 21 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 22 | backlog = "default" 23 | 24 | # Sets the per-worker maximum number of concurrent connections. All socket listeners 25 | # will stop accepting connections when this limit is reached for each worker. 26 | # By default max connections is set to a 25k. 27 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 28 | max-connections = "default" 29 | 30 | # Sets the per-worker maximum concurrent connection establish process. All listeners 31 | # will stop accepting connections when this limit is reached. It can be used to limit 32 | # the global TLS CPU usage. By default max connections is set to a 256. 33 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 34 | max-connection-rate = "default" 35 | 36 | # Set server keep-alive preference. By default keep alive is set to 5 seconds. 37 | # Takes a string value: Either "default", "disabled", "os", 38 | # or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds". 39 | keep-alive = "default" 40 | 41 | # Set server client timeout in milliseconds for first request. Defines a timeout 42 | # for reading client request header. If a client does not transmit the entire set of 43 | # headers within this time, the request is terminated with the 408 (Request Time-out) 44 | # error. To disable timeout, set the value to 0. 45 | # By default client timeout is set to 5000 milliseconds. 46 | # Takes a string value: Either "default", or a string of the format "N milliseconds" 47 | # where N is an integer > 0 e.g. "6 milliseconds". 48 | client-timeout = "default" 49 | 50 | # Set server connection shutdown timeout in milliseconds. Defines a timeout for 51 | # shutdown connection. If a shutdown procedure does not complete within this time, 52 | # the request is dropped. To disable timeout set value to 0. 53 | # By default client timeout is set to 5000 milliseconds. 54 | # Takes a string value: Either "default", or a string of the format "N milliseconds" 55 | # where N is an integer > 0 e.g. "6 milliseconds". 56 | client-shutdown = "default" 57 | 58 | # Timeout for graceful workers shutdown. After receiving a stop signal, workers have 59 | # this much time to finish serving requests. Workers still alive after the timeout 60 | # are force dropped. By default shutdown timeout sets to 30 seconds. 61 | # Takes a string value: Either "default", or a string of the format "N seconds" 62 | # where N is an integer > 0 e.g. "6 seconds". 63 | shutdown-timeout = "default" 64 | 65 | [actix.tls] # TLS is disabled by default because the certs don't exist 66 | enabled = false 67 | certificate = "path/to/cert/cert.pem" 68 | private-key = "path/to/cert/key.pem" 69 | 70 | # The `application` table be used to express application-specific settings. 71 | # See the `README.md` file for more details on how to use this. 72 | [application] 73 | -------------------------------------------------------------------------------- /actix-settings/src/defaults.toml: -------------------------------------------------------------------------------- 1 | [actix] 2 | # For more info, see: https://docs.rs/actix-web/4/actix_web/struct.HttpServer.html. 3 | 4 | hosts = [ 5 | ["0.0.0.0", 9000] # This should work for both development and deployment... 6 | # # ... but other entries are possible, as well. 7 | ] 8 | mode = "development" # Either "development" or "production". 9 | enable-compression = true # Toggle compression middleware. 10 | enable-log = true # Toggle logging middleware. 11 | 12 | # The number of workers that the server should start. 13 | # By default the number of available logical cpu cores is used. 14 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 15 | num-workers = "default" 16 | 17 | # The maximum number of pending connections. This refers to the number of clients 18 | # that can be waiting to be served. Exceeding this number results in the client 19 | # getting an error when attempting to connect. It should only affect servers under 20 | # significant load. Generally set in the 64-2048 range. The default value is 2048. 21 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 22 | backlog = "default" 23 | 24 | # Sets the per-worker maximum number of concurrent connections. All socket listeners 25 | # will stop accepting connections when this limit is reached for each worker. 26 | # By default max connections is set to a 25k. 27 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 28 | max-connections = "default" 29 | 30 | # Sets the per-worker maximum concurrent connection establish process. All listeners 31 | # will stop accepting connections when this limit is reached. It can be used to limit 32 | # the global TLS CPU usage. By default max connections is set to a 256. 33 | # Takes a string value: Either "default", or an integer N > 0 e.g. "6". 34 | max-connection-rate = "default" 35 | 36 | # Set server keep-alive preference. By default keep alive is set to 5 seconds. 37 | # Takes a string value: Either "default", "disabled", "os", 38 | # or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds". 39 | keep-alive = "default" 40 | 41 | # Set server client timeout in milliseconds for first request. Defines a timeout 42 | # for reading client request header. If a client does not transmit the entire set of 43 | # headers within this time, the request is terminated with the 408 (Request Time-out) 44 | # error. To disable timeout, set the value to 0. 45 | # By default client timeout is set to 5000 milliseconds. 46 | # Takes a string value: Either "default", or a string of the format "N milliseconds" 47 | # where N is an integer > 0 e.g. "6 milliseconds". 48 | client-timeout = "default" 49 | 50 | # Set server connection shutdown timeout in milliseconds. Defines a timeout for 51 | # shutdown connection. If a shutdown procedure does not complete within this time, 52 | # the request is dropped. To disable timeout set value to 0. 53 | # By default client timeout is set to 5000 milliseconds. 54 | # Takes a string value: Either "default", or a string of the format "N milliseconds" 55 | # where N is an integer > 0 e.g. "6 milliseconds". 56 | client-shutdown = "default" 57 | 58 | # Timeout for graceful workers shutdown. After receiving a stop signal, workers have 59 | # this much time to finish serving requests. Workers still alive after the timeout 60 | # are force dropped. By default shutdown timeout sets to 30 seconds. 61 | # Takes a string value: Either "default", or a string of the format "N seconds" 62 | # where N is an integer > 0 e.g. "6 seconds". 63 | shutdown-timeout = "default" 64 | 65 | [actix.tls] # TLS is disabled by default because the certs don't exist 66 | enabled = false 67 | certificate = "path/to/cert/cert.pem" 68 | private-key = "path/to/cert/key.pem" 69 | 70 | # The `application` table be used to express application-specific settings. 71 | # See the `README.md` file for more details on how to use this. 72 | [application] 73 | -------------------------------------------------------------------------------- /actix-settings/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError}; 2 | 3 | use derive_more::derive::{Display, Error}; 4 | #[cfg(feature = "openssl")] 5 | use openssl::error::ErrorStack as OpenSSLError; 6 | use toml::de::Error as TomlError; 7 | 8 | /// Errors that can be returned from methods in this crate. 9 | #[derive(Debug, Display, Error)] 10 | pub enum Error { 11 | /// Environment variable does not exists or is invalid. 12 | #[display("Env var error: {_0}")] 13 | EnvVarError(VarError), 14 | 15 | /// File already exists on disk. 16 | #[display("File exists: {}", _0.display())] 17 | FileExists(#[error(not(source))] PathBuf), 18 | 19 | /// Invalid value. 20 | #[allow(missing_docs)] 21 | #[display("Expected {expected}, got {got} (@ {file}:{line}:{column})")] 22 | InvalidValue { 23 | expected: &'static str, 24 | got: String, 25 | file: &'static str, 26 | line: u32, 27 | column: u32, 28 | }, 29 | 30 | /// I/O error. 31 | #[display("I/O error: {_0}")] 32 | IoError(io::Error), 33 | 34 | /// OpenSSL Error. 35 | #[cfg(feature = "openssl")] 36 | #[display("OpenSSL error: {_0}")] 37 | OpenSSLError(OpenSSLError), 38 | 39 | /// Value is not a boolean. 40 | #[display("Failed to parse boolean: {_0}")] 41 | ParseBoolError(ParseBoolError), 42 | 43 | /// Value is not an integer. 44 | #[display("Failed to parse integer: {_0}")] 45 | ParseIntError(ParseIntError), 46 | 47 | /// Value is not an address. 48 | #[display("Failed to parse address: {_0}")] 49 | ParseAddressError(#[error(not(source))] String), 50 | 51 | /// Error deserializing as TOML. 52 | #[display("TOML error: {_0}")] 53 | TomlError(TomlError), 54 | } 55 | 56 | macro_rules! InvalidValue { 57 | (expected: $expected:expr, got: $got:expr,) => { 58 | crate::Error::InvalidValue { 59 | expected: $expected, 60 | got: $got.to_string(), 61 | file: file!(), 62 | line: line!(), 63 | column: column!(), 64 | } 65 | }; 66 | } 67 | 68 | impl From for Error { 69 | fn from(err: io::Error) -> Self { 70 | Self::IoError(err) 71 | } 72 | } 73 | 74 | #[cfg(feature = "openssl")] 75 | impl From for Error { 76 | fn from(err: OpenSSLError) -> Self { 77 | Self::OpenSSLError(err) 78 | } 79 | } 80 | 81 | impl From for Error { 82 | fn from(err: ParseBoolError) -> Self { 83 | Self::ParseBoolError(err) 84 | } 85 | } 86 | 87 | impl From for Error { 88 | fn from(err: ParseIntError) -> Self { 89 | Self::ParseIntError(err) 90 | } 91 | } 92 | 93 | impl From for Error { 94 | fn from(err: TomlError) -> Self { 95 | Self::TomlError(err) 96 | } 97 | } 98 | 99 | impl From for Error { 100 | fn from(err: VarError) -> Self { 101 | Self::EnvVarError(err) 102 | } 103 | } 104 | 105 | impl From for io::Error { 106 | fn from(err: Error) -> Self { 107 | match err { 108 | Error::EnvVarError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), 109 | 110 | Error::FileExists(_) => io::Error::new(io::ErrorKind::AlreadyExists, err.to_string()), 111 | 112 | Error::InvalidValue { .. } => { 113 | io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) 114 | } 115 | 116 | Error::IoError(io_error) => io_error, 117 | 118 | #[cfg(feature = "openssl")] 119 | Error::OpenSSLError(ossl_error) => io::Error::new(io::ErrorKind::Other, ossl_error), 120 | 121 | Error::ParseBoolError(_) => { 122 | io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) 123 | } 124 | 125 | Error::ParseIntError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), 126 | 127 | Error::ParseAddressError(_) => { 128 | io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) 129 | } 130 | 131 | Error::TomlError(_) => io::Error::new(io::ErrorKind::InvalidInput, err.to_string()), 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /actix-settings/src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use crate::Error; 4 | 5 | /// A specialized `FromStr` trait that returns [`Error`] errors 6 | pub trait Parse: Sized { 7 | /// Parse `Self` from `string`. 8 | fn parse(string: &str) -> Result; 9 | } 10 | 11 | impl Parse for bool { 12 | fn parse(string: &str) -> Result { 13 | Self::from_str(string).map_err(Error::from) 14 | } 15 | } 16 | 17 | macro_rules! impl_parse_for_int_type { 18 | ($($int_type:ty),+ $(,)?) => { 19 | $( 20 | impl Parse for $int_type { 21 | fn parse(string: &str) -> Result { 22 | Self::from_str(string).map_err(Error::from) 23 | } 24 | } 25 | )+ 26 | } 27 | } 28 | impl_parse_for_int_type![i8, i16, i32, i64, i128, u8, u16, u32, u64, u128]; 29 | 30 | impl Parse for String { 31 | fn parse(string: &str) -> Result { 32 | Ok(string.to_string()) 33 | } 34 | } 35 | 36 | impl Parse for PathBuf { 37 | fn parse(string: &str) -> Result { 38 | Ok(PathBuf::from(string)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /actix-settings/src/settings/address.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use regex::Regex; 3 | use serde::Deserialize; 4 | 5 | use crate::{Error, Parse}; 6 | 7 | static ADDR_REGEX: Lazy = Lazy::new(|| { 8 | Regex::new( 9 | r#"(?x) 10 | \[ # opening square bracket 11 | (\s)* # optional whitespace 12 | "(?P[^"]+)" # host name (string) 13 | , # separating comma 14 | (\s)* # optional whitespace 15 | (?P\d+) # port number (integer) 16 | (\s)* # optional whitespace 17 | \] # closing square bracket 18 | "#, 19 | ) 20 | .expect("Failed to compile regex: ADDR_REGEX") 21 | }); 22 | 23 | static ADDR_LIST_REGEX: Lazy = Lazy::new(|| { 24 | Regex::new( 25 | r#"(?x) 26 | \[ # opening square bracket (list) 27 | (\s)* # optional whitespace 28 | (?P( 29 | \[".*", (\s)* \d+\] # element 30 | (,)? # element separator 31 | (\s)* # optional whitespace 32 | )*) 33 | (\s)* # optional whitespace 34 | \] # closing square bracket (list) 35 | "#, 36 | ) 37 | .expect("Failed to compile regex: ADDRS_REGEX") 38 | }); 39 | 40 | /// A host/port pair for the server to bind to. 41 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] 42 | pub struct Address { 43 | /// Host part of address. 44 | pub host: String, 45 | 46 | /// Port part of address. 47 | pub port: u16, 48 | } 49 | 50 | impl Parse for Address { 51 | fn parse(string: &str) -> Result { 52 | let mut items = string 53 | .trim() 54 | .trim_start_matches('[') 55 | .trim_end_matches(']') 56 | .split(','); 57 | 58 | let parse_error = || Error::ParseAddressError(string.to_string()); 59 | 60 | if !ADDR_REGEX.is_match(string) { 61 | return Err(parse_error()); 62 | } 63 | 64 | Ok(Self { 65 | host: items.next().ok_or_else(parse_error)?.trim().to_string(), 66 | port: items.next().ok_or_else(parse_error)?.trim().parse()?, 67 | }) 68 | } 69 | } 70 | 71 | impl Parse for Vec
{ 72 | fn parse(string: &str) -> Result { 73 | let parse_error = || Error::ParseAddressError(string.to_string()); 74 | 75 | if !ADDR_LIST_REGEX.is_match(string) { 76 | return Err(parse_error()); 77 | } 78 | 79 | let mut addrs = vec![]; 80 | 81 | for list_caps in ADDR_LIST_REGEX.captures_iter(string) { 82 | let elements = &list_caps["elements"].trim(); 83 | for elt_caps in ADDR_REGEX.captures_iter(elements) { 84 | addrs.push(Address { 85 | host: elt_caps["host"].to_string(), 86 | port: elt_caps["port"].parse()?, 87 | }); 88 | } 89 | } 90 | 91 | Ok(addrs) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /actix-settings/src/settings/backlog.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de; 4 | 5 | use crate::{AsResult, Error, Parse}; 6 | 7 | /// The maximum number of pending connections. 8 | /// 9 | /// This refers to the number of clients that can be waiting to be served. Exceeding this number 10 | /// results in the client getting an error when attempting to connect. It should only affect servers 11 | /// under significant load. 12 | /// 13 | /// Generally set in the 64–2048 range. The default value is 2048. Takes a string value: Either 14 | /// "default", or an integer N > 0 e.g. "6". 15 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 16 | pub enum Backlog { 17 | /// The default number of connections. See struct docs. 18 | Default, 19 | 20 | /// A specific number of connections. 21 | Manual(usize), 22 | } 23 | 24 | impl Parse for Backlog { 25 | fn parse(string: &str) -> AsResult { 26 | match string { 27 | "default" => Ok(Backlog::Default), 28 | string => match string.parse::() { 29 | Ok(val) => Ok(Backlog::Manual(val)), 30 | Err(_) => Err(InvalidValue! { 31 | expected: "an integer > 0", 32 | got: string, 33 | }), 34 | }, 35 | } 36 | } 37 | } 38 | 39 | impl<'de> de::Deserialize<'de> for Backlog { 40 | fn deserialize(deserializer: D) -> Result 41 | where 42 | D: de::Deserializer<'de>, 43 | { 44 | struct BacklogVisitor; 45 | 46 | impl de::Visitor<'_> for BacklogVisitor { 47 | type Value = Backlog; 48 | 49 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | let msg = "Either \"default\" or a string containing an integer > 0"; 51 | f.write_str(msg) 52 | } 53 | 54 | fn visit_str(self, value: &str) -> Result 55 | where 56 | E: de::Error, 57 | { 58 | match Backlog::parse(value) { 59 | Ok(backlog) => Ok(backlog), 60 | Err(Error::InvalidValue { expected, got, .. }) => Err( 61 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 62 | ), 63 | Err(_) => unreachable!(), 64 | } 65 | } 66 | } 67 | 68 | deserializer.deserialize_string(BacklogVisitor) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /actix-settings/src/settings/keep_alive.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use once_cell::sync::Lazy; 4 | use regex::Regex; 5 | use serde::de; 6 | 7 | use crate::{AsResult, Error, Parse}; 8 | 9 | /// The server keep-alive preference. 10 | /// 11 | /// By default keep alive is set to 5 seconds. Takes a string value: Either "default", "disabled", 12 | /// "os", or a string of the format "N seconds" where N is an integer > 0 e.g. "6 seconds". 13 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 14 | pub enum KeepAlive { 15 | /// The default keep-alive as defined by Actix Web. 16 | Default, 17 | 18 | /// Disable keep-alive. 19 | Disabled, 20 | 21 | /// Let the OS determine keep-alive duration. 22 | /// 23 | /// Note: this is usually quite long. 24 | Os, 25 | 26 | /// A specific keep-alive duration (in seconds). 27 | Seconds(usize), 28 | } 29 | 30 | impl Parse for KeepAlive { 31 | fn parse(string: &str) -> AsResult { 32 | pub(crate) static FMT: Lazy = 33 | Lazy::new(|| Regex::new(r"^\d+ seconds$").expect("Failed to compile regex: FMT")); 34 | 35 | pub(crate) static DIGITS: Lazy = 36 | Lazy::new(|| Regex::new(r"^\d+").expect("Failed to compile regex: FMT")); 37 | 38 | macro_rules! invalid_value { 39 | ($got:expr) => { 40 | Err(InvalidValue! { 41 | expected: "a string of the format \"N seconds\" where N is an integer > 0", 42 | got: $got, 43 | }) 44 | }; 45 | } 46 | 47 | let digits_in = |m: regex::Match<'_>| &string[m.start()..m.end()]; 48 | match string { 49 | "default" => Ok(KeepAlive::Default), 50 | "disabled" => Ok(KeepAlive::Disabled), 51 | "OS" | "os" => Ok(KeepAlive::Os), 52 | string if !FMT.is_match(string) => invalid_value!(string), 53 | string => match DIGITS.find(string) { 54 | None => invalid_value!(string), 55 | Some(mat) => match digits_in(mat).parse() { 56 | Ok(val) => Ok(KeepAlive::Seconds(val)), 57 | Err(_) => invalid_value!(string), 58 | }, 59 | }, 60 | } 61 | } 62 | } 63 | 64 | impl<'de> de::Deserialize<'de> for KeepAlive { 65 | fn deserialize(deserializer: D) -> Result 66 | where 67 | D: de::Deserializer<'de>, 68 | { 69 | struct KeepAliveVisitor; 70 | 71 | impl de::Visitor<'_> for KeepAliveVisitor { 72 | type Value = KeepAlive; 73 | 74 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 75 | let msg = "Either \"default\", \"disabled\", \"os\", or a string of the format \"N seconds\" where N is an integer > 0"; 76 | f.write_str(msg) 77 | } 78 | 79 | fn visit_str(self, value: &str) -> Result 80 | where 81 | E: de::Error, 82 | { 83 | match KeepAlive::parse(value) { 84 | Ok(keep_alive) => Ok(keep_alive), 85 | Err(Error::InvalidValue { expected, got, .. }) => Err( 86 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 87 | ), 88 | Err(_) => unreachable!(), 89 | } 90 | } 91 | } 92 | 93 | deserializer.deserialize_string(KeepAliveVisitor) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /actix-settings/src/settings/max_connection_rate.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de; 4 | 5 | use crate::{AsResult, Error, Parse}; 6 | 7 | /// The maximum per-worker concurrent TLS connection limit. 8 | /// 9 | /// All listeners will stop accepting connections when this limit is reached. It can be used to 10 | /// limit the global TLS CPU usage. By default max connections is set to a 256. Takes a string 11 | /// value: Either "default", or an integer N > 0 e.g. "6". 12 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 | pub enum MaxConnectionRate { 14 | /// The default connection limit. See struct docs. 15 | Default, 16 | 17 | /// A specific connection limit. 18 | Manual(usize), 19 | } 20 | 21 | impl Parse for MaxConnectionRate { 22 | fn parse(string: &str) -> AsResult { 23 | match string { 24 | "default" => Ok(MaxConnectionRate::Default), 25 | string => match string.parse::() { 26 | Ok(val) => Ok(MaxConnectionRate::Manual(val)), 27 | Err(_) => Err(InvalidValue! { 28 | expected: "an integer > 0", 29 | got: string, 30 | }), 31 | }, 32 | } 33 | } 34 | } 35 | 36 | impl<'de> de::Deserialize<'de> for MaxConnectionRate { 37 | fn deserialize(deserializer: D) -> Result 38 | where 39 | D: de::Deserializer<'de>, 40 | { 41 | struct MaxConnectionRateVisitor; 42 | 43 | impl de::Visitor<'_> for MaxConnectionRateVisitor { 44 | type Value = MaxConnectionRate; 45 | 46 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | let msg = "Either \"default\" or a string containing an integer > 0"; 48 | f.write_str(msg) 49 | } 50 | 51 | fn visit_str(self, value: &str) -> Result 52 | where 53 | E: de::Error, 54 | { 55 | match MaxConnectionRate::parse(value) { 56 | Ok(max_connection_rate) => Ok(max_connection_rate), 57 | Err(Error::InvalidValue { expected, got, .. }) => Err( 58 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 59 | ), 60 | Err(_) => unreachable!(), 61 | } 62 | } 63 | } 64 | 65 | deserializer.deserialize_string(MaxConnectionRateVisitor) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /actix-settings/src/settings/max_connections.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de; 4 | 5 | use crate::{AsResult, Error, Parse}; 6 | 7 | /// The maximum per-worker number of concurrent connections. 8 | /// 9 | /// All socket listeners will stop accepting connections when this limit is reached for each worker. 10 | /// By default max connections is set to a 25k. Takes a string value: Either "default", or an 11 | /// integer N > 0 e.g. "6". 12 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 | pub enum MaxConnections { 14 | /// The default number of connections. See struct docs. 15 | Default, 16 | 17 | /// A specific number of connections. 18 | Manual(usize), 19 | } 20 | 21 | impl Parse for MaxConnections { 22 | fn parse(string: &str) -> AsResult { 23 | match string { 24 | "default" => Ok(MaxConnections::Default), 25 | string => match string.parse::() { 26 | Ok(val) => Ok(MaxConnections::Manual(val)), 27 | Err(_) => Err(InvalidValue! { 28 | expected: "an integer > 0", 29 | got: string, 30 | }), 31 | }, 32 | } 33 | } 34 | } 35 | 36 | impl<'de> de::Deserialize<'de> for MaxConnections { 37 | fn deserialize(deserializer: D) -> Result 38 | where 39 | D: de::Deserializer<'de>, 40 | { 41 | struct MaxConnectionsVisitor; 42 | 43 | impl de::Visitor<'_> for MaxConnectionsVisitor { 44 | type Value = MaxConnections; 45 | 46 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | let msg = "Either \"default\" or a string containing an integer > 0"; 48 | f.write_str(msg) 49 | } 50 | 51 | fn visit_str(self, value: &str) -> Result 52 | where 53 | E: de::Error, 54 | { 55 | match MaxConnections::parse(value) { 56 | Ok(max_connections) => Ok(max_connections), 57 | Err(Error::InvalidValue { expected, got, .. }) => Err( 58 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 59 | ), 60 | Err(_) => unreachable!(), 61 | } 62 | } 63 | } 64 | 65 | deserializer.deserialize_string(MaxConnectionsVisitor) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /actix-settings/src/settings/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | mod address; 4 | mod backlog; 5 | mod keep_alive; 6 | mod max_connection_rate; 7 | mod max_connections; 8 | mod mode; 9 | mod num_workers; 10 | mod timeout; 11 | #[cfg(feature = "openssl")] 12 | mod tls; 13 | 14 | #[cfg(feature = "openssl")] 15 | pub use self::tls::Tls; 16 | pub use self::{ 17 | address::Address, backlog::Backlog, keep_alive::KeepAlive, 18 | max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode, 19 | num_workers::NumWorkers, timeout::Timeout, 20 | }; 21 | 22 | /// Settings types for Actix Web. 23 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] 24 | #[serde(rename_all = "kebab-case")] 25 | pub struct ActixSettings { 26 | /// List of addresses for the server to bind to. 27 | pub hosts: Vec
, 28 | 29 | /// Marker of intended deployment environment. 30 | pub mode: Mode, 31 | 32 | /// True if the `Compress` middleware should be enabled. 33 | pub enable_compression: bool, 34 | 35 | /// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled. 36 | pub enable_log: bool, 37 | 38 | /// The number of workers that the server should start. 39 | pub num_workers: NumWorkers, 40 | 41 | /// The maximum number of pending connections. 42 | pub backlog: Backlog, 43 | 44 | /// The per-worker maximum number of concurrent connections. 45 | pub max_connections: MaxConnections, 46 | 47 | /// The per-worker maximum concurrent TLS connection limit. 48 | pub max_connection_rate: MaxConnectionRate, 49 | 50 | /// Server keep-alive preference. 51 | pub keep_alive: KeepAlive, 52 | 53 | /// Timeout duration for reading client request header. 54 | pub client_timeout: Timeout, 55 | 56 | /// Timeout duration for connection shutdown. 57 | pub client_shutdown: Timeout, 58 | 59 | /// Timeout duration for graceful worker shutdown. 60 | pub shutdown_timeout: Timeout, 61 | 62 | /// TLS (HTTPS) configuration. 63 | #[cfg(feature = "openssl")] 64 | pub tls: Tls, 65 | } 66 | -------------------------------------------------------------------------------- /actix-settings/src/settings/mode.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::{AsResult, Parse}; 4 | 5 | /// Marker of intended deployment environment. 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum Mode { 9 | /// Marks development environment. 10 | Development, 11 | 12 | /// Marks production environment. 13 | Production, 14 | } 15 | 16 | impl Parse for Mode { 17 | fn parse(string: &str) -> AsResult { 18 | match string { 19 | "development" => Ok(Self::Development), 20 | "production" => Ok(Self::Production), 21 | _ => Err(InvalidValue! { 22 | expected: "\"development\" | \"production\".", 23 | got: string, 24 | }), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /actix-settings/src/settings/num_workers.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::de; 4 | 5 | use crate::{AsResult, Error, Parse}; 6 | 7 | /// The number of workers that the server should start. 8 | /// 9 | /// By default the number of available logical cpu cores is used. Takes a string value: Either 10 | /// "default", or an integer N > 0 e.g. "6". 11 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 12 | pub enum NumWorkers { 13 | /// The default number of workers. See struct docs. 14 | Default, 15 | 16 | /// A specific number of workers. 17 | Manual(usize), 18 | } 19 | 20 | impl Parse for NumWorkers { 21 | fn parse(string: &str) -> AsResult { 22 | match string { 23 | "default" => Ok(NumWorkers::Default), 24 | string => match string.parse::() { 25 | Ok(val) => Ok(NumWorkers::Manual(val)), 26 | Err(_) => Err(InvalidValue! { 27 | expected: "a positive integer", 28 | got: string, 29 | }), 30 | }, 31 | } 32 | } 33 | } 34 | 35 | impl<'de> de::Deserialize<'de> for NumWorkers { 36 | fn deserialize(deserializer: D) -> Result 37 | where 38 | D: de::Deserializer<'de>, 39 | { 40 | struct NumWorkersVisitor; 41 | 42 | impl de::Visitor<'_> for NumWorkersVisitor { 43 | type Value = NumWorkers; 44 | 45 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | let msg = "Either \"default\" or a string containing an integer > 0"; 47 | f.write_str(msg) 48 | } 49 | 50 | fn visit_str(self, value: &str) -> Result 51 | where 52 | E: de::Error, 53 | { 54 | match NumWorkers::parse(value) { 55 | Ok(num_workers) => Ok(num_workers), 56 | Err(Error::InvalidValue { expected, got, .. }) => Err( 57 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 58 | ), 59 | Err(_) => unreachable!(), 60 | } 61 | } 62 | } 63 | 64 | deserializer.deserialize_string(NumWorkersVisitor) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /actix-settings/src/settings/timeout.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use once_cell::sync::Lazy; 4 | use regex::Regex; 5 | use serde::de; 6 | 7 | use crate::{AsResult, Error, Parse}; 8 | 9 | /// A timeout duration in milliseconds or seconds. 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 11 | pub enum Timeout { 12 | /// The default timeout. Depends on context. 13 | Default, 14 | 15 | /// Timeout in milliseconds. 16 | Milliseconds(usize), 17 | 18 | /// Timeout in seconds. 19 | Seconds(usize), 20 | } 21 | 22 | impl Parse for Timeout { 23 | fn parse(string: &str) -> AsResult { 24 | pub static FMT: Lazy = Lazy::new(|| { 25 | Regex::new(r"^\d+ (milliseconds|seconds)$").expect("Failed to compile regex: FMT") 26 | }); 27 | 28 | pub static DIGITS: Lazy = 29 | Lazy::new(|| Regex::new(r"^\d+").expect("Failed to compile regex: DIGITS")); 30 | 31 | pub static UNIT: Lazy = Lazy::new(|| { 32 | Regex::new(r"(milliseconds|seconds)$").expect("Failed to compile regex: UNIT") 33 | }); 34 | 35 | macro_rules! invalid_value { 36 | ($got:expr) => { 37 | Err(InvalidValue! { 38 | expected: "a string of the format \"N seconds\" or \"N milliseconds\" where N is an integer > 0", 39 | got: $got, 40 | }) 41 | } 42 | } 43 | 44 | match string { 45 | "default" => Ok(Timeout::Default), 46 | 47 | string if !FMT.is_match(string) => invalid_value!(string), 48 | 49 | string => match (DIGITS.find(string), UNIT.find(string)) { 50 | (None, _) | (_, None) => invalid_value!(string), 51 | 52 | (Some(digits), Some(unit)) => { 53 | let digits = &string[digits.range()]; 54 | let unit = &string[unit.range()]; 55 | 56 | match (digits.parse(), unit) { 57 | (Ok(n), "milliseconds") => Ok(Timeout::Milliseconds(n)), 58 | (Ok(n), "seconds") => Ok(Timeout::Seconds(n)), 59 | _ => invalid_value!(string), 60 | } 61 | } 62 | }, 63 | } 64 | } 65 | } 66 | 67 | impl<'de> de::Deserialize<'de> for Timeout { 68 | fn deserialize(deserializer: D) -> Result 69 | where 70 | D: de::Deserializer<'de>, 71 | { 72 | struct TimeoutVisitor; 73 | 74 | impl de::Visitor<'_> for TimeoutVisitor { 75 | type Value = Timeout; 76 | 77 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 78 | let msg = "Either \"default\", \"disabled\", \"os\", or a string of the format \"N seconds\" where N is an integer > 0"; 79 | f.write_str(msg) 80 | } 81 | 82 | fn visit_str(self, value: &str) -> Result 83 | where 84 | E: de::Error, 85 | { 86 | match Timeout::parse(value) { 87 | Ok(num_workers) => Ok(num_workers), 88 | Err(Error::InvalidValue { expected, got, .. }) => Err( 89 | de::Error::invalid_value(de::Unexpected::Str(&got), &expected), 90 | ), 91 | Err(_) => unreachable!(), 92 | } 93 | } 94 | } 95 | 96 | deserializer.deserialize_string(TimeoutVisitor) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /actix-settings/src/settings/tls.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; 4 | use serde::Deserialize; 5 | 6 | use crate::AsResult; 7 | 8 | /// TLS (HTTPS) configuration. 9 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] 10 | #[serde(rename_all = "kebab-case")] 11 | #[doc(alias = "ssl", alias = "https")] 12 | pub struct Tls { 13 | /// True if accepting TLS connections should be enabled. 14 | pub enabled: bool, 15 | 16 | /// Path to certificate `.pem` file. 17 | pub certificate: PathBuf, 18 | 19 | /// Path to private key `.pem` file. 20 | pub private_key: PathBuf, 21 | } 22 | 23 | impl Tls { 24 | /// Returns an [`SslAcceptorBuilder`] with the configured settings. 25 | /// 26 | /// The result is often used with [`actix_web::HttpServer::bind_openssl()`]. 27 | /// 28 | /// # Example 29 | /// 30 | /// ```no_run 31 | /// use std::io; 32 | /// use actix_settings::{ApplySettings as _, Settings}; 33 | /// use actix_web::{get, web, App, HttpServer, Responder}; 34 | /// 35 | /// #[actix_web::main] 36 | /// async fn main() -> io::Result<()> { 37 | /// let settings = Settings::from_default_template(); 38 | /// 39 | /// HttpServer::new(|| { 40 | /// App::new().route("/", web::to(|| async { "Hello, World!" })) 41 | /// }) 42 | /// .try_apply_settings(&settings)? 43 | /// .bind(("127.0.0.1", 8080))? 44 | /// .bind_openssl(("127.0.0.1", 8443), settings.actix.tls.get_ssl_acceptor_builder()?)? 45 | /// .run() 46 | /// .await 47 | /// } 48 | /// ``` 49 | pub fn get_ssl_acceptor_builder(&self) -> AsResult { 50 | let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; 51 | builder.set_certificate_chain_file(&self.certificate)?; 52 | builder.set_private_key_file(&self.private_key, SslFiletype::PEM)?; 53 | builder.check_private_key()?; 54 | 55 | Ok(builder) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /actix-web-httpauth/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Unreleased 4 | 5 | ## 0.8.2 6 | 7 | - Minimum supported Rust version (MSRV) is now 1.75. 8 | 9 | ## 0.8.1 10 | 11 | - Implement `From` for `BasicAuth`. 12 | - Minimum supported Rust version (MSRV) is now 1.68. 13 | 14 | ## 0.8.0 15 | 16 | - Removed `AuthExtractor` trait; implement `FromRequest` for your custom auth types. [#264] 17 | - `BasicAuth::user_id()` now returns `&str`. [#249] 18 | - `BasicAuth::password()` now returns `Option<&str>`. [#249] 19 | - `Basic::user_id()` now returns `&str`. [#264] 20 | - `Basic::password()` now returns `Option<&str>`. [#264] 21 | - `Bearer::token()` now returns `&str`. [#264] 22 | 23 | [#249]: https://github.com/actix/actix-extras/pull/249 24 | [#264]: https://github.com/actix/actix-extras/pull/264 25 | 26 | ## 0.7.0 27 | 28 | - Auth validator functions now need to return `(Error, ServiceRequest)` in error cases. [#260] 29 | - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. 30 | 31 | [#260]: https://github.com/actix/actix-extras/pull/260 32 | 33 | ## 0.6.0 34 | 35 | - Update `actix-web` dependency to `4`. 36 | 37 | ## 0.6.0-beta.8 38 | 39 | - Relax body type bounds on middleware impl. [#223] 40 | - Update `actix-web` dependency to `4.0.0-rc.1`. 41 | 42 | [#223]: https://github.com/actix/actix-extras/pull/223 43 | 44 | ## 0.6.0-beta.7 45 | 46 | - Minimum supported Rust version (MSRV) is now 1.54. 47 | 48 | ## 0.6.0-beta.6 49 | 50 | - Update `actix-web` dependency to `4.0.0.beta-15`. [#216] 51 | 52 | [#216]: https://github.com/actix/actix-extras/pull/216 53 | 54 | ## 0.6.0-beta.5 55 | 56 | - Update `actix-web` dependency to `4.0.0.beta-14`. [#209] 57 | 58 | [#209]: https://github.com/actix/actix-extras/pull/209 59 | 60 | ## 0.6.0-beta.4 61 | 62 | - impl `AuthExtractor` trait for `Option` and `Result`. [#205] 63 | 64 | [#205]: https://github.com/actix/actix-extras/pull/205 65 | 66 | ## 0.6.0-beta.3 67 | 68 | - Update `actix-web` dependency to v4.0.0-beta.10. [#203] 69 | - Minimum supported Rust version (MSRV) is now 1.52. 70 | 71 | [#203]: https://github.com/actix/actix-extras/pull/203 72 | 73 | ## 0.6.0-beta.2 74 | 75 | - No notable changes. 76 | 77 | ## 0.6.0-beta.1 78 | 79 | - Update `actix-web` dependency to 4.0.0 beta. 80 | - Minimum supported Rust version (MSRV) is now 1.46.0. 81 | 82 | ## 0.5.1 83 | 84 | - Correct error handling when extracting auth details from request. [#128] 85 | 86 | [#128]: https://github.com/actix/actix-extras/pull/128 87 | 88 | ## 0.5.0 89 | 90 | - Update `actix-web` dependency to 3.0.0. 91 | - Minimum supported Rust version (MSRV) is now 1.42.0. 92 | 93 | ## 0.4.2 94 | 95 | - Update the `base64` dependency to 0.12 96 | - AuthenticationError's status code is preserved when converting to a ResponseError 97 | - Minimize `futures` dependency 98 | - Fix panic on `AuthenticationMiddleware` [#69] 99 | 100 | [#69]: https://github.com/actix/actix-web-httpauth/pull/69 101 | 102 | ## 0.4.1 103 | 104 | - Move repository to actix-extras 105 | 106 | ## 0.4.0 107 | 108 | - Depends on `actix-web = "^2.0"`, `actix-service = "^1.0"`, and `futures = "^0.3"` version now ([#14]) 109 | - Depends on `bytes = "^0.5"` and `base64 = "^0.11"` now 110 | 111 | [#14]: https://github.com/actix/actix-web-httpauth/pull/14 112 | 113 | ## 0.3.2 - 2019-07-19 114 | 115 | - Middleware accepts any `Fn` as a validator function instead of `FnMut` [#11] 116 | 117 | [#11]: https://github.com/actix/actix-web-httpauth/pull/11 118 | 119 | ## 0.3.1 - 2019-06-09 120 | 121 | - Multiple calls to the middleware would result in panic 122 | 123 | ## 0.3.0 - 2019-06-05 124 | 125 | - Crate edition was changed to `2018`, same as `actix-web` 126 | - Depends on `actix-web = "^1.0"` version now 127 | - `WWWAuthenticate` header struct was renamed into `WwwAuthenticate` 128 | - Challenges and extractor configs are now operating with `Cow<'static, str>` types instead of `String` types 129 | 130 | ## 0.2.0 - 2019-04-26 131 | 132 | - `actix-web` dependency is used without default features now [#6] 133 | - `base64` dependency version was bumped to `0.10` 134 | 135 | [#6]: https://github.com/actix/actix-web-httpauth/pull/6 136 | 137 | ## 0.1.0 - 2018-09-08 138 | 139 | - Update to `actix-web = "0.7"` version 140 | 141 | ## 0.0.4 - 2018-07-01 142 | 143 | - Fix possible panic at `IntoHeaderValue` implementation for `headers::authorization::Basic` 144 | - Fix possible panic at `headers::www_authenticate::challenge::bearer::Bearer::to_bytes` call 145 | -------------------------------------------------------------------------------- /actix-web-httpauth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-httpauth" 3 | version = "0.8.2" 4 | description = "HTTP authentication schemes for Actix Web" 5 | categories = ["web-programming"] 6 | keywords = ["http", "web", "framework", "authentication", "security"] 7 | authors = [ 8 | "svartalf ", 9 | "Yuki Okushi ", 10 | ] 11 | repository.workspace = true 12 | homepage.workspace = true 13 | license.workspace = true 14 | edition.workspace = true 15 | rust-version.workspace = true 16 | 17 | [package.metadata.docs.rs] 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | all-features = true 20 | 21 | [dependencies] 22 | actix-utils = "3" 23 | actix-web = { version = "4.1", default-features = false } 24 | 25 | base64 = "0.22" 26 | futures-core = "0.3.17" 27 | futures-util = { version = "0.3.17", default-features = false, features = ["std"] } 28 | log = "0.4" 29 | pin-project-lite = "0.2.7" 30 | 31 | [dev-dependencies] 32 | actix-cors = "0.7" 33 | actix-service = "2" 34 | actix-web = { version = "4.1", default-features = false, features = ["macros"] } 35 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 36 | tracing = "0.1.30" 37 | 38 | [lints] 39 | workspace = true 40 | -------------------------------------------------------------------------------- /actix-web-httpauth/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-web-httpauth/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-web-httpauth/README.md: -------------------------------------------------------------------------------- 1 | # actix-web-httpauth 2 | 3 | > HTTP authentication schemes for [Actix Web](https://actix.rs). 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth) 8 | [![Documentation](https://docs.rs/actix-web-httpauth/badge.svg?version=0.8.2)](https://docs.rs/actix-web-httpauth/0.8.2) 9 | ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-web-httpauth) 10 | [![Dependency Status](https://deps.rs/crate/actix-web-httpauth/0.8.2/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.8.2) 11 | 12 | 13 | 14 | ## Documentation & Resources 15 | 16 | - [API Documentation](https://docs.rs/actix-web-httpauth/) 17 | - Minimum Supported Rust Version (MSRV): 1.57 18 | 19 | ## Features 20 | 21 | - Typed [Authorization] and [WWW-Authenticate] headers 22 | - [Extractors] for authorization headers 23 | - [Middleware] for easier authorization checking 24 | 25 | All supported schemas can be used in both middleware and request handlers. 26 | 27 | ## Supported Schemes 28 | 29 | - [HTTP Basic](https://tools.ietf.org/html/rfc7617) 30 | - [OAuth Bearer](https://tools.ietf.org/html/rfc6750) 31 | 32 | 33 | 34 | [Authorization]: https://docs.rs/actix-web-httpauth/*/actix_web_httpauth/headers/authorization/index.html 35 | [WWW-Authenticate]: https://docs.rs/actix-web-httpauth/*/actix_web_httpauth/headers/www_authenticate/index.html 36 | [Extractors]: https://actix.rs/docs/extractors/ 37 | [Middleware]: https://docs.rs/actix-web-httpauth/*/actix_web_httpauth/middleware/index.html 38 | -------------------------------------------------------------------------------- /actix-web-httpauth/examples/middleware-closure.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{middleware, web, App, HttpServer}; 2 | use actix_web_httpauth::middleware::HttpAuthentication; 3 | 4 | #[actix_web::main] 5 | async fn main() -> std::io::Result<()> { 6 | HttpServer::new(|| { 7 | let auth = HttpAuthentication::basic(|req, _credentials| async { Ok(req) }); 8 | App::new() 9 | .wrap(middleware::Logger::default()) 10 | .wrap(auth) 11 | .service(web::resource("/").to(|| async { "Test\r\n" })) 12 | }) 13 | .bind("127.0.0.1:8080")? 14 | .workers(1) 15 | .run() 16 | .await 17 | } 18 | -------------------------------------------------------------------------------- /actix-web-httpauth/examples/middleware.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | dev::ServiceRequest, error, get, middleware::Logger, App, Error, HttpServer, Responder, 3 | }; 4 | use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; 5 | use tracing::level_filters::LevelFilter; 6 | use tracing_subscriber::EnvFilter; 7 | 8 | /// Validator that: 9 | /// - accepts Bearer auth; 10 | /// - returns a custom response for requests without a valid Bearer Authorization header; 11 | /// - rejects tokens containing an "x" (for quick testing using command line HTTP clients). 12 | async fn validator( 13 | req: ServiceRequest, 14 | credentials: Option, 15 | ) -> Result { 16 | let Some(credentials) = credentials else { 17 | return Err((error::ErrorBadRequest("no bearer header"), req)); 18 | }; 19 | 20 | eprintln!("{credentials:?}"); 21 | 22 | if credentials.token().contains('x') { 23 | return Err((error::ErrorBadRequest("token contains x"), req)); 24 | } 25 | 26 | Ok(req) 27 | } 28 | 29 | #[get("/")] 30 | async fn index(auth: BearerAuth) -> impl Responder { 31 | format!("authenticated for token: {}", auth.token().to_owned()) 32 | } 33 | 34 | #[actix_web::main] 35 | async fn main() -> std::io::Result<()> { 36 | tracing_subscriber::fmt() 37 | .with_env_filter( 38 | EnvFilter::builder() 39 | .with_default_directive(LevelFilter::INFO.into()) 40 | .from_env_lossy(), 41 | ) 42 | .without_time() 43 | .init(); 44 | 45 | HttpServer::new(|| { 46 | let auth = HttpAuthentication::with_fn(validator); 47 | 48 | App::new() 49 | .service(index) 50 | .wrap(auth) 51 | .wrap(Logger::default().log_target("@")) 52 | }) 53 | .bind("127.0.0.1:8080")? 54 | .workers(2) 55 | .run() 56 | .await 57 | } 58 | -------------------------------------------------------------------------------- /actix-web-httpauth/examples/with-cors.rs: -------------------------------------------------------------------------------- 1 | use actix_cors::Cors; 2 | use actix_web::{dev::ServiceRequest, get, App, Error, HttpResponse, HttpServer}; 3 | use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; 4 | 5 | async fn ok_validator( 6 | req: ServiceRequest, 7 | credentials: BearerAuth, 8 | ) -> Result { 9 | eprintln!("{credentials:?}"); 10 | Ok(req) 11 | } 12 | 13 | #[get("/")] 14 | async fn index() -> HttpResponse { 15 | HttpResponse::Ok().finish() 16 | } 17 | 18 | #[actix_web::main] 19 | async fn main() -> std::io::Result<()> { 20 | HttpServer::new(move || { 21 | App::new() 22 | .wrap(HttpAuthentication::bearer(ok_validator)) 23 | // ensure the CORS middleware is wrapped around the httpauth middleware so it is able to 24 | // add headers to error responses 25 | .wrap(Cors::permissive()) 26 | .service(index) 27 | }) 28 | .bind(("127.0.0.1", 8080))? 29 | .run() 30 | .await 31 | } 32 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/extractors/basic.rs: -------------------------------------------------------------------------------- 1 | //! Extractor for the "Basic" HTTP Authentication Scheme. 2 | 3 | use std::borrow::Cow; 4 | 5 | use actix_utils::future::{ready, Ready}; 6 | use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest}; 7 | 8 | use super::{config::AuthExtractorConfig, errors::AuthenticationError}; 9 | use crate::headers::{ 10 | authorization::{Authorization, Basic}, 11 | www_authenticate::basic::Basic as Challenge, 12 | }; 13 | 14 | /// [`BasicAuth`] extractor configuration used for [`WWW-Authenticate`] header later. 15 | /// 16 | /// [`WWW-Authenticate`]: crate::headers::www_authenticate::WwwAuthenticate 17 | #[derive(Debug, Clone, Default)] 18 | pub struct Config(Challenge); 19 | 20 | impl Config { 21 | /// Set challenge `realm` attribute. 22 | /// 23 | /// The "realm" attribute indicates the scope of protection in the manner described in HTTP/1.1 24 | /// [RFC 2617 §1.2](https://tools.ietf.org/html/rfc2617#section-1.2). 25 | pub fn realm(mut self, value: T) -> Config 26 | where 27 | T: Into>, 28 | { 29 | self.0.realm = Some(value.into()); 30 | self 31 | } 32 | } 33 | 34 | impl AsRef for Config { 35 | fn as_ref(&self) -> &Challenge { 36 | &self.0 37 | } 38 | } 39 | 40 | impl AuthExtractorConfig for Config { 41 | type Inner = Challenge; 42 | 43 | fn into_inner(self) -> Self::Inner { 44 | self.0 45 | } 46 | } 47 | 48 | /// Extractor for HTTP Basic auth. 49 | /// 50 | /// # Examples 51 | /// ``` 52 | /// use actix_web_httpauth::extractors::basic::BasicAuth; 53 | /// 54 | /// async fn index(auth: BasicAuth) -> String { 55 | /// format!("Hello, {}!", auth.user_id()) 56 | /// } 57 | /// ``` 58 | /// 59 | /// If authentication fails, this extractor fetches the [`Config`] instance from the [app data] in 60 | /// order to properly form the `WWW-Authenticate` response header. 61 | /// 62 | /// # Examples 63 | /// ``` 64 | /// use actix_web::{web, App}; 65 | /// use actix_web_httpauth::extractors::basic::{self, BasicAuth}; 66 | /// 67 | /// async fn index(auth: BasicAuth) -> String { 68 | /// format!("Hello, {}!", auth.user_id()) 69 | /// } 70 | /// 71 | /// App::new() 72 | /// .app_data(basic::Config::default().realm("Restricted area")) 73 | /// .service(web::resource("/index.html").route(web::get().to(index))); 74 | /// ``` 75 | /// 76 | /// [app data]: https://docs.rs/actix-web/4/actix_web/struct.App.html#method.app_data 77 | #[derive(Debug, Clone)] 78 | pub struct BasicAuth(Basic); 79 | 80 | impl BasicAuth { 81 | /// Returns client's user-ID. 82 | pub fn user_id(&self) -> &str { 83 | self.0.user_id() 84 | } 85 | 86 | /// Returns client's password. 87 | pub fn password(&self) -> Option<&str> { 88 | self.0.password() 89 | } 90 | } 91 | 92 | impl From for BasicAuth { 93 | fn from(basic: Basic) -> Self { 94 | Self(basic) 95 | } 96 | } 97 | 98 | impl FromRequest for BasicAuth { 99 | type Future = Ready>; 100 | type Error = AuthenticationError; 101 | 102 | fn from_request(req: &HttpRequest, _: &mut Payload) -> ::Future { 103 | ready( 104 | Authorization::::parse(req) 105 | .map(|auth| BasicAuth(auth.into_scheme())) 106 | .map_err(|err| { 107 | log::debug!("`BasicAuth` extract error: {}", err); 108 | 109 | let challenge = req 110 | .app_data::() 111 | .map(|config| config.0.clone()) 112 | .unwrap_or_default(); 113 | 114 | AuthenticationError::new(challenge) 115 | }), 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/extractors/bearer.rs: -------------------------------------------------------------------------------- 1 | //! Extractor for the "Bearer" HTTP Authentication Scheme. 2 | 3 | use std::{borrow::Cow, default::Default}; 4 | 5 | use actix_utils::future::{ready, Ready}; 6 | use actix_web::{dev::Payload, http::header::Header, FromRequest, HttpRequest}; 7 | 8 | use super::{config::AuthExtractorConfig, errors::AuthenticationError}; 9 | pub use crate::headers::www_authenticate::bearer::Error; 10 | use crate::headers::{authorization, www_authenticate::bearer}; 11 | 12 | /// [`BearerAuth`] extractor configuration. 13 | #[derive(Debug, Clone, Default)] 14 | pub struct Config(bearer::Bearer); 15 | 16 | impl Config { 17 | /// Set challenge `scope` attribute. 18 | /// 19 | /// The `"scope"` attribute is a space-delimited list of case-sensitive 20 | /// scope values indicating the required scope of the access token for 21 | /// accessing the requested resource. 22 | pub fn scope>>(mut self, value: T) -> Config { 23 | self.0.scope = Some(value.into()); 24 | self 25 | } 26 | 27 | /// Set challenge `realm` attribute. 28 | /// 29 | /// The "realm" attribute indicates the scope of protection in the manner 30 | /// described in HTTP/1.1 [RFC 2617](https://tools.ietf.org/html/rfc2617#section-1.2). 31 | pub fn realm>>(mut self, value: T) -> Config { 32 | self.0.realm = Some(value.into()); 33 | self 34 | } 35 | } 36 | 37 | impl AsRef for Config { 38 | fn as_ref(&self) -> &bearer::Bearer { 39 | &self.0 40 | } 41 | } 42 | 43 | impl AuthExtractorConfig for Config { 44 | type Inner = bearer::Bearer; 45 | 46 | fn into_inner(self) -> Self::Inner { 47 | self.0 48 | } 49 | } 50 | 51 | /// Extractor for HTTP Bearer auth 52 | /// 53 | /// # Examples 54 | /// ``` 55 | /// use actix_web_httpauth::extractors::bearer::BearerAuth; 56 | /// 57 | /// async fn index(auth: BearerAuth) -> String { 58 | /// format!("Hello, user with token {}!", auth.token()) 59 | /// } 60 | /// ``` 61 | /// 62 | /// If authentication fails, this extractor fetches the [`Config`] instance 63 | /// from the [app data] in order to properly form the `WWW-Authenticate` 64 | /// response header. 65 | /// 66 | /// # Examples 67 | /// ``` 68 | /// use actix_web::{web, App}; 69 | /// use actix_web_httpauth::extractors::bearer::{self, BearerAuth}; 70 | /// 71 | /// async fn index(auth: BearerAuth) -> String { 72 | /// format!("Hello, {}!", auth.token()) 73 | /// } 74 | /// 75 | /// App::new() 76 | /// .app_data( 77 | /// bearer::Config::default() 78 | /// .realm("Restricted area") 79 | /// .scope("email photo"), 80 | /// ) 81 | /// .service(web::resource("/index.html").route(web::get().to(index))); 82 | /// ``` 83 | #[derive(Debug, Clone)] 84 | pub struct BearerAuth(authorization::Bearer); 85 | 86 | impl BearerAuth { 87 | /// Returns bearer token provided by client. 88 | pub fn token(&self) -> &str { 89 | self.0.token() 90 | } 91 | } 92 | 93 | impl FromRequest for BearerAuth { 94 | type Future = Ready>; 95 | type Error = AuthenticationError; 96 | 97 | fn from_request(req: &HttpRequest, _payload: &mut Payload) -> ::Future { 98 | ready( 99 | authorization::Authorization::::parse(req) 100 | .map(|auth| BearerAuth(auth.into_scheme())) 101 | .map_err(|_| { 102 | let bearer = req 103 | .app_data::() 104 | .map(|config| config.0.clone()) 105 | .unwrap_or_default(); 106 | 107 | AuthenticationError::new(bearer) 108 | }), 109 | ) 110 | } 111 | } 112 | 113 | /// Extended error customization for HTTP `Bearer` auth. 114 | impl AuthenticationError { 115 | /// Attach `Error` to the current Authentication error. 116 | /// 117 | /// Error status code will be changed to the one provided by the `kind` 118 | /// Error. 119 | pub fn with_error(mut self, kind: Error) -> Self { 120 | *self.status_code_mut() = kind.status_code(); 121 | self.challenge_mut().error = Some(kind); 122 | self 123 | } 124 | 125 | /// Attach error description to the current Authentication error. 126 | pub fn with_error_description(mut self, desc: T) -> Self 127 | where 128 | T: Into>, 129 | { 130 | self.challenge_mut().error_description = Some(desc.into()); 131 | self 132 | } 133 | 134 | /// Attach error URI to the current Authentication error. 135 | /// 136 | /// It is up to implementor to provide properly formed absolute URI. 137 | pub fn with_error_uri(mut self, uri: T) -> Self 138 | where 139 | T: Into>, 140 | { 141 | self.challenge_mut().error_uri = Some(uri.into()); 142 | self 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/extractors/config.rs: -------------------------------------------------------------------------------- 1 | use super::AuthenticationError; 2 | use crate::headers::www_authenticate::Challenge; 3 | 4 | /// Trait implemented for types that provides configuration for the authentication 5 | /// [extractors](crate::extractors). 6 | pub trait AuthExtractorConfig { 7 | /// Associated challenge type. 8 | type Inner: Challenge; 9 | 10 | /// Convert the config instance into a HTTP challenge. 11 | fn into_inner(self) -> Self::Inner; 12 | } 13 | 14 | impl From for AuthenticationError<::Inner> 15 | where 16 | T: AuthExtractorConfig, 17 | { 18 | fn from(config: T) -> Self { 19 | AuthenticationError::new(config.into_inner()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/extractors/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use actix_web::{http::StatusCode, HttpResponse, ResponseError}; 4 | 5 | use crate::headers::www_authenticate::{Challenge, WwwAuthenticate}; 6 | 7 | /// Authentication error returned by authentication extractors. 8 | /// 9 | /// Different extractors may extend `AuthenticationError` implementation in order to provide access 10 | /// inner challenge fields. 11 | #[derive(Debug)] 12 | pub struct AuthenticationError { 13 | challenge: C, 14 | status_code: StatusCode, 15 | } 16 | 17 | impl AuthenticationError { 18 | /// Creates new authentication error from the provided `challenge`. 19 | /// 20 | /// By default returned error will resolve into the `HTTP 401` status code. 21 | pub fn new(challenge: C) -> AuthenticationError { 22 | AuthenticationError { 23 | challenge, 24 | status_code: StatusCode::UNAUTHORIZED, 25 | } 26 | } 27 | 28 | /// Returns mutable reference to the inner challenge instance. 29 | pub fn challenge_mut(&mut self) -> &mut C { 30 | &mut self.challenge 31 | } 32 | 33 | /// Returns mutable reference to the inner status code. 34 | /// 35 | /// Can be used to override returned status code, but by default this lib tries to stick to the 36 | /// RFC, so it might be unreasonable. 37 | pub fn status_code_mut(&mut self) -> &mut StatusCode { 38 | &mut self.status_code 39 | } 40 | } 41 | 42 | impl fmt::Display for AuthenticationError { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | fmt::Display::fmt(&self.status_code, f) 45 | } 46 | } 47 | 48 | impl Error for AuthenticationError {} 49 | 50 | impl ResponseError for AuthenticationError { 51 | fn status_code(&self) -> StatusCode { 52 | self.status_code 53 | } 54 | 55 | fn error_response(&self) -> HttpResponse { 56 | HttpResponse::build(self.status_code()) 57 | .insert_header(WwwAuthenticate(self.challenge.clone())) 58 | .finish() 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use actix_web::Error; 65 | 66 | use super::*; 67 | use crate::headers::www_authenticate::basic::Basic; 68 | 69 | #[test] 70 | fn test_status_code_is_preserved_across_error_conversions() { 71 | let ae = AuthenticationError::new(Basic::default()); 72 | let expected = ae.status_code; 73 | 74 | // Converting the AuthenticationError into a ResponseError should preserve the status code. 75 | let err = Error::from(ae); 76 | let res_err = err.as_response_error(); 77 | assert_eq!(expected, res_err.status_code()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/extractors/mod.rs: -------------------------------------------------------------------------------- 1 | //! Type-safe authentication information extractors. 2 | 3 | pub mod basic; 4 | pub mod bearer; 5 | mod config; 6 | mod errors; 7 | 8 | pub use self::{config::AuthExtractorConfig, errors::AuthenticationError}; 9 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/authorization/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, str}; 2 | 3 | use actix_web::http::header; 4 | 5 | /// Possible errors while parsing `Authorization` header. 6 | /// 7 | /// Should not be used directly unless you are implementing your own 8 | /// [authentication scheme](super::Scheme). 9 | #[derive(Debug)] 10 | pub enum ParseError { 11 | /// Header value is malformed. 12 | Invalid, 13 | 14 | /// Authentication scheme is missing. 15 | MissingScheme, 16 | 17 | /// Required authentication field is missing. 18 | MissingField(&'static str), 19 | 20 | /// Unable to convert header into the str. 21 | ToStrError(header::ToStrError), 22 | 23 | /// Malformed base64 string. 24 | Base64DecodeError(base64::DecodeError), 25 | 26 | /// Malformed UTF-8 string. 27 | Utf8Error(str::Utf8Error), 28 | } 29 | 30 | impl fmt::Display for ParseError { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | match self { 33 | ParseError::Invalid => f.write_str("Invalid header value"), 34 | ParseError::MissingScheme => f.write_str("Missing authorization scheme"), 35 | ParseError::MissingField(field) => write!(f, "Missing header field ({field})"), 36 | ParseError::ToStrError(err) => fmt::Display::fmt(err, f), 37 | ParseError::Base64DecodeError(err) => fmt::Display::fmt(err, f), 38 | ParseError::Utf8Error(err) => fmt::Display::fmt(err, f), 39 | } 40 | } 41 | } 42 | 43 | impl Error for ParseError { 44 | fn source(&self) -> Option<&(dyn Error + 'static)> { 45 | match self { 46 | ParseError::Invalid => None, 47 | ParseError::MissingScheme => None, 48 | ParseError::MissingField(_) => None, 49 | ParseError::ToStrError(err) => Some(err), 50 | ParseError::Base64DecodeError(err) => Some(err), 51 | ParseError::Utf8Error(err) => Some(err), 52 | } 53 | } 54 | } 55 | 56 | impl From for ParseError { 57 | fn from(err: header::ToStrError) -> Self { 58 | ParseError::ToStrError(err) 59 | } 60 | } 61 | 62 | impl From for ParseError { 63 | fn from(err: base64::DecodeError) -> Self { 64 | ParseError::Base64DecodeError(err) 65 | } 66 | } 67 | 68 | impl From for ParseError { 69 | fn from(err: str::Utf8Error) -> Self { 70 | ParseError::Utf8Error(err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/authorization/header.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use actix_web::{ 4 | error::ParseError, 5 | http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, 6 | HttpMessage, 7 | }; 8 | 9 | use crate::headers::authorization::scheme::Scheme; 10 | 11 | /// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2) 12 | /// 13 | /// The "Authorization" header field allows a user agent to authenticate itself with an origin 14 | /// server—usually, but not necessarily, after receiving a 401 (Unauthorized) response. Its value 15 | /// consists of credentials containing the authentication information of the user agent for the 16 | /// realm of the resource being requested. 17 | /// 18 | /// `Authorization` is generic over an [authentication scheme](Scheme). 19 | /// 20 | /// # Examples 21 | /// ``` 22 | /// # use actix_web::{HttpRequest, Result, http::header::Header}; 23 | /// # use actix_web_httpauth::headers::authorization::{Authorization, Basic}; 24 | /// fn handler(req: HttpRequest) -> Result { 25 | /// let auth = Authorization::::parse(&req)?; 26 | /// 27 | /// Ok(format!("Hello, {}!", auth.as_ref().user_id())) 28 | /// } 29 | /// ``` 30 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 31 | pub struct Authorization(S); 32 | 33 | impl Authorization { 34 | /// Consumes `Authorization` header and returns inner [`Scheme`] implementation. 35 | pub fn into_scheme(self) -> S { 36 | self.0 37 | } 38 | } 39 | 40 | impl From for Authorization { 41 | fn from(scheme: S) -> Authorization { 42 | Authorization(scheme) 43 | } 44 | } 45 | 46 | impl AsRef for Authorization { 47 | fn as_ref(&self) -> &S { 48 | &self.0 49 | } 50 | } 51 | 52 | impl AsMut for Authorization { 53 | fn as_mut(&mut self) -> &mut S { 54 | &mut self.0 55 | } 56 | } 57 | 58 | impl fmt::Display for Authorization { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | fmt::Display::fmt(&self.0, f) 61 | } 62 | } 63 | 64 | impl Header for Authorization { 65 | #[inline] 66 | fn name() -> HeaderName { 67 | AUTHORIZATION 68 | } 69 | 70 | fn parse(msg: &T) -> Result { 71 | let header = msg.headers().get(Self::name()).ok_or(ParseError::Header)?; 72 | let scheme = S::parse(header).map_err(|_| ParseError::Header)?; 73 | 74 | Ok(Authorization(scheme)) 75 | } 76 | } 77 | 78 | impl TryIntoHeaderValue for Authorization { 79 | type Error = ::Error; 80 | 81 | fn try_into_value(self) -> Result { 82 | self.0.try_into_value() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/authorization/mod.rs: -------------------------------------------------------------------------------- 1 | //! `Authorization` header and various auth schemes. 2 | 3 | mod errors; 4 | mod header; 5 | mod scheme; 6 | 7 | pub use self::{ 8 | errors::ParseError, 9 | header::Authorization, 10 | scheme::{basic::Basic, bearer::Bearer, Scheme}, 11 | }; 12 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/authorization/scheme/bearer.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt}; 2 | 3 | use actix_web::{ 4 | http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}, 5 | web::{BufMut, BytesMut}, 6 | }; 7 | 8 | use crate::headers::authorization::{errors::ParseError, scheme::Scheme}; 9 | 10 | /// Credentials for `Bearer` authentication scheme, defined in [RFC 6750]. 11 | /// 12 | /// Should be used in combination with [`Authorization`] header. 13 | /// 14 | /// [RFC 6750]: https://tools.ietf.org/html/rfc6750 15 | /// [`Authorization`]: crate::headers::authorization::Authorization 16 | #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] 17 | pub struct Bearer { 18 | token: Cow<'static, str>, 19 | } 20 | 21 | impl Bearer { 22 | /// Creates new `Bearer` credentials with the token provided. 23 | /// 24 | /// # Example 25 | /// ``` 26 | /// # use actix_web_httpauth::headers::authorization::Bearer; 27 | /// let credentials = Bearer::new("mF_9.B5f-4.1JqM"); 28 | /// ``` 29 | pub fn new(token: T) -> Bearer 30 | where 31 | T: Into>, 32 | { 33 | Bearer { 34 | token: token.into(), 35 | } 36 | } 37 | 38 | /// Gets reference to the credentials token. 39 | pub fn token(&self) -> &str { 40 | self.token.as_ref() 41 | } 42 | } 43 | 44 | impl Scheme for Bearer { 45 | fn parse(header: &HeaderValue) -> Result { 46 | // "Bearer *" length 47 | if header.len() < 8 { 48 | return Err(ParseError::Invalid); 49 | } 50 | 51 | let mut parts = header.to_str()?.splitn(2, ' '); 52 | 53 | match parts.next() { 54 | Some("Bearer") => {} 55 | _ => return Err(ParseError::MissingScheme), 56 | } 57 | 58 | let token = parts.next().ok_or(ParseError::Invalid)?; 59 | 60 | Ok(Bearer { 61 | token: token.to_string().into(), 62 | }) 63 | } 64 | } 65 | 66 | impl fmt::Debug for Bearer { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | f.write_fmt(format_args!("Bearer ******")) 69 | } 70 | } 71 | 72 | impl fmt::Display for Bearer { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | f.write_fmt(format_args!("Bearer {}", self.token)) 75 | } 76 | } 77 | 78 | impl TryIntoHeaderValue for Bearer { 79 | type Error = InvalidHeaderValue; 80 | 81 | fn try_into_value(self) -> Result { 82 | let mut buffer = BytesMut::with_capacity(7 + self.token.len()); 83 | buffer.put(&b"Bearer "[..]); 84 | buffer.extend_from_slice(self.token.as_bytes()); 85 | 86 | HeaderValue::from_maybe_shared(buffer.freeze()) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | 94 | #[test] 95 | fn test_parse_header() { 96 | let value = HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM"); 97 | let scheme = Bearer::parse(&value); 98 | 99 | assert!(scheme.is_ok()); 100 | let scheme = scheme.unwrap(); 101 | assert_eq!(scheme.token, "mF_9.B5f-4.1JqM"); 102 | } 103 | 104 | #[test] 105 | fn test_empty_header() { 106 | let value = HeaderValue::from_static(""); 107 | let scheme = Bearer::parse(&value); 108 | 109 | assert!(scheme.is_err()); 110 | } 111 | 112 | #[test] 113 | fn test_wrong_scheme() { 114 | let value = HeaderValue::from_static("OAuthToken foo"); 115 | let scheme = Bearer::parse(&value); 116 | 117 | assert!(scheme.is_err()); 118 | } 119 | 120 | #[test] 121 | fn test_missing_token() { 122 | let value = HeaderValue::from_static("Bearer "); 123 | let scheme = Bearer::parse(&value); 124 | 125 | assert!(scheme.is_err()); 126 | } 127 | 128 | #[test] 129 | fn test_into_header_value() { 130 | let bearer = Bearer::new("mF_9.B5f-4.1JqM"); 131 | 132 | let result = bearer.try_into_value(); 133 | assert!(result.is_ok()); 134 | assert_eq!( 135 | result.unwrap(), 136 | HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM") 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/authorization/scheme/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use actix_web::http::header::{HeaderValue, TryIntoHeaderValue}; 4 | 5 | pub mod basic; 6 | pub mod bearer; 7 | 8 | use crate::headers::authorization::errors::ParseError; 9 | 10 | /// Authentication scheme for [`Authorization`](super::Authorization) header. 11 | pub trait Scheme: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync { 12 | /// Try to parse an authentication scheme from the `Authorization` header. 13 | fn parse(header: &HeaderValue) -> Result; 14 | } 15 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Typed HTTP headers. 2 | 3 | pub mod authorization; 4 | pub mod www_authenticate; 5 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/challenge/basic.rs: -------------------------------------------------------------------------------- 1 | //! Challenge for the "Basic" HTTP Authentication Scheme. 2 | 3 | use std::{borrow::Cow, fmt, str}; 4 | 5 | use actix_web::{ 6 | http::header::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}, 7 | web::{BufMut, Bytes, BytesMut}, 8 | }; 9 | 10 | use super::Challenge; 11 | use crate::utils; 12 | 13 | /// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme, 14 | /// described in [RFC 7617](https://tools.ietf.org/html/rfc7617) 15 | /// 16 | /// # Examples 17 | /// ``` 18 | /// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; 19 | /// use actix_web_httpauth::headers::www_authenticate::basic::Basic; 20 | /// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate; 21 | /// 22 | /// fn index(_req: HttpRequest) -> HttpResponse { 23 | /// let challenge = Basic::with_realm("Restricted area"); 24 | /// 25 | /// HttpResponse::Unauthorized() 26 | /// .insert_header(WwwAuthenticate(challenge)) 27 | /// .finish() 28 | /// } 29 | /// ``` 30 | /// 31 | /// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html 32 | #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)] 33 | pub struct Basic { 34 | // "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A 35 | pub(crate) realm: Option>, 36 | } 37 | 38 | impl Basic { 39 | /// Creates new `Basic` challenge with an empty `realm` field. 40 | /// 41 | /// # Examples 42 | /// ``` 43 | /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; 44 | /// let challenge = Basic::new(); 45 | /// ``` 46 | pub fn new() -> Basic { 47 | Default::default() 48 | } 49 | 50 | /// Creates new `Basic` challenge from the provided `realm` field value. 51 | /// 52 | /// # Examples 53 | /// 54 | /// ``` 55 | /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; 56 | /// let challenge = Basic::with_realm("Restricted area"); 57 | /// ``` 58 | /// 59 | /// ``` 60 | /// # use actix_web_httpauth::headers::www_authenticate::basic::Basic; 61 | /// let my_realm = "Earth realm".to_string(); 62 | /// let challenge = Basic::with_realm(my_realm); 63 | /// ``` 64 | pub fn with_realm(value: T) -> Basic 65 | where 66 | T: Into>, 67 | { 68 | Basic { 69 | realm: Some(value.into()), 70 | } 71 | } 72 | } 73 | 74 | #[doc(hidden)] 75 | impl Challenge for Basic { 76 | fn to_bytes(&self) -> Bytes { 77 | // 5 is for `"Basic"`, 9 is for `"realm=\"\""` 78 | let length = 5 + self.realm.as_ref().map_or(0, |realm| realm.len() + 9); 79 | let mut buffer = BytesMut::with_capacity(length); 80 | buffer.put(&b"Basic"[..]); 81 | if let Some(ref realm) = self.realm { 82 | buffer.put(&b" realm=\""[..]); 83 | utils::put_quoted(&mut buffer, realm); 84 | buffer.put_u8(b'"'); 85 | } 86 | 87 | buffer.freeze() 88 | } 89 | } 90 | 91 | impl fmt::Display for Basic { 92 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 93 | let bytes = self.to_bytes(); 94 | let repr = str::from_utf8(&bytes) 95 | // Should not happen since challenges are crafted manually 96 | // from a `&'static str` or `String` 97 | .map_err(|_| fmt::Error)?; 98 | 99 | f.write_str(repr) 100 | } 101 | } 102 | 103 | impl TryIntoHeaderValue for Basic { 104 | type Error = InvalidHeaderValue; 105 | 106 | fn try_into_value(self) -> Result { 107 | HeaderValue::from_maybe_shared(self.to_bytes()) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_plain_into_header_value() { 117 | let challenge = Basic { realm: None }; 118 | 119 | let value = challenge.try_into_value(); 120 | assert!(value.is_ok()); 121 | let value = value.unwrap(); 122 | assert_eq!(value, "Basic"); 123 | } 124 | 125 | #[test] 126 | fn test_with_realm_into_header_value() { 127 | let challenge = Basic { 128 | realm: Some("Restricted area".into()), 129 | }; 130 | 131 | let value = challenge.try_into_value(); 132 | assert!(value.is_ok()); 133 | let value = value.unwrap(); 134 | assert_eq!(value, "Basic realm=\"Restricted area\""); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/builder.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use super::{Bearer, Error}; 4 | 5 | /// Builder for the [`Bearer`] challenge. 6 | /// 7 | /// It is up to implementor to fill all required fields, neither this `Builder` nor [`Bearer`] 8 | /// provide any validation. 9 | #[derive(Debug, Default)] 10 | pub struct BearerBuilder(Bearer); 11 | 12 | impl BearerBuilder { 13 | /// Provides the `scope` attribute, as defined in [RFC 6749 §3.3]. 14 | /// 15 | /// [RFC 6749 §3.3]: https://tools.ietf.org/html/rfc6749#section-3.3 16 | pub fn scope(mut self, value: T) -> Self 17 | where 18 | T: Into>, 19 | { 20 | self.0.scope = Some(value.into()); 21 | self 22 | } 23 | 24 | /// Provides the `realm` attribute, as defined in [RFC 2617]. 25 | /// 26 | /// [RFC 2617]: https://tools.ietf.org/html/rfc2617 27 | pub fn realm(mut self, value: T) -> Self 28 | where 29 | T: Into>, 30 | { 31 | self.0.realm = Some(value.into()); 32 | self 33 | } 34 | 35 | /// Provides the `error` attribute, as defined in [RFC 6750, Section 3.1]. 36 | /// 37 | /// [RFC 6750 §3.1]: https://tools.ietf.org/html/rfc6750#section-3.1 38 | pub fn error(mut self, value: Error) -> Self { 39 | self.0.error = Some(value); 40 | self 41 | } 42 | 43 | /// Provides the `error_description` attribute, as defined in [RFC 6750, Section 3]. 44 | /// 45 | /// [RFC 6750 §3]: https://tools.ietf.org/html/rfc6750#section-3 46 | pub fn error_description(mut self, value: T) -> Self 47 | where 48 | T: Into>, 49 | { 50 | self.0.error_description = Some(value.into()); 51 | self 52 | } 53 | 54 | /// Provides the `error_uri` attribute, as defined in [RFC 6750 §3]. 55 | /// 56 | /// It is up to implementor to provide properly-formed absolute URI. 57 | /// 58 | /// [RFC 6750 §3](https://tools.ietf.org/html/rfc6750#section-3) 59 | pub fn error_uri(mut self, value: T) -> Self 60 | where 61 | T: Into>, 62 | { 63 | self.0.error_uri = Some(value.into()); 64 | self 65 | } 66 | 67 | /// Consumes the builder and returns built `Bearer` instance. 68 | pub fn finish(self) -> Bearer { 69 | self.0 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use actix_web::http::StatusCode; 4 | 5 | /// Bearer authorization error types, described in [RFC 6750]. 6 | /// 7 | /// [RFC 6750]: https://tools.ietf.org/html/rfc6750#section-3.1 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 9 | pub enum Error { 10 | /// The request is missing a required parameter, includes an unsupported parameter or parameter 11 | /// value, repeats the same parameter, uses more than one method for including an access token, 12 | /// or is otherwise malformed. 13 | InvalidRequest, 14 | 15 | /// The access token provided is expired, revoked, malformed, or invalid for other reasons. 16 | InvalidToken, 17 | 18 | /// The request requires higher privileges than provided by the access token. 19 | InsufficientScope, 20 | } 21 | 22 | impl Error { 23 | /// Returns [HTTP status code] suitable for current error type. 24 | /// 25 | /// [HTTP status code]: `actix_web::http::StatusCode` 26 | #[allow(clippy::trivially_copy_pass_by_ref)] 27 | pub fn status_code(&self) -> StatusCode { 28 | match self { 29 | Error::InvalidRequest => StatusCode::BAD_REQUEST, 30 | Error::InvalidToken => StatusCode::UNAUTHORIZED, 31 | Error::InsufficientScope => StatusCode::FORBIDDEN, 32 | } 33 | } 34 | 35 | #[doc(hidden)] 36 | #[allow(clippy::trivially_copy_pass_by_ref)] 37 | pub fn as_str(&self) -> &str { 38 | match self { 39 | Error::InvalidRequest => "invalid_request", 40 | Error::InvalidToken => "invalid_token", 41 | Error::InsufficientScope => "insufficient_scope", 42 | } 43 | } 44 | } 45 | 46 | impl fmt::Display for Error { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | f.write_str(self.as_str()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/challenge/bearer/mod.rs: -------------------------------------------------------------------------------- 1 | //! Challenge for the "Bearer" HTTP Authentication Scheme. 2 | 3 | mod builder; 4 | mod challenge; 5 | mod errors; 6 | 7 | pub use self::{builder::BearerBuilder, challenge::Bearer, errors::Error}; 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | 13 | #[test] 14 | fn to_bytes() { 15 | let bearer = Bearer::build() 16 | .error(Error::InvalidToken) 17 | .error_description("Subject 8740827c-2e0a-447b-9716-d73042e4039d not found") 18 | .finish(); 19 | 20 | assert_eq!( 21 | "Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"", 22 | format!("{bearer}") 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/challenge/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | use actix_web::{http::header::TryIntoHeaderValue, web::Bytes}; 4 | 5 | pub mod basic; 6 | pub mod bearer; 7 | 8 | /// Authentication challenge for `WWW-Authenticate` header. 9 | pub trait Challenge: TryIntoHeaderValue + Debug + Display + Clone + Send + Sync { 10 | /// Converts the challenge into a bytes suitable for HTTP transmission. 11 | fn to_bytes(&self) -> Bytes; 12 | } 13 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/header.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | error::ParseError, 3 | http::header::{Header, HeaderName, HeaderValue, TryIntoHeaderValue, WWW_AUTHENTICATE}, 4 | HttpMessage, 5 | }; 6 | 7 | use super::Challenge; 8 | 9 | /// `WWW-Authenticate` header, described in [RFC 7235]. 10 | /// 11 | /// This header is generic over the [`Challenge`] trait, see [`Basic`](super::basic::Basic) and 12 | /// [`Bearer`](super::bearer::Bearer) challenges for details. 13 | /// 14 | /// [RFC 7235]: https://tools.ietf.org/html/rfc7235#section-4.1 15 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 16 | pub struct WwwAuthenticate(pub C); 17 | 18 | impl Header for WwwAuthenticate { 19 | #[inline] 20 | fn name() -> HeaderName { 21 | WWW_AUTHENTICATE 22 | } 23 | 24 | fn parse(_msg: &T) -> Result { 25 | unimplemented!() 26 | } 27 | } 28 | 29 | impl TryIntoHeaderValue for WwwAuthenticate { 30 | type Error = ::Error; 31 | 32 | fn try_into_value(self) -> Result { 33 | self.0.try_into_value() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/headers/www_authenticate/mod.rs: -------------------------------------------------------------------------------- 1 | //! `WWW-Authenticate` header and various auth challenges. 2 | 3 | mod challenge; 4 | mod header; 5 | 6 | pub use self::{ 7 | challenge::{basic, bearer, Challenge}, 8 | header::WwwAuthenticate, 9 | }; 10 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! HTTP authentication schemes for [Actix Web](https://actix.rs). 2 | //! 3 | //! Provides: 4 | //! - Typed [Authorization] and [WWW-Authenticate] headers 5 | //! - [Extractors] for an [Authorization] header 6 | //! - [Middleware] for easier authorization checking 7 | //! 8 | //! ## Supported schemes 9 | //! - `Bearer` as defined in [RFC 6750](https://tools.ietf.org/html/rfc6750). 10 | //! - `Basic` as defined in [RFC 7617](https://tools.ietf.org/html/rfc7617). 11 | //! 12 | //! [Authorization]: `self::headers::authorization::Authorization` 13 | //! [WWW-Authenticate]: `self::headers::www_authenticate::WwwAuthenticate` 14 | //! [Extractors]: https://actix.rs/docs/extractors 15 | //! [Middleware]: self::middleware 16 | 17 | #![forbid(unsafe_code)] 18 | #![warn(missing_docs)] 19 | #![doc(html_logo_url = "https://actix.rs/img/logo.png")] 20 | #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] 21 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 22 | 23 | pub mod extractors; 24 | pub mod headers; 25 | pub mod middleware; 26 | mod utils; 27 | -------------------------------------------------------------------------------- /actix-web-httpauth/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use actix_web::web::BytesMut; 4 | 5 | enum State { 6 | YieldStr, 7 | YieldQuote, 8 | } 9 | 10 | struct Quoted<'a> { 11 | inner: ::std::iter::Peekable>, 12 | state: State, 13 | } 14 | 15 | impl Quoted<'_> { 16 | pub fn new(s: &str) -> Quoted<'_> { 17 | Quoted { 18 | inner: s.split('"').peekable(), 19 | state: State::YieldStr, 20 | } 21 | } 22 | } 23 | 24 | impl<'a> Iterator for Quoted<'a> { 25 | type Item = &'a str; 26 | 27 | fn next(&mut self) -> Option { 28 | match self.state { 29 | State::YieldStr => match self.inner.next() { 30 | Some(val) => { 31 | self.state = State::YieldQuote; 32 | Some(val) 33 | } 34 | None => None, 35 | }, 36 | 37 | State::YieldQuote => match self.inner.peek() { 38 | Some(_) => { 39 | self.state = State::YieldStr; 40 | Some("\\\"") 41 | } 42 | None => None, 43 | }, 44 | } 45 | } 46 | } 47 | 48 | /// Escapes the quotes in `val`. 49 | pub fn put_quoted(buf: &mut BytesMut, val: &str) { 50 | for part in Quoted::new(val) { 51 | buf.extend_from_slice(part.as_bytes()); 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use std::str; 58 | 59 | use actix_web::web::BytesMut; 60 | 61 | use super::put_quoted; 62 | 63 | #[test] 64 | fn test_quote_str() { 65 | let input = "a \"quoted\" string"; 66 | let mut output = BytesMut::new(); 67 | put_quoted(&mut output, input); 68 | let result = str::from_utf8(&output).unwrap(); 69 | 70 | assert_eq!(result, "a \\\"quoted\\\" string"); 71 | } 72 | 73 | #[test] 74 | fn test_without_quotes() { 75 | let input = "non-quoted string"; 76 | let mut output = BytesMut::new(); 77 | put_quoted(&mut output, input); 78 | let result = str::from_utf8(&output).unwrap(); 79 | 80 | assert_eq!(result, "non-quoted string"); 81 | } 82 | 83 | #[test] 84 | fn test_starts_with_quote() { 85 | let input = "\"first-quoted string"; 86 | let mut output = BytesMut::new(); 87 | put_quoted(&mut output, input); 88 | let result = str::from_utf8(&output).unwrap(); 89 | 90 | assert_eq!(result, "\\\"first-quoted string"); 91 | } 92 | 93 | #[test] 94 | fn test_ends_with_quote() { 95 | let input = "last-quoted string\""; 96 | let mut output = BytesMut::new(); 97 | put_quoted(&mut output, input); 98 | let result = str::from_utf8(&output).unwrap(); 99 | 100 | assert_eq!(result, "last-quoted string\\\""); 101 | } 102 | 103 | #[test] 104 | fn test_double_quote() { 105 | let input = "quote\"\"string"; 106 | let mut output = BytesMut::new(); 107 | put_quoted(&mut output, input); 108 | let result = str::from_utf8(&output).unwrap(); 109 | 110 | assert_eq!(result, "quote\\\"\\\"string"); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /actix-ws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Ensure TCP connection is properly shut down when session is dropped. 6 | 7 | ## 0.3.0 8 | 9 | - Add `AggregatedMessage[Stream]` types. 10 | - Add `MessageStream::max_frame_size()` setter method. 11 | - Add `Session::continuation()` method. 12 | - The `Session::text()` method now receives an `impl Into`, making broadcasting text messages more efficient. 13 | - Remove type parameters from `Session::{text, binary}()` methods, replacing with equivalent `impl Trait` parameters. 14 | - Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream. 15 | 16 | ## 0.2.5 17 | 18 | - Adopted into @actix org from . 19 | -------------------------------------------------------------------------------- /actix-ws/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-ws" 3 | version = "0.3.0" 4 | description = "WebSockets for Actix Web, without actors" 5 | categories = ["web-programming::websocket"] 6 | keywords = ["actix", "web", "websocket", "websockets", "streaming"] 7 | authors = [ 8 | "asonix ", 9 | "Rob Ede ", 10 | ] 11 | repository.workspace = true 12 | homepage.workspace = true 13 | license.workspace = true 14 | edition.workspace = true 15 | rust-version.workspace = true 16 | 17 | [dependencies] 18 | actix-codec = "0.5" 19 | actix-http = { version = "3", default-features = false, features = ["ws"] } 20 | actix-web = { version = "4", default-features = false } 21 | bytestring = "1" 22 | futures-core = "0.3.17" 23 | tokio = { version = "1.24", features = ["sync"] } 24 | 25 | [dev-dependencies] 26 | actix-web = "4.8" 27 | futures-util = { version = "0.3.17", default-features = false, features = ["std"] } 28 | tokio = { version = "1.24", features = ["sync", "rt", "macros"] } 29 | tracing = "0.1.30" 30 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 31 | 32 | [lints] 33 | workspace = true 34 | -------------------------------------------------------------------------------- /actix-ws/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /actix-ws/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /actix-ws/README.md: -------------------------------------------------------------------------------- 1 | # `actix-ws` 2 | 3 | > WebSockets for Actix Web, without actors. 4 | 5 | 6 | 7 | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)](https://crates.io/crates/actix-ws) 8 | [![Documentation](https://docs.rs/actix-ws/badge.svg?version=0.3.0)](https://docs.rs/actix-ws/0.3.0) 9 | ![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg) 10 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-ws.svg) 11 |
12 | [![Dependency Status](https://deps.rs/crate/actix-ws/0.3.0/status.svg)](https://deps.rs/crate/actix-ws/0.3.0) 13 | [![Download](https://img.shields.io/crates/d/actix-ws.svg)](https://crates.io/crates/actix-ws) 14 | [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) 15 | 16 | 17 | 18 | ## Example 19 | 20 | ```rust 21 | use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder}; 22 | use actix_ws::Message; 23 | 24 | async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result { 25 | let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; 26 | 27 | actix_web::rt::spawn(async move { 28 | while let Some(Ok(msg)) = msg_stream.recv().await { 29 | match msg { 30 | Message::Ping(bytes) => { 31 | if session.pong(&bytes).await.is_err() { 32 | return; 33 | } 34 | } 35 | Message::Text(msg) => println!("Got text: {msg}"), 36 | _ => break, 37 | } 38 | } 39 | 40 | let _ = session.close(None).await; 41 | }); 42 | 43 | Ok(response) 44 | } 45 | 46 | #[actix_web::main] 47 | async fn main() -> std::io::Result<()> { 48 | HttpServer::new(move || { 49 | App::new() 50 | .wrap(Logger::default()) 51 | .route("/ws", web::get().to(ws)) 52 | }) 53 | .bind("127.0.0.1:8080")? 54 | .run() 55 | .await?; 56 | 57 | Ok(()) 58 | } 59 | ``` 60 | 61 | ## Resources 62 | 63 | - [API Documentation](https://docs.rs/actix-ws) 64 | - [Example Chat Project](https://github.com/actix/examples/tree/master/websockets/chat-actorless) 65 | - Minimum Supported Rust Version (MSRV): 1.75 66 | 67 | ## License 68 | 69 | This project is licensed under either of 70 | 71 | - Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 72 | - MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) 73 | 74 | at your option. 75 | -------------------------------------------------------------------------------- /actix-ws/examples/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat 5 | 64 | 65 | 66 | 67 |
    68 | 69 | 70 | -------------------------------------------------------------------------------- /actix-ws/examples/chat.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | sync::Arc, 4 | time::{Duration, Instant}, 5 | }; 6 | 7 | use actix_web::{ 8 | middleware::Logger, web, web::Html, App, HttpRequest, HttpResponse, HttpServer, Responder, 9 | }; 10 | use actix_ws::{AggregatedMessage, Session}; 11 | use bytestring::ByteString; 12 | use futures_util::{stream::FuturesUnordered, StreamExt as _}; 13 | use tokio::sync::Mutex; 14 | use tracing::level_filters::LevelFilter; 15 | use tracing_subscriber::EnvFilter; 16 | 17 | #[derive(Clone)] 18 | struct Chat { 19 | inner: Arc>, 20 | } 21 | 22 | struct ChatInner { 23 | sessions: Vec, 24 | } 25 | 26 | impl Chat { 27 | fn new() -> Self { 28 | Chat { 29 | inner: Arc::new(Mutex::new(ChatInner { 30 | sessions: Vec::new(), 31 | })), 32 | } 33 | } 34 | 35 | async fn insert(&self, session: Session) { 36 | self.inner.lock().await.sessions.push(session); 37 | } 38 | 39 | async fn send(&self, msg: impl Into) { 40 | let msg = msg.into(); 41 | 42 | let mut inner = self.inner.lock().await; 43 | let mut unordered = FuturesUnordered::new(); 44 | 45 | for mut session in inner.sessions.drain(..) { 46 | let msg = msg.clone(); 47 | 48 | unordered.push(async move { 49 | let res = session.text(msg).await; 50 | res.map(|_| session) 51 | .map_err(|_| tracing::debug!("Dropping session")) 52 | }); 53 | } 54 | 55 | while let Some(res) = unordered.next().await { 56 | if let Ok(session) = res { 57 | inner.sessions.push(session); 58 | } 59 | } 60 | } 61 | } 62 | 63 | async fn ws( 64 | req: HttpRequest, 65 | body: web::Payload, 66 | chat: web::Data, 67 | ) -> Result { 68 | let (response, mut session, stream) = actix_ws::handle(&req, body)?; 69 | 70 | // increase the maximum allowed frame size to 128KiB and aggregate continuation frames 71 | let mut stream = stream.max_frame_size(128 * 1024).aggregate_continuations(); 72 | 73 | chat.insert(session.clone()).await; 74 | tracing::info!("Inserted session"); 75 | 76 | let alive = Arc::new(Mutex::new(Instant::now())); 77 | 78 | let mut session2 = session.clone(); 79 | let alive2 = alive.clone(); 80 | actix_web::rt::spawn(async move { 81 | let mut interval = actix_web::rt::time::interval(Duration::from_secs(5)); 82 | 83 | loop { 84 | interval.tick().await; 85 | if session2.ping(b"").await.is_err() { 86 | break; 87 | } 88 | 89 | if Instant::now().duration_since(*alive2.lock().await) > Duration::from_secs(10) { 90 | let _ = session2.close(None).await; 91 | break; 92 | } 93 | } 94 | }); 95 | 96 | actix_web::rt::spawn(async move { 97 | while let Some(Ok(msg)) = stream.recv().await { 98 | match msg { 99 | AggregatedMessage::Ping(bytes) => { 100 | if session.pong(&bytes).await.is_err() { 101 | return; 102 | } 103 | } 104 | 105 | AggregatedMessage::Text(string) => { 106 | tracing::info!("Relaying text, {string}"); 107 | chat.send(string).await; 108 | } 109 | 110 | AggregatedMessage::Close(reason) => { 111 | let _ = session.close(reason).await; 112 | tracing::info!("Got close, bailing"); 113 | return; 114 | } 115 | 116 | AggregatedMessage::Pong(_) => { 117 | *alive.lock().await = Instant::now(); 118 | } 119 | 120 | _ => (), 121 | }; 122 | } 123 | let _ = session.close(None).await; 124 | }); 125 | tracing::info!("Spawned"); 126 | 127 | Ok(response) 128 | } 129 | 130 | async fn index() -> impl Responder { 131 | Html::new(include_str!("chat.html").to_owned()) 132 | } 133 | 134 | #[tokio::main(flavor = "current_thread")] 135 | async fn main() -> io::Result<()> { 136 | tracing_subscriber::fmt() 137 | .with_env_filter( 138 | EnvFilter::builder() 139 | .with_default_directive(LevelFilter::INFO.into()) 140 | .from_env_lossy(), 141 | ) 142 | .init(); 143 | 144 | let chat = Chat::new(); 145 | 146 | HttpServer::new(move || { 147 | App::new() 148 | .wrap(Logger::default()) 149 | .app_data(web::Data::new(chat.clone())) 150 | .route("/", web::get().to(index)) 151 | .route("/ws", web::get().to(ws)) 152 | }) 153 | .bind("127.0.0.1:8080")? 154 | .run() 155 | .await?; 156 | 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /actix-ws/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! WebSockets for Actix Web, without actors. 2 | //! 3 | //! For usage, see documentation on [`handle()`]. 4 | 5 | #![warn(missing_docs)] 6 | #![doc(html_logo_url = "https://actix.rs/img/logo.png")] 7 | #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] 8 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 9 | 10 | pub use actix_http::ws::{CloseCode, CloseReason, Item, Message, ProtocolError}; 11 | use actix_http::{ 12 | body::{BodyStream, MessageBody}, 13 | ws::handshake, 14 | }; 15 | use actix_web::{web, HttpRequest, HttpResponse}; 16 | use tokio::sync::mpsc::channel; 17 | 18 | mod aggregated; 19 | mod session; 20 | mod stream; 21 | 22 | pub use self::{ 23 | aggregated::{AggregatedMessage, AggregatedMessageStream}, 24 | session::{Closed, Session}, 25 | stream::{MessageStream, StreamingBody}, 26 | }; 27 | 28 | /// Begin handling websocket traffic 29 | /// 30 | /// ```no_run 31 | /// use std::io; 32 | /// use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder}; 33 | /// use actix_ws::Message; 34 | /// use futures_util::StreamExt as _; 35 | /// 36 | /// async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result { 37 | /// let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; 38 | /// 39 | /// actix_web::rt::spawn(async move { 40 | /// while let Some(Ok(msg)) = msg_stream.next().await { 41 | /// match msg { 42 | /// Message::Ping(bytes) => { 43 | /// if session.pong(&bytes).await.is_err() { 44 | /// return; 45 | /// } 46 | /// } 47 | /// 48 | /// Message::Text(msg) => println!("Got text: {msg}"), 49 | /// _ => break, 50 | /// } 51 | /// } 52 | /// 53 | /// let _ = session.close(None).await; 54 | /// }); 55 | /// 56 | /// Ok(response) 57 | /// } 58 | /// 59 | /// #[tokio::main(flavor = "current_thread")] 60 | /// async fn main() -> io::Result<()> { 61 | /// HttpServer::new(move || { 62 | /// App::new() 63 | /// .route("/ws", web::get().to(ws)) 64 | /// .wrap(Logger::default()) 65 | /// }) 66 | /// .bind(("127.0.0.1", 8080))? 67 | /// .run() 68 | /// .await 69 | /// } 70 | /// ``` 71 | pub fn handle( 72 | req: &HttpRequest, 73 | body: web::Payload, 74 | ) -> Result<(HttpResponse, Session, MessageStream), actix_web::Error> { 75 | let mut response = handshake(req.head())?; 76 | let (tx, rx) = channel(32); 77 | 78 | Ok(( 79 | response 80 | .message_body(BodyStream::new(StreamingBody::new(rx)).boxed())? 81 | .into(), 82 | Session::new(tx), 83 | MessageStream::new(body.into_inner()), 84 | )) 85 | } 86 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | threshold: 100% # make CI green 8 | patch: 9 | default: 10 | threshold: 100% # make CI green 11 | 12 | # ignore code coverage on following paths 13 | ignore: 14 | - "**/tests" 15 | - "**/benches" 16 | - "**/examples" 17 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # depends on: 2 | # - https://crates.io/crates/fd-find 3 | # - https://crates.io/crates/cargo-check-external-types 4 | 5 | _list: 6 | @just --list 7 | 8 | toolchain := "" 9 | 10 | msrv := ``` 11 | cargo metadata --format-version=1 \ 12 | | jq -r 'first(.packages[] | select(.source == null and .rust_version)) | .rust_version' \ 13 | | sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/' 14 | ``` 15 | msrv_rustup := "+" + msrv 16 | 17 | # Run Clippy over workspace. 18 | [group("lint")] 19 | clippy: 20 | cargo {{ toolchain }} clippy --workspace --all-targets --all-features 21 | 22 | # Format project. 23 | [group("lint")] 24 | fmt: update-readmes 25 | cargo +nightly fmt 26 | fd --type=file --hidden --extension=yml --extension=md --exec-batch npx -y prettier --write 27 | 28 | # Check project. 29 | [group("lint")] 30 | check: 31 | cargo +nightly fmt -- --check 32 | fd --type=file --hidden --extension=yml --extension=md --exec-batch npx -y prettier --check 33 | 34 | # Update READMEs from crate root documentation. 35 | [group("lint")] 36 | update-readmes: 37 | cd ./actix-cors && cargo rdme --force 38 | cd ./actix-identity && cargo rdme --force 39 | cd ./actix-session && cargo rdme --force 40 | fd README.md --exec-batch npx -y prettier --write 41 | 42 | # Test workspace code. 43 | [group("test")] 44 | test: 45 | cargo {{ toolchain }} nextest run --workspace --all-features 46 | cargo {{ toolchain }} test --doc --workspace --all-features 47 | 48 | # Downgrade dev-dependencies necessary to run MSRV checks/tests. 49 | [private] 50 | downgrade-for-msrv: 51 | cargo update -p=native-tls --precise=0.2.13 52 | cargo update -p=litemap --precise=0.7.4 53 | cargo update -p=zerofrom --precise=0.1.5 54 | 55 | # Test workspace using MSRV. 56 | [group("test")] 57 | test-msrv: 58 | @just downgrade-for-msrv 59 | @just toolchain={{ msrv_rustup }} test 60 | 61 | # Test workspace code and docs. 62 | [group("test")] 63 | test-all: test test-docs 64 | 65 | # Test workspace and collect coverage info. 66 | [private] 67 | test-coverage: 68 | cargo {{ toolchain }} llvm-cov nextest --no-report --all-features 69 | cargo {{ toolchain }} llvm-cov --doc --no-report --all-features 70 | 71 | # Test workspace and generate Codecov report. 72 | test-coverage-codecov: test-coverage 73 | cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json 74 | 75 | # Test workspace and generate LCOV report. 76 | test-coverage-lcov: test-coverage 77 | cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info 78 | 79 | # Test workspace docs. 80 | [group("test")] 81 | [group("docs")] 82 | test-docs: 83 | cargo {{ toolchain }} test --doc --workspace --all-features --no-fail-fast -- --nocapture 84 | 85 | # Document crates in workspace. 86 | [group("docs")] 87 | doc *args: && doc-set-workspace-crates 88 | rm -f "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" 89 | RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace --all-features {{ args }} 90 | 91 | [group("docs")] 92 | [private] 93 | doc-set-workspace-crates: 94 | #!/usr/bin/env bash 95 | ( 96 | echo "window.ALL_CRATES = " 97 | cargo metadata --format-version=1 \ 98 | | jq '[.packages[] | select(.source == null) | .targets | map(select(.doc) | .name)] | flatten' 99 | echo ";" 100 | ) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" 101 | 102 | # Document crates in workspace and watch for changes. 103 | [group("docs")] 104 | doc-watch: 105 | @just doc --open 106 | cargo watch -- just doc 107 | 108 | # Check for unintentional external type exposure on all crates in workspace. 109 | [group("lint")] 110 | check-external-types-all: 111 | #!/usr/bin/env bash 112 | set -euo pipefail 113 | exit=0 114 | for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do 115 | if ! just toolchain={{ toolchain }} check-external-types-manifest "$f"; then exit=1; fi 116 | echo 117 | echo 118 | done 119 | exit $exit 120 | 121 | # Check for unintentional external type exposure on all crates in workspace. 122 | [group("lint")] 123 | check-external-types-all-table: 124 | #!/usr/bin/env bash 125 | set -euo pipefail 126 | for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do 127 | echo 128 | echo "Checking for $f" 129 | just toolchain={{ toolchain }} check-external-types-manifest "$f" --output-format=markdown-table 130 | done 131 | 132 | # Check for unintentional external type exposure on a crate. 133 | [group("lint")] 134 | check-external-types-manifest manifest_path *extra_args="": 135 | cargo {{ toolchain }} check-external-types --manifest-path "{{ manifest_path }}" {{ extra_args }} 136 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Crate" 3 | use_field_init_shorthand = true 4 | --------------------------------------------------------------------------------