mirror of
https://github.com/actix/actix-extras.git
synced 2025-04-22 01:46:50 +02:00
Compare commits
210 Commits
limitation
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
c04cc19e73 | ||
|
6a13b3b182 | ||
|
d994912ac2 | ||
|
5f6f20cf37 | ||
|
5145924410 | ||
|
b20dec36ac | ||
|
f6e45d487b | ||
|
c53e198ea7 | ||
|
4d9984ee76 | ||
|
9a08090709 | ||
|
7d3348bb29 | ||
|
c0fa63af39 | ||
|
0b5e2b3647 | ||
|
b95595b9cd | ||
|
4b3f87e915 | ||
|
144c7f92b9 | ||
|
c71b9dd443 | ||
|
282d56e96b | ||
|
d514ad3af5 | ||
|
109e6a4793 | ||
|
bb0c7f21d9 | ||
|
3f7a479a76 | ||
|
fc4b656c3b | ||
|
0f35de7da1 | ||
|
8294fcc645 | ||
|
3de6b03711 | ||
|
64931189c7 | ||
|
265b213123 | ||
|
695369f02f | ||
|
87d9e51112 | ||
|
8c11d37dda | ||
|
d97b36652a | ||
|
98847b9279 | ||
|
cd1b77134e | ||
|
105932706d | ||
|
18f94fa8b5 | ||
|
66b82f0f30 | ||
|
d67abde5f3 | ||
|
3eafe7f5ce | ||
|
3b5f7ae68c | ||
|
036af488fd | ||
|
77406cbb71 | ||
|
2ede588693 | ||
|
21680e0ebe | ||
|
370f9d3033 | ||
|
8f4fb348b3 | ||
|
ff4b173716 | ||
|
49aacfce9f | ||
|
dd20ebb6cb | ||
|
a3211b73d3 | ||
|
a89d3a58bc | ||
|
3c640ec120 | ||
|
26ccf8b200 | ||
|
dd1421f1a0 | ||
|
4eb779be77 | ||
|
48646d1bd3 | ||
|
275675e1c2 | ||
|
50d2fee4e2 | ||
|
0c0d13be12 | ||
|
d10b71fe06 | ||
|
f2339971cd | ||
|
517e72f248 | ||
|
504e89403b | ||
|
31b1dc5aa8 | ||
|
d7daf441d1 | ||
|
2de4b1886c | ||
|
caa5dbc5b3 | ||
|
c259e715f8 | ||
|
d8a86751f0 | ||
|
cac93d2bc7 | ||
|
95f4e0f692 | ||
|
24f3985eab | ||
|
b0d2947a4a | ||
|
0802eff40d | ||
|
2a6a36af23 | ||
|
3ebdc6192c | ||
|
87cf947a45 | ||
|
f063bec5ba | ||
|
45e9e00285 | ||
|
6934db623b | ||
|
1a658a98e1 | ||
|
2a092a19a8 | ||
|
032aeb6fdb | ||
|
52e58610e4 | ||
|
023158cfa8 | ||
|
14c605fae2 | ||
|
d94c023bf9 | ||
|
7e21fd753e | ||
|
e7ee2a06ab | ||
|
8aa2c959c4 | ||
|
2f1d1daee8 | ||
|
d15572b501 | ||
|
b9e47d61c3 | ||
|
515a727ca3 | ||
|
20234ec555 | ||
|
e4bb5ed355 | ||
|
5368569d00 | ||
|
21b9408a23 | ||
|
5879740322 | ||
|
4adc9f8884 | ||
|
abf75eeb06 | ||
|
433c926503 | ||
|
8ebb12b75a | ||
|
931c4eea4d | ||
|
8195484415 | ||
|
3ae4ef2706 | ||
|
65c698cd7f | ||
|
f2ef72d056 | ||
|
e4ee236341 | ||
|
41ae57d414 | ||
|
1b82024499 | ||
|
6b04450703 | ||
|
c0c7588a57 | ||
|
b918084a53 | ||
|
b762b41360 | ||
|
da53492c8c | ||
|
2c81bc093b | ||
|
a2ef65715b | ||
|
9beb348d45 | ||
|
66544952b6 | ||
|
9ddb95b74a | ||
|
f450e3fb85 | ||
|
31951dcc9b | ||
|
dfc6fe1986 | ||
|
122fba0580 | ||
|
f250348e57 | ||
|
e6f99e915d | ||
|
9d68074bf1 | ||
|
bbb4ed047c | ||
|
39291c86b7 | ||
|
db2193b8c5 | ||
|
f0c33a970f | ||
|
74c8545363 | ||
|
9112cf9f23 | ||
|
563d6e0b20 | ||
|
a71c7f6a90 | ||
|
a5f5e31a82 | ||
|
5414e2655b | ||
|
daffc24245 | ||
|
2e0cbb8bbb | ||
|
7fe13e142e | ||
|
8ddbf26cc1 | ||
|
b9769edca1 | ||
|
e3027549c5 | ||
|
1934457e48 | ||
|
254d4084a9 | ||
|
a9e615bac4 | ||
|
1e70159e08 | ||
|
89bf63e1ef | ||
|
8b4e8ea34e | ||
|
5ceb3c72cd | ||
|
c62b271d9a | ||
|
320cbebc7e | ||
|
0c859a96c8 | ||
|
d55fc6d7f5 | ||
|
e2bf504055 | ||
|
77b8dcdf59 | ||
|
b694c9317a | ||
|
57eaad2ffe | ||
|
0cb0e28208 | ||
|
8049a75d9f | ||
|
0dd810e213 | ||
|
5bf831c27b | ||
|
a7e3503ad1 | ||
|
819f45106f | ||
|
4f76943423 | ||
|
2f30fd71a9 | ||
|
5198c68c06 | ||
|
2d4cf5f422 | ||
|
53dce5c34f | ||
|
8de686a711 | ||
|
50fd71d496 | ||
|
3c5478966f | ||
|
1e18d62852 | ||
|
7aeeb9a445 | ||
|
5b2085f414 | ||
|
6afca96ddf | ||
|
a48c2926f9 | ||
|
6d0ab96dfd | ||
|
a593a8dc90 | ||
|
4d79d263ef | ||
|
31540f8e4b | ||
|
11046d7663 | ||
|
73b2aac6d6 | ||
|
76d9313171 | ||
|
373a89a978 | ||
|
61f16c609a | ||
|
ecd2016c09 | ||
|
471f07e27f | ||
|
077c6edced | ||
|
fad631c448 | ||
|
20f72cab3e | ||
|
4bad825456 | ||
|
cb3eba93cc | ||
|
9d993c6c73 | ||
|
4761826616 | ||
|
ec340670a8 | ||
|
3a7834c3ba | ||
|
7db43782ce | ||
|
1ee1afb2a6 | ||
|
9e4754bbfa | ||
|
cd3e5f9772 | ||
|
45ee50f9cb | ||
|
1d6ef8938f | ||
|
316c0d238d | ||
|
5baa3c3d95 | ||
|
2dea1f2748 | ||
|
09ff35bd2d | ||
|
6caf37cedd | ||
|
bafd8179ff |
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -3,35 +3,41 @@ name: bug report
|
|||||||
about: create a bug report
|
about: create a bug report
|
||||||
---
|
---
|
||||||
|
|
||||||
Your issue may already be reported!
|
Your issue may already be reported! Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one.
|
||||||
Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one.
|
|
||||||
|
|
||||||
## Expected Behavior
|
## Expected Behavior
|
||||||
|
|
||||||
<!--- If you're describing a bug, tell us what should happen -->
|
<!--- If you're describing a bug, tell us what should happen -->
|
||||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||||
|
|
||||||
## Current Behavior
|
## Current Behavior
|
||||||
|
|
||||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
## Possible Solution
|
## Possible Solution
|
||||||
|
|
||||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||||
<!--- or ideas how to implement the addition or change -->
|
<!--- or ideas how to implement the addition or change -->
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
## Steps to Reproduce (for bugs)
|
||||||
|
|
||||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||||
|
|
||||||
1.
|
1.
|
||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
4.
|
4.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
## Your Environment
|
## Your Environment
|
||||||
|
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
|
||||||
* Rust Version (I.e, output of `rustc -V`):
|
- Rust version (output of `rustc -V`):
|
||||||
* Actix-* crate(s) Version:
|
- `actix-*` crate versions:
|
||||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,12 +2,14 @@
|
|||||||
<!-- Please fill out the following to make our reviews easy. -->
|
<!-- Please fill out the following to make our reviews easy. -->
|
||||||
|
|
||||||
## PR Type
|
## PR Type
|
||||||
|
|
||||||
<!-- What kind of change does this PR make? -->
|
<!-- What kind of change does this PR make? -->
|
||||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||||
|
|
||||||
INSERT_PR_TYPE
|
INSERT_PR_TYPE
|
||||||
|
|
||||||
|
|
||||||
## PR Checklist
|
## PR Checklist
|
||||||
|
|
||||||
<!-- Check your PR fulfills the following items. -->
|
<!-- Check your PR fulfills the following items. -->
|
||||||
<!-- For draft PRs check the boxes as you complete them. -->
|
<!-- For draft PRs check the boxes as you complete them. -->
|
||||||
|
|
||||||
@ -16,11 +18,10 @@ INSERT_PR_TYPE
|
|||||||
- [ ] A changelog entry has been made for the appropriate packages.
|
- [ ] A changelog entry has been made for the appropriate packages.
|
||||||
- [ ] Format code with the nightly rustfmt (`cargo +nightly fmt`).
|
- [ ] Format code with the nightly rustfmt (`cargo +nightly fmt`).
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
<!-- Describe the current and new behavior. -->
|
<!-- Describe the current and new behavior. -->
|
||||||
<!-- Emphasize any breaking changes. -->
|
<!-- Emphasize any breaking changes. -->
|
||||||
|
|
||||||
|
|
||||||
<!-- If this PR fixes or closes an issue, reference it here. -->
|
<!-- If this PR fixes or closes an issue, reference it here. -->
|
||||||
<!-- Closes #000 -->
|
<!-- Closes #000 -->
|
||||||
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@ -1,10 +1,10 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
- package-ecosystem: cargo
|
- package-ecosystem: cargo
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
|
37
.github/workflows/ci-post-merge.yml
vendored
37
.github/workflows/ci-post-merge.yml
vendored
@ -31,13 +31,14 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- uses: taiki-e/install-action@v2.18.11
|
- name: Install cargo-hack, cargo-ci-cache-clean
|
||||||
|
uses: taiki-e/install-action@v2.49.42
|
||||||
with:
|
with:
|
||||||
tool: cargo-hack
|
tool: cargo-hack,cargo-ci-cache-clean
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
run: cargo ci-min
|
run: cargo ci-min
|
||||||
@ -52,10 +53,8 @@ jobs:
|
|||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: cargo ci-test
|
run: cargo ci-test
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: CI cache clean
|
||||||
run: |
|
run: cargo-ci-cache-clean
|
||||||
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
build_and_test_other_nightly:
|
build_and_test_other_nightly:
|
||||||
strategy:
|
strategy:
|
||||||
@ -72,14 +71,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install OpenSSL
|
||||||
|
if: matrix.target.os == 'windows-latest'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
choco install openssl --version=1.1.1.2100 -y --no-progress
|
||||||
|
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
|
||||||
|
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- uses: taiki-e/install-action@v2.18.11
|
- name: Install cargo-hack and cargo-ci-cache-clean
|
||||||
|
uses: taiki-e/install-action@v2.49.42
|
||||||
with:
|
with:
|
||||||
tool: cargo-hack
|
tool: cargo-hack,cargo-ci-cache-clean
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
run: cargo ci-min
|
run: cargo ci-min
|
||||||
@ -92,9 +101,7 @@ jobs:
|
|||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: cargo ci-test --exclude=actix-redis --exclude=actix-session --exclude=actix-limitation -- --nocapture
|
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation -- --nocapture
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: CI cache clean
|
||||||
run: |
|
run: cargo-ci-cache-clean
|
||||||
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
target:
|
target:
|
||||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||||
version:
|
version:
|
||||||
- { name: msrv, version: 1.68.0 }
|
- { name: msrv, version: 1.75.0 }
|
||||||
- { name: stable, version: stable }
|
- { name: stable, version: stable }
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
|
||||||
@ -44,19 +44,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install cargo-hack
|
- name: Install cargo-hack and cargo-ci-cache-clean, just
|
||||||
uses: taiki-e/install-action@v2.18.11
|
uses: taiki-e/install-action@v2.49.42
|
||||||
with:
|
with:
|
||||||
tool: cargo-hack
|
tool: cargo-hack,cargo-ci-cache-clean,just
|
||||||
|
|
||||||
# - name: workaround MSRV issues
|
- name: workaround MSRV issues
|
||||||
# if: matrix.version.name == 'msrv'
|
if: matrix.version.name == 'msrv'
|
||||||
# run: |
|
run: just downgrade-for-msrv
|
||||||
# cargo update -p=time:0.3.20 --precise=0.3.16
|
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
run: cargo ci-min
|
run: cargo ci-min
|
||||||
@ -71,10 +70,8 @@ jobs:
|
|||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: cargo ci-test
|
run: cargo ci-test
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: CI cache clean
|
||||||
run: |
|
run: cargo-ci-cache-clean
|
||||||
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
build_and_test_other:
|
build_and_test_other:
|
||||||
strategy:
|
strategy:
|
||||||
@ -85,7 +82,7 @@ jobs:
|
|||||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
|
||||||
version:
|
version:
|
||||||
- { name: msrv, version: 1.68.0 }
|
- { name: msrv, version: 1.75.0 }
|
||||||
- { name: stable, version: stable }
|
- { name: stable, version: stable }
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
|
||||||
@ -94,20 +91,28 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install OpenSSL
|
||||||
|
if: matrix.target.os == 'windows-latest'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
choco install openssl --version=1.1.1.2100 -y --no-progress
|
||||||
|
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
|
||||||
|
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Rust (${{ matrix.version.name }})
|
- name: Install Rust (${{ matrix.version.name }})
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.version.version }}
|
toolchain: ${{ matrix.version.version }}
|
||||||
|
|
||||||
- name: Install cargo-hack
|
- name: Install cargo-hack, cargo-ci-cache-clean, just
|
||||||
uses: taiki-e/install-action@v2.18.11
|
uses: taiki-e/install-action@v2.49.42
|
||||||
with:
|
with:
|
||||||
tool: cargo-hack
|
tool: cargo-hack,cargo-ci-cache-clean,just
|
||||||
|
|
||||||
# - name: workaround MSRV issues
|
- name: workaround MSRV issues
|
||||||
# if: matrix.version.name == 'msrv'
|
if: matrix.version.name == 'msrv'
|
||||||
# run: |
|
run: just downgrade-for-msrv
|
||||||
# cargo update -p=time:0.3.20 --precise=0.3.16
|
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
run: cargo ci-min
|
run: cargo ci-min
|
||||||
@ -120,24 +125,29 @@ jobs:
|
|||||||
|
|
||||||
- name: tests
|
- name: tests
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: cargo ci-test --exclude=actix-redis --exclude=actix-session --exclude=actix-limitation
|
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: CI cache clean
|
||||||
run: |
|
run: cargo-ci-cache-clean
|
||||||
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
doc_tests:
|
doc_tests:
|
||||||
name: doc tests
|
name: Documentation Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
|
||||||
- name: doc tests
|
- name: Install just
|
||||||
timeout-minutes: 40
|
uses: taiki-e/install-action@v2.49.42
|
||||||
run: cargo ci-doctest -- --nocapture
|
with:
|
||||||
|
tool: just
|
||||||
|
|
||||||
|
- name: Test docs
|
||||||
|
run: just test-docs
|
||||||
|
|
||||||
|
- name: Build docs
|
||||||
|
run: just doc
|
||||||
|
23
.github/workflows/coverage.yml
vendored
23
.github/workflows/coverage.yml
vendored
@ -24,17 +24,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
|
components: llvm-tools-preview
|
||||||
|
|
||||||
- name: Generate coverage file
|
- name: Install just, cargo-llvm-cov, cargo-nextest
|
||||||
run: |
|
uses: taiki-e/install-action@v2.49.42
|
||||||
cargo install cargo-tarpaulin --vers "^0.13"
|
with:
|
||||||
cargo tarpaulin --workspace --out Xml --verbose
|
tool: just,cargo-llvm-cov,cargo-nextest
|
||||||
|
|
||||||
|
- name: Generate code coverage
|
||||||
|
run: just test-coverage-codecov
|
||||||
|
|
||||||
- name: Upload to Codecov
|
- name: Upload to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5.4.0
|
||||||
with:
|
with:
|
||||||
file: cobertura.xml
|
files: codecov.json
|
||||||
|
fail_ci_if_error: true
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
46
.github/workflows/lint.yml
vendored
46
.github/workflows/lint.yml
vendored
@ -2,7 +2,8 @@ name: Lint
|
|||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
permissions: { contents: read }
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@ -15,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
- name: Install Rust (nightly)
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
@ -24,43 +25,24 @@ jobs:
|
|||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
checks: write # to add clippy checks to PR diffs
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- name: Check with Clippy
|
- name: Check with Clippy
|
||||||
run: cargo clippy --workspace --tests --all-features -- -A unknown_lints
|
uses: giraffate/clippy-action@v1.0.1
|
||||||
|
|
||||||
public-api-diff:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout ${{ github.base_ref }}
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
reporter: github-pr-check
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: checkout ${{ github.head_ref }}
|
clippy_flags: >-
|
||||||
uses: actions/checkout@v4
|
--workspace --all-features --tests --examples --bins --
|
||||||
|
-A unknown_lints -D clippy::todo -D clippy::dbg_macro
|
||||||
- name: Install Rust (nightly)
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
|
||||||
with:
|
|
||||||
toolchain: nightly
|
|
||||||
|
|
||||||
- name: Install cargo-public-api
|
|
||||||
uses: taiki-e/cache-cargo-install-action@v1.2.1
|
|
||||||
with:
|
|
||||||
tool: cargo-public-api
|
|
||||||
|
|
||||||
- name: generate API diff
|
|
||||||
run: |
|
|
||||||
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
|
|
||||||
|
|
||||||
cargo public-api --manifest-path "$f" --all-features diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} >> /tmp/diff.txt
|
|
||||||
done
|
|
||||||
cat /tmp/diff.txt
|
|
||||||
|
34
.github/workflows/upload-doc.yml
vendored
34
.github/workflows/upload-doc.yml
vendored
@ -1,34 +0,0 @@
|
|||||||
name: Upload Documentation
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: { branches: [master] }
|
|
||||||
|
|
||||||
permissions: { contents: write }
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust (nightly)
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1.5.0
|
|
||||||
with: { toolchain: nightly }
|
|
||||||
|
|
||||||
- name: Build Docs
|
|
||||||
run: cargo doc --workspace --all-features --no-deps
|
|
||||||
|
|
||||||
- name: Tweak HTML
|
|
||||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_cors/index.html">' > target/doc/index.html
|
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.3
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
BRANCH: gh-pages
|
|
||||||
FOLDER: target/doc
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
guide/build/
|
guide/build/
|
||||||
/gh-pages
|
/gh-pages
|
||||||
|
5
.prettierrc.yml
Normal file
5
.prettierrc.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
overrides:
|
||||||
|
- files: "*.md"
|
||||||
|
options:
|
||||||
|
proseWrap: never
|
||||||
|
printWidth: 9999
|
3292
Cargo.lock
generated
Normal file
3292
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@ -5,23 +5,29 @@ members = [
|
|||||||
"actix-identity",
|
"actix-identity",
|
||||||
"actix-limitation",
|
"actix-limitation",
|
||||||
"actix-protobuf",
|
"actix-protobuf",
|
||||||
"actix-redis",
|
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-settings",
|
"actix-settings",
|
||||||
"actix-web-httpauth",
|
"actix-web-httpauth",
|
||||||
|
"actix-ws",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
repository = "https://github.com/actix/actix-extras"
|
||||||
|
homepage = "https://actix.rs"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.68"
|
rust-version = "1.75"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
rust-2018-idioms = { level = "deny" }
|
||||||
|
nonstandard-style = { level = "deny" }
|
||||||
|
future-incompatible = { level = "deny" }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
actix-cors = { path = "./actix-cors" }
|
actix-cors = { path = "./actix-cors" }
|
||||||
actix-identity = { path = "./actix-identity" }
|
actix-identity = { path = "./actix-identity" }
|
||||||
actix-limitation = { path = "./actix-limitation" }
|
actix-limitation = { path = "./actix-limitation" }
|
||||||
actix-protobuf = { path = "./actix-protobuf" }
|
actix-protobuf = { path = "./actix-protobuf" }
|
||||||
actix-redis = { path = "./actix-redis" }
|
|
||||||
actix-session = { path = "./actix-session" }
|
actix-session = { path = "./actix-session" }
|
||||||
actix-settings = { path = "./actix-settings" }
|
actix-settings = { path = "./actix-settings" }
|
||||||
actix-web-httpauth = { path = "./actix-web-httpauth" }
|
actix-web-httpauth = { path = "./actix-web-httpauth" }
|
||||||
|
@ -186,8 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2017-NOW Nikolay Kim
|
Copyright 2017-NOW Actix team
|
||||||
Copyright 2017-NOW svartalf and Actix team
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
Copyright (c) 2017 Nikolay Kim
|
Copyright (c) 2023 Actix team
|
||||||
Copyright (c) 2017 svartalf and Actix team
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
person obtaining a copy of this software and associated
|
person obtaining a copy of this software and associated
|
||||||
|
66
README.md
66
README.md
@ -2,23 +2,27 @@
|
|||||||
|
|
||||||
> A collection of additional crates supporting [Actix Web].
|
> A collection of additional crates supporting [Actix Web].
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://github.com/actix/actix-extras/actions/workflows/ci.yml)
|
[](https://github.com/actix/actix-extras/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/actix/actix-extras)
|
[](https://codecov.io/gh/actix/actix-extras)
|
||||||
[](https://discord.gg/5Ux4QGChWc)
|
[](https://discord.gg/5Ux4QGChWc)
|
||||||
[](https://deps.rs/repo/github/actix/actix-extras)
|
[](https://deps.rs/repo/github/actix/actix-extras)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
## Crates by @actix
|
## Crates by @actix
|
||||||
|
|
||||||
| Crate | | |
|
| Crate | | |
|
||||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| [actix-cors] | [](https://crates.io/crates/actix-cors) [](https://deps.rs/crate/actix-cors/0.6.4) | Cross-Origin Resource Sharing (CORS) controls. |
|
| [actix-cors] | [](https://crates.io/crates/actix-cors) [](https://deps.rs/crate/actix-cors) | Cross-Origin Resource Sharing (CORS) controls. |
|
||||||
| [actix-identity] | [](https://crates.io/crates/actix-identity) [](https://deps.rs/crate/actix-identity/0.5.2) | Identity management. |
|
| [actix-identity] | [](https://crates.io/crates/actix-identity) [](https://deps.rs/crate/actix-identity) | Identity management. |
|
||||||
| [actix-limitation] | [](https://crates.io/crates/actix-limitation) [](https://deps.rs/crate/actix-limitation/0.4.0) | Rate-limiting using a fixed window counter for arbitrary keys, backed by Redis. |
|
| [actix-limitation] | [](https://crates.io/crates/actix-limitation) [](https://deps.rs/crate/actix-limitation) | Rate-limiting using a fixed window counter for arbitrary keys, backed by Redis. |
|
||||||
| [actix-protobuf] | [](https://crates.io/crates/actix-protobuf) [](https://deps.rs/crate/actix-protobuf/0.9.0) | Protobuf payload extractor. |
|
| [actix-protobuf] | [](https://crates.io/crates/actix-protobuf) [](https://deps.rs/crate/actix-protobuf) | Protobuf payload extractor. |
|
||||||
| [actix-redis] | [](https://crates.io/crates/actix-redis) [](https://deps.rs/crate/actix-redis/0.12.0) | Actor-based Redis client. |
|
| [actix-session] | [](https://crates.io/crates/actix-session) [](https://deps.rs/crate/actix-session) | Session management. |
|
||||||
| [actix-session] | [](https://crates.io/crates/actix-session) [](https://deps.rs/crate/actix-session/0.7.2) | Session management. |
|
| [actix-settings] | [](https://crates.io/crates/actix-settings) [](https://deps.rs/crate/actix-settings) | Easily manage Actix Web's settings from a TOML file and environment variables. |
|
||||||
| [actix-settings] | [](https://crates.io/crates/actix-settings) [](https://deps.rs/crate/actix-settings/0.6.0) | Easily manage Actix Web's settings from a TOML file and environment variables. |
|
| [actix-web-httpauth] | [](https://crates.io/crates/actix-web-httpauth) [](https://deps.rs/crate/actix-web-httpauth) | HTTP authentication schemes. |
|
||||||
| [actix-web-httpauth] | [](https://crates.io/crates/actix-web-httpauth) [](https://deps.rs/crate/actix-web-httpauth/0.8.0) | HTTP authentication schemes. |
|
| [actix-ws] | [][actix-ws] [](https://deps.rs/crate/actix-ws) | WebSockets for Actix Web, without actors. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -27,23 +31,26 @@
|
|||||||
These crates are provided by the community.
|
These crates are provided by the community.
|
||||||
|
|
||||||
| Crate | | |
|
| Crate | | |
|
||||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
| [actix-web-lab] | [][actix-web-lab] [](https://deps.rs/crate/actix-web-lab/0.19.1) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. |
|
| [actix-web-lab] | [][actix-web-lab] [](https://deps.rs/crate/actix-web-lab) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. |
|
||||||
| [actix-multipart-extract] | [][actix-multipart-extract] [](https://deps.rs/crate/actix-multipart-extract/0.1.5) | Better multipart form support for Actix Web. |
|
| [actix-form-data] | [][actix-form-data] [](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams. |
|
||||||
| [actix-form-data] | [][actix-form-data] [](https://deps.rs/crate/actix-form-data/0.7.0-beta.0) | Multipart form data from actix multipart streams |
|
| [actix-governor] | [][actix-governor] [](https://deps.rs/crate/actix-governor) | Rate-limiting backed by governor. |
|
||||||
| [actix-governor] | [][actix-governor] [](https://deps.rs/crate/actix-governor/0.4.0) | Rate-limiting backed by governor. |
|
| [actix-casbin] | [][actix-casbin] [](https://deps.rs/crate/actix-casbin) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
|
||||||
| [actix-casbin] | [][actix-casbin] [](https://deps.rs/crate/actix-casbin/0.4.2) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
|
| [actix-ip-filter] | [][actix-ip-filter] [](https://deps.rs/crate/actix-ip-filter) | IP address filter. Supports glob patterns. |
|
||||||
| [actix-ip-filter] | [][actix-ip-filter] [](https://deps.rs/crate/actix-ip-filter/0.3.1) | IP address filter. Supports glob patterns. |
|
| [actix-web-static-files] | [][actix-web-static-files] [](https://deps.rs/crate/actix-web-static-files) | Static files as embedded resources. |
|
||||||
| [actix-web-static-files] | [][actix-web-static-files] [](https://deps.rs/crate/actix-web-static-files/4.0.1) | Static files as embedded resources. |
|
| [actix-web-grants] | [][actix-web-grants] [](https://deps.rs/crate/actix-web-grants) | Extension for validating user authorities. |
|
||||||
| [actix-web-grants] | [][actix-web-grants] [](https://deps.rs/crate/actix-web-grants/3.0.1) | Extension for validating user authorities. |
|
| [aliri_actix] | [][aliri_actix] [](https://deps.rs/crate/aliri_actix) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
|
||||||
| [aliri_actix] | [][aliri_actix] [](https://deps.rs/crate/aliri_actix/0.8.0) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
|
| [actix-web-flash-messages] | [][actix-web-flash-messages] [](https://deps.rs/crate/actix-web-flash-messages) | Support for flash messages/one-time notifications in `actix-web`. |
|
||||||
| [actix-web-flash-messages] | [][actix-web-flash-messages] [](https://deps.rs/crate/actix-web-flash-messages/0.4.2) | Support for flash messages/one-time notifications in `actix-web`. |
|
| [awmp] | [][awmp] [](https://deps.rs/crate/awmp) | An easy to use wrapper around multipart fields for Actix Web. |
|
||||||
| [awmp] | [][awmp] [](https://deps.rs/crate/awmp/0.8.1) | An easy to use wrapper around multipart fields for Actix Web. |
|
| [tracing-actix-web] | [][tracing-actix-web] [](https://deps.rs/crate/tracing-actix-web) | A middleware to collect telemetry data from applications built on top of the Actix Web framework. |
|
||||||
| [tracing-actix-web] | [][tracing-actix-web] [](https://deps.rs/crate/tracing-actix-web/0.7.3) | A middleware to collect telemetry data from applications built on top of the actix-web framework. |
|
| [actix-hash] | [][actix-hash] [](https://deps.rs/crate/actix-hash) | Hashing utilities for Actix Web. |
|
||||||
| [actix-ws] | [][actix-ws] [](https://deps.rs/crate/actix-ws/0.2.5) | Actor-less WebSockets for the Actix Runtime. |
|
| [actix-bincode] |  [](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web. |
|
||||||
| [actix-hash] | [][actix-hash] [](https://deps.rs/crate/actix-hash/0.5.0) | Hashing utilities for Actix Web. |
|
| [sentinel-actix] |  [](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web. |
|
||||||
| [actix-bincode] |  [](https://deps.rs/crate/actix-bincode/0.2.1) | Bincode payload extractor for Actix Web |
|
| [actix-telepathy] |  [](https://deps.rs/crate/actix-telepathy) | Build distributed applications with `RemoteActors` and `RemoteMessages`. |
|
||||||
| [sentinel-actix] |  [](https://deps.rs/crate/sentinel-actix/0.1.0) | General and flexible protection for Actix Web |
|
| [apistos] |  [](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web. |
|
||||||
|
| [actix-web-validation] |  [](https://deps.rs/crate/actix-web-validation) | Request validation for Actix Web. |
|
||||||
|
| [actix-jwt-cookies] |  [](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies?path=%2F) | Store your data in encrypted cookies and get it elegantly. |
|
||||||
|
| [actix-ws-broadcaster] |  [](https://deps.rs/repo/github/Necoo33/actix-ws-broadcaster?path=%2F) | A broadcaster library for actix-ws that includes grouping and conditional broadcasting. |
|
||||||
|
|
||||||
To add a crate to this list, submit a pull request.
|
To add a crate to this list, submit a pull request.
|
||||||
|
|
||||||
@ -56,7 +63,6 @@ To add a crate to this list, submit a pull request.
|
|||||||
[actix-identity]: ./actix-identity
|
[actix-identity]: ./actix-identity
|
||||||
[actix-limitation]: ./actix-limitation
|
[actix-limitation]: ./actix-limitation
|
||||||
[actix-protobuf]: ./actix-protobuf
|
[actix-protobuf]: ./actix-protobuf
|
||||||
[actix-redis]: ./actix-redis
|
|
||||||
[actix-session]: ./actix-session
|
[actix-session]: ./actix-session
|
||||||
[actix-settings]: ./actix-settings
|
[actix-settings]: ./actix-settings
|
||||||
[actix-web-httpauth]: ./actix-web-httpauth
|
[actix-web-httpauth]: ./actix-web-httpauth
|
||||||
@ -76,3 +82,9 @@ To add a crate to this list, submit a pull request.
|
|||||||
[actix-hash]: https://crates.io/crates/actix-hash
|
[actix-hash]: https://crates.io/crates/actix-hash
|
||||||
[actix-bincode]: https://crates.io/crates/actix-bincode
|
[actix-bincode]: https://crates.io/crates/actix-bincode
|
||||||
[sentinel-actix]: https://crates.io/crates/sentinel-actix
|
[sentinel-actix]: https://crates.io/crates/sentinel-actix
|
||||||
|
[actix-telepathy]: https://crates.io/crates/actix-telepathy
|
||||||
|
[actix-web-validation]: https://crates.io/crates/actix-web-validation
|
||||||
|
[actix-telepathy]: https://crates.io/crates/actix-telepathy
|
||||||
|
[apistos]: https://crates.io/crates/apistos
|
||||||
|
[actix-jwt-cookies]: https://crates.io/crates/actix-jwt-cookies
|
||||||
|
[actix-ws-broadcaster]: https://crates.io/crates/actix-ws-broadcaster
|
||||||
|
@ -2,38 +2,46 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Implement `PartialEq` for `Cors` allowing for better testing.
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- `Cors` is now marked `#[must_use]`.
|
||||||
|
- Default for `Cors::block_on_origin_mismatch` is now false.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
|
## 0.6.5
|
||||||
|
|
||||||
|
- Fix `Vary` header when Private Network Access is enabled.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.68.
|
- Minimum supported Rust version (MSRV) is now 1.68.
|
||||||
|
|
||||||
## 0.6.4
|
## 0.6.4
|
||||||
|
|
||||||
- Add `Cors::allow_private_network_access()` behind an unstable flag (`draft-private-network-access`). [#297]
|
- Add `Cors::allow_private_network_access()` behind an unstable flag (`draft-private-network-access`).
|
||||||
|
|
||||||
[#297]: https://github.com/actix/actix-extras/pull/297
|
|
||||||
|
|
||||||
## 0.6.3
|
## 0.6.3
|
||||||
|
|
||||||
- Add `Cors::block_on_origin_mismatch()` option for controlling if requests are pre-emptively rejected. [#287]
|
- Add `Cors::block_on_origin_mismatch()` option for controlling if requests are pre-emptively rejected.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
|
||||||
[#287]: https://github.com/actix/actix-extras/pull/287
|
|
||||||
|
|
||||||
## 0.6.2
|
## 0.6.2
|
||||||
|
|
||||||
- Fix `expose_any_header` to return list of response headers. [#273]
|
- Fix `expose_any_header` to return list of response headers.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
[#273]: https://github.com/actix/actix-extras/pull/273
|
|
||||||
|
|
||||||
## 0.6.1
|
## 0.6.1
|
||||||
|
|
||||||
- Do not consider requests without a `Access-Control-Request-Method` as preflight. [#226]
|
- Do not consider requests without a `Access-Control-Request-Method` as preflight.
|
||||||
|
|
||||||
[#226]: https://github.com/actix/actix-extras/pull/226
|
|
||||||
|
|
||||||
## 0.6.0
|
## 0.6.0
|
||||||
|
|
||||||
- Update `actix-web` dependency to 4.0.
|
- Update `actix-web` dependency to 4.0.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>0.6.0 pre-releases</summary>
|
||||||
|
|
||||||
## 0.6.0-beta.10
|
## 0.6.0-beta.10
|
||||||
|
|
||||||
- Ensure that preflight responses contain a `Vary` header. [#224]
|
- Ensure that preflight responses contain a `Vary` header. [#224]
|
||||||
@ -93,6 +101,8 @@
|
|||||||
- Update `actix-web` dependency to 4.0.0 beta.
|
- Update `actix-web` dependency to 4.0.0 beta.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.46.0.
|
- Minimum supported Rust version (MSRV) is now 1.46.0.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 0.5.4
|
## 0.5.4
|
||||||
|
|
||||||
- Fix `expose_any_header` method, now set the correct field. [#143]
|
- Fix `expose_any_header` method, now set the correct field. [#143]
|
||||||
@ -121,13 +131,11 @@
|
|||||||
- `CorsFactory` is removed. [#119]
|
- `CorsFactory` is removed. [#119]
|
||||||
- The `impl Default` constructor is now overly-restrictive. [#119]
|
- The `impl Default` constructor is now overly-restrictive. [#119]
|
||||||
- Added `Cors::permissive()` constructor that allows anything. [#119]
|
- Added `Cors::permissive()` constructor that allows anything. [#119]
|
||||||
- Adds methods for each property to reset to a permissive state. (`allow_any_origin`,
|
- Adds methods for each property to reset to a permissive state. (`allow_any_origin`, `expose_any_header`, etc.) [#119]
|
||||||
`expose_any_header`, etc.) [#119]
|
|
||||||
- Errors are now propagated with `Transform::InitError` instead of panicking. [#119]
|
- Errors are now propagated with `Transform::InitError` instead of panicking. [#119]
|
||||||
- Fixes bug where allowed origin functions are not called if `allowed_origins` is All. [#119]
|
- Fixes bug where allowed origin functions are not called if `allowed_origins` is All. [#119]
|
||||||
- `AllOrSome` is no longer public. [#119]
|
- `AllOrSome` is no longer public. [#119]
|
||||||
- Functions used for `allowed_origin_fn` now receive the Origin HeaderValue as the
|
- Functions used for `allowed_origin_fn` now receive the Origin HeaderValue as the first parameter. [#120]
|
||||||
first parameter. [#120]
|
|
||||||
|
|
||||||
[#114]: https://github.com/actix/actix-extras/pull/114
|
[#114]: https://github.com/actix/actix-extras/pull/114
|
||||||
[#118]: https://github.com/actix/actix-extras/pull/118
|
[#118]: https://github.com/actix/actix-extras/pull/118
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-cors"
|
name = "actix-cors"
|
||||||
version = "0.6.4"
|
version = "0.7.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
]
|
]
|
||||||
description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web"
|
description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web"
|
||||||
keywords = ["actix", "cors", "web", "security", "crossorigin"]
|
keywords = ["actix", "cors", "web", "security", "crossorigin"]
|
||||||
homepage = "https://actix.rs"
|
repository.workspace = true
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
homepage.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -24,7 +24,7 @@ draft-private-network-access = []
|
|||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
derive_more = "0.99.7"
|
derive_more = { version = "2", features = ["display", "error"] }
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
@ -32,5 +32,8 @@ smallvec = "1"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4", default-features = false, features = ["macros"] }
|
actix-web = { version = "4", default-features = false, features = ["macros"] }
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -1,14 +1,72 @@
|
|||||||
# actix-cors
|
# actix-cors
|
||||||
|
|
||||||
> Cross-Origin Resource Sharing (CORS) controls for Actix Web.
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-cors)
|
[](https://crates.io/crates/actix-cors)
|
||||||
[](https://docs.rs/actix-cors/0.6.4)
|
[](https://docs.rs/actix-cors/0.7.1)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-cors/0.6.4)
|

|
||||||
|
<br />
|
||||||
|
[](https://deps.rs/crate/actix-cors/0.7.1)
|
||||||
|
[](https://crates.io/crates/actix-cors)
|
||||||
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- cargo-rdme start -->
|
||||||
|
|
||||||
|
Cross-Origin Resource Sharing (CORS) controls for Actix Web.
|
||||||
|
|
||||||
|
This middleware can be applied to both applications and resources. Once built, a [`Cors`] builder can be used as an argument for Actix Web's `App::wrap()`, `Scope::wrap()`, or `Resource::wrap()` methods.
|
||||||
|
|
||||||
|
This CORS middleware automatically handles `OPTIONS` preflight requests.
|
||||||
|
|
||||||
|
## Crate Features
|
||||||
|
|
||||||
|
- `draft-private-network-access`: ⚠️ Unstable. Adds opt-in support for the [Private Network Access] spec extensions. This feature is unstable since it will follow breaking changes in the draft spec until it is finalized.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_cors::Cors;
|
||||||
|
use actix_web::{get, http, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
|
||||||
|
#[get("/index.html")]
|
||||||
|
async fn index(req: HttpRequest) -> &'static str {
|
||||||
|
"<p>Hello World!</p>"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
HttpServer::new(|| {
|
||||||
|
let cors = Cors::default()
|
||||||
|
.allowed_origin("https://www.rust-lang.org")
|
||||||
|
.allowed_origin_fn(|origin, _req_head| {
|
||||||
|
origin.as_bytes().ends_with(b".rust-lang.org")
|
||||||
|
})
|
||||||
|
.allowed_methods(vec!["GET", "POST"])
|
||||||
|
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
|
||||||
|
.allowed_header(http::header::CONTENT_TYPE)
|
||||||
|
.max_age(3600);
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.wrap(cors)
|
||||||
|
.service(index)
|
||||||
|
})
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Private Network Access]: https://wicg.github.io/private-network-access
|
||||||
|
|
||||||
|
<!-- cargo-rdme end -->
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-cors)
|
- [API Documentation](https://docs.rs/actix-cors)
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/cors)
|
- [Example Project](https://github.com/actix/examples/tree/master/cors)
|
||||||
- Minimum Supported Rust Version (MSRV): 1.57
|
- Minimum Supported Rust Version (MSRV): 1.75
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashSet, convert::TryInto, iter::FromIterator, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use actix_utils::future::{self, Ready};
|
use actix_utils::future::{self, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@ -52,13 +52,20 @@ static ALL_METHODS_SET: Lazy<HashSet<Method>> = Lazy::new(|| {
|
|||||||
/// The alternative [`Cors::permissive()`] constructor is available for local development, allowing
|
/// The alternative [`Cors::permissive()`] constructor is available for local development, allowing
|
||||||
/// all origins and headers, etc. **The permissive constructor should not be used in production.**
|
/// all origins and headers, etc. **The permissive constructor should not be used in production.**
|
||||||
///
|
///
|
||||||
|
/// # Behavior
|
||||||
|
///
|
||||||
|
/// In all cases, behavior for this crate follows the [Fetch Standard CORS protocol]. See that
|
||||||
|
/// document for information on exact semantics for configuration options and combinations.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
///
|
||||||
/// Errors surface in the middleware initialization phase. This means that, if you have logs enabled
|
/// Errors surface in the middleware initialization phase. This means that, if you have logs enabled
|
||||||
/// in Actix Web (using `env_logger` or other crate that exposes logs from the `log` crate), error
|
/// in Actix Web (using `env_logger` or other crate that exposes logs from the `log` crate), error
|
||||||
/// messages will outline what is wrong with the CORS configuration in the server logs and the
|
/// messages will outline what is wrong with the CORS configuration in the server logs and the
|
||||||
/// server will fail to start up or serve requests.
|
/// server will fail to start up or serve requests.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use actix_cors::Cors;
|
/// use actix_cors::Cors;
|
||||||
/// use actix_web::http::header;
|
/// use actix_web::http::header;
|
||||||
@ -72,14 +79,18 @@ static ALL_METHODS_SET: Lazy<HashSet<Method>> = Lazy::new(|| {
|
|||||||
///
|
///
|
||||||
/// // `cors` can now be used in `App::wrap`.
|
/// // `cors` can now be used in `App::wrap`.
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[must_use]
|
||||||
pub struct Cors {
|
pub struct Cors {
|
||||||
inner: Rc<Inner>,
|
inner: Rc<Inner>,
|
||||||
error: Option<Either<HttpError, CorsError>>,
|
error: Option<Either<HttpError, CorsError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cors {
|
impl Cors {
|
||||||
/// A very permissive set of default for quick development. Not recommended for production use.
|
/// Constructs a very permissive set of defaults for quick development. (Not recommended for
|
||||||
|
/// production use.)
|
||||||
///
|
///
|
||||||
/// *All* origins, methods, request headers and exposed headers allowed. Credentials supported.
|
/// *All* origins, methods, request headers and exposed headers allowed. Credentials supported.
|
||||||
/// Max age 1 hour. Does not send wildcard.
|
/// Max age 1 hour. Does not send wildcard.
|
||||||
@ -104,7 +115,7 @@ impl Cors {
|
|||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
allow_private_network_access: false,
|
allow_private_network_access: false,
|
||||||
vary_header: true,
|
vary_header: true,
|
||||||
block_on_origin_mismatch: true,
|
block_on_origin_mismatch: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Cors {
|
Cors {
|
||||||
@ -124,7 +135,7 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an origin that is allowed to make requests.
|
/// Adds an origin that is allowed to make requests.
|
||||||
///
|
///
|
||||||
/// This method allows specifying a finite set of origins to verify the value of the `Origin`
|
/// This method allows specifying a finite set of origins to verify the value of the `Origin`
|
||||||
/// request header. These are `origin-or-null` types in the [Fetch Standard].
|
/// request header. These are `origin-or-null` types in the [Fetch Standard].
|
||||||
@ -177,7 +188,7 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determinate allowed origins by processing requests which didn't match any origins specified
|
/// Determinates allowed origins by processing requests which didn't match any origins specified
|
||||||
/// in the `allowed_origin`.
|
/// in the `allowed_origin`.
|
||||||
///
|
///
|
||||||
/// The function will receive two parameters, the Origin header value, and the `RequestHead` of
|
/// The function will receive two parameters, the Origin header value, and the `RequestHead` of
|
||||||
@ -203,20 +214,17 @@ impl Cors {
|
|||||||
/// See [`Cors::allowed_methods`] for more info on allowed methods.
|
/// See [`Cors::allowed_methods`] for more info on allowed methods.
|
||||||
pub fn allow_any_method(mut self) -> Cors {
|
pub fn allow_any_method(mut self) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.allowed_methods = ALL_METHODS_SET.clone();
|
ALL_METHODS_SET.clone_into(&mut cors.allowed_methods);
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a list of methods which allowed origins can perform.
|
/// Sets a list of methods which allowed origins can perform.
|
||||||
///
|
///
|
||||||
/// These will be sent in the `Access-Control-Allow-Methods` response header as specified in
|
/// These will be sent in the `Access-Control-Allow-Methods` response header.
|
||||||
/// the [Fetch Standard CORS protocol].
|
|
||||||
///
|
///
|
||||||
/// This defaults to an empty set.
|
/// This defaults to an empty set.
|
||||||
///
|
|
||||||
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
|
||||||
pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors
|
pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors
|
||||||
where
|
where
|
||||||
U: IntoIterator<Item = M>,
|
U: IntoIterator<Item = M>,
|
||||||
@ -279,16 +287,13 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a list of request header field names which can be used when this resource is accessed by
|
/// Sets a list of request header field names which can be used when this resource is accessed
|
||||||
/// allowed origins.
|
/// by allowed origins.
|
||||||
///
|
///
|
||||||
/// If `All` is set, whatever is requested by the client in `Access-Control-Request-Headers`
|
/// If `All` is set, whatever is requested by the client in `Access-Control-Request-Headers`
|
||||||
/// will be echoed back in the `Access-Control-Allow-Headers` header as specified in
|
/// will be echoed back in the `Access-Control-Allow-Headers` header.
|
||||||
/// the [Fetch Standard CORS protocol].
|
|
||||||
///
|
///
|
||||||
/// This defaults to an empty set.
|
/// This defaults to an empty set.
|
||||||
///
|
|
||||||
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
|
||||||
pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors
|
pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors
|
||||||
where
|
where
|
||||||
U: IntoIterator<Item = H>,
|
U: IntoIterator<Item = H>,
|
||||||
@ -329,13 +334,11 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a list of headers which are safe to expose to the API of a CORS API specification.
|
/// Sets a list of headers which are safe to expose to the API of a CORS API specification.
|
||||||
/// This corresponds to the `Access-Control-Expose-Headers` response header as specified in
|
///
|
||||||
/// the [Fetch Standard CORS protocol].
|
/// This corresponds to the `Access-Control-Expose-Headers` response header.
|
||||||
///
|
///
|
||||||
/// This defaults to an empty set.
|
/// This defaults to an empty set.
|
||||||
///
|
|
||||||
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
|
||||||
pub fn expose_headers<U, H>(mut self, headers: U) -> Cors
|
pub fn expose_headers<U, H>(mut self, headers: U) -> Cors
|
||||||
where
|
where
|
||||||
U: IntoIterator<Item = H>,
|
U: IntoIterator<Item = H>,
|
||||||
@ -364,12 +367,11 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a maximum time (in seconds) for which this CORS request may be cached. This value is set
|
/// Sets a maximum time (in seconds) for which this CORS request may be cached.
|
||||||
/// as the `Access-Control-Max-Age` header as specified in the [Fetch Standard CORS protocol].
|
///
|
||||||
|
/// This value is set as the `Access-Control-Max-Age` header.
|
||||||
///
|
///
|
||||||
/// Pass a number (of seconds) or use None to disable sending max age header.
|
/// Pass a number (of seconds) or use None to disable sending max age header.
|
||||||
///
|
|
||||||
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
|
||||||
pub fn max_age(mut self, max_age: impl Into<Option<usize>>) -> Cors {
|
pub fn max_age(mut self, max_age: impl Into<Option<usize>>) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.max_age = max_age.into();
|
cors.max_age = max_age.into();
|
||||||
@ -378,17 +380,17 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set to use wildcard origins.
|
/// Configures use of wildcard (`*`) origin in responses when appropriate.
|
||||||
///
|
///
|
||||||
/// If send wildcard is set and the `allowed_origins` parameter is `All`, a wildcard
|
/// If send wildcard is set and the `allowed_origins` parameter is `All`, a wildcard
|
||||||
/// `Access-Control-Allow-Origin` response header is sent, rather than the request’s
|
/// `Access-Control-Allow-Origin` response header is sent, rather than the request’s
|
||||||
/// `Origin` header.
|
/// `Origin` header.
|
||||||
///
|
///
|
||||||
/// This **CANNOT** be used in conjunction with `allowed_origins` set to `All` and
|
/// This option **CANNOT** be used in conjunction with a [credential
|
||||||
/// `allow_credentials` set to `true`. Depending on the mode of usage, this will either result
|
/// supported](Self::supports_credentials()) configuration. Doing so will result in an error
|
||||||
/// in an `CorsError::CredentialsWithWildcardOrigin` error during actix launch or runtime.
|
/// during server startup.
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to disabled.
|
||||||
pub fn send_wildcard(mut self) -> Cors {
|
pub fn send_wildcard(mut self) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.send_wildcard = true;
|
cors.send_wildcard = true;
|
||||||
@ -397,21 +399,16 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows users to make authenticated requests
|
/// Allows users to make authenticated requests.
|
||||||
///
|
///
|
||||||
/// If true, injects the `Access-Control-Allow-Credentials` header in responses. This allows
|
/// If true, injects the `Access-Control-Allow-Credentials` header in responses. This allows
|
||||||
/// cookies and credentials to be submitted across domains as specified in
|
/// cookies and credentials to be submitted across domains.
|
||||||
/// the [Fetch Standard CORS protocol].
|
|
||||||
///
|
///
|
||||||
/// This option cannot be used in conjunction with an `allowed_origin` set to `All` and
|
/// This option **CANNOT** be used in conjunction with option cannot be used in conjunction
|
||||||
/// `send_wildcards` set to `true`.
|
/// with [wildcard origins](Self::send_wildcard()) configured. Doing so will result in an error
|
||||||
|
/// during server startup.
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to disabled.
|
||||||
///
|
|
||||||
/// A server initialization error will occur if credentials are allowed, but the Origin is set
|
|
||||||
/// to send wildcards (`*`); this is not allowed by the CORS protocol.
|
|
||||||
///
|
|
||||||
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
|
|
||||||
pub fn supports_credentials(mut self) -> Cors {
|
pub fn supports_credentials(mut self) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.supports_credentials = true;
|
cors.supports_credentials = true;
|
||||||
@ -439,7 +436,7 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable `Vary` header support.
|
/// Disables `Vary` header support.
|
||||||
///
|
///
|
||||||
/// When enabled the header `Vary: Origin` will be returned as per the Fetch Standard
|
/// When enabled the header `Vary: Origin` will be returned as per the Fetch Standard
|
||||||
/// implementation guidelines.
|
/// implementation guidelines.
|
||||||
@ -457,12 +454,12 @@ impl Cors {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable support for preflight requests.
|
/// Disables preflight request handling.
|
||||||
///
|
///
|
||||||
/// When enabled CORS middleware automatically handles `OPTIONS` requests.
|
/// When enabled CORS middleware automatically handles `OPTIONS` requests. This is useful for
|
||||||
/// This is useful for application level middleware.
|
/// application level middleware.
|
||||||
///
|
///
|
||||||
/// By default *preflight* support is enabled.
|
/// By default, preflight support is enabled.
|
||||||
pub fn disable_preflight(mut self) -> Cors {
|
pub fn disable_preflight(mut self) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.preflight = false;
|
cors.preflight = false;
|
||||||
@ -480,7 +477,7 @@ impl Cors {
|
|||||||
/// and block requests based on pre-flight requests. Use this setting to allow cURL and other
|
/// and block requests based on pre-flight requests. Use this setting to allow cURL and other
|
||||||
/// non-browser HTTP clients to function as normal, no matter what `Origin` the request has.
|
/// non-browser HTTP clients to function as normal, no matter what `Origin` the request has.
|
||||||
///
|
///
|
||||||
/// Defaults to `true`.
|
/// Defaults to false.
|
||||||
pub fn block_on_origin_mismatch(mut self, block: bool) -> Cors {
|
pub fn block_on_origin_mismatch(mut self, block: bool) -> Cors {
|
||||||
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
if let Some(cors) = cors(&mut self.inner, &self.error) {
|
||||||
cors.block_on_origin_mismatch = block;
|
cors.block_on_origin_mismatch = block;
|
||||||
@ -516,7 +513,7 @@ impl Default for Cors {
|
|||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
allow_private_network_access: false,
|
allow_private_network_access: false,
|
||||||
vary_header: true,
|
vary_header: true,
|
||||||
block_on_origin_mismatch: true,
|
block_on_origin_mismatch: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Cors {
|
Cors {
|
||||||
@ -611,14 +608,27 @@ where
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Cors {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
// Because of the cors-function, checking if the content is equal implies that the errors are equal
|
||||||
|
//
|
||||||
|
// Proof by contradiction:
|
||||||
|
// Lets assume that the inner values are equal, but the error values are not.
|
||||||
|
// This means there had been an error, which has been fixed.
|
||||||
|
// This cannot happen as the first call to set the invalid value means that further usages of the cors-function will reject other input.
|
||||||
|
// => inner has to be in a different state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::convert::{Infallible, TryInto};
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body,
|
body,
|
||||||
dev::{fn_service, Transform},
|
dev::fn_service,
|
||||||
http::{header::HeaderName, StatusCode},
|
http::StatusCode,
|
||||||
test::{self, TestRequest},
|
test::{self, TestRequest},
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
};
|
};
|
||||||
@ -649,8 +659,9 @@ mod test {
|
|||||||
.insert_header(("Origin", "https://www.example.com"))
|
.insert_header(("Origin", "https://www.example.com"))
|
||||||
.to_srv_request();
|
.to_srv_request();
|
||||||
|
|
||||||
let resp = test::call_service(&cors, req).await;
|
let res = test::call_service(&cors, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert!(!res.headers().contains_key("Access-Control-Allow-Origin"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
@ -681,4 +692,11 @@ mod test {
|
|||||||
|
|
||||||
Cors::default().new_transform(srv).await.unwrap();
|
Cors::default().new_transform(srv).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn impl_eq() {
|
||||||
|
assert_eq!(Cors::default(), Cors::default());
|
||||||
|
assert_ne!(Cors::default().send_wildcard(), Cors::default());
|
||||||
|
assert_ne!(Cors::default(), Cors::permissive());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,40 @@
|
|||||||
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
|
||||||
use derive_more::{Display, Error};
|
use derive_more::derive::{Display, Error};
|
||||||
|
|
||||||
/// Errors that can occur when processing CORS guarded requests.
|
/// Errors that can occur when processing CORS guarded requests.
|
||||||
#[derive(Debug, Clone, Display, Error)]
|
#[derive(Debug, Clone, Display, Error)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum CorsError {
|
pub enum CorsError {
|
||||||
/// Allowed origin argument must not be wildcard (`*`).
|
/// Allowed origin argument must not be wildcard (`*`).
|
||||||
#[display(fmt = "`allowed_origin` argument must not be wildcard (`*`)")]
|
#[display("`allowed_origin` argument must not be wildcard (`*`)")]
|
||||||
WildcardOrigin,
|
WildcardOrigin,
|
||||||
|
|
||||||
/// Request header `Origin` is required but was not provided.
|
/// Request header `Origin` is required but was not provided.
|
||||||
#[display(fmt = "Request header `Origin` is required but was not provided")]
|
#[display("Request header `Origin` is required but was not provided")]
|
||||||
MissingOrigin,
|
MissingOrigin,
|
||||||
|
|
||||||
/// Request header `Access-Control-Request-Method` is required but is missing.
|
/// Request header `Access-Control-Request-Method` is required but is missing.
|
||||||
#[display(fmt = "Request header `Access-Control-Request-Method` is required but is missing")]
|
#[display("Request header `Access-Control-Request-Method` is required but is missing")]
|
||||||
MissingRequestMethod,
|
MissingRequestMethod,
|
||||||
|
|
||||||
/// Request header `Access-Control-Request-Method` has an invalid value.
|
/// Request header `Access-Control-Request-Method` has an invalid value.
|
||||||
#[display(fmt = "Request header `Access-Control-Request-Method` has an invalid value")]
|
#[display("Request header `Access-Control-Request-Method` has an invalid value")]
|
||||||
BadRequestMethod,
|
BadRequestMethod,
|
||||||
|
|
||||||
/// Request header `Access-Control-Request-Headers` has an invalid value.
|
/// Request header `Access-Control-Request-Headers` has an invalid value.
|
||||||
#[display(fmt = "Request header `Access-Control-Request-Headers` has an invalid value")]
|
#[display("Request header `Access-Control-Request-Headers` has an invalid value")]
|
||||||
BadRequestHeaders,
|
BadRequestHeaders,
|
||||||
|
|
||||||
/// Origin is not allowed to make this request.
|
/// Origin is not allowed to make this request.
|
||||||
#[display(fmt = "Origin is not allowed to make this request")]
|
#[display("Origin is not allowed to make this request")]
|
||||||
OriginNotAllowed,
|
OriginNotAllowed,
|
||||||
|
|
||||||
/// Request method is not allowed.
|
/// Request method is not allowed.
|
||||||
#[display(fmt = "Requested method is not allowed")]
|
#[display("Requested method is not allowed")]
|
||||||
MethodNotAllowed,
|
MethodNotAllowed,
|
||||||
|
|
||||||
/// One or more request headers are not allowed.
|
/// One or more request headers are not allowed.
|
||||||
#[display(fmt = "One or more request headers are not allowed")]
|
#[display("One or more request headers are not allowed")]
|
||||||
HeadersNotAllowed,
|
HeadersNotAllowed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
use std::{
|
use std::{collections::HashSet, fmt, rc::Rc};
|
||||||
collections::HashSet,
|
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
fmt,
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::RequestHead,
|
dev::RequestHead,
|
||||||
@ -32,6 +27,12 @@ impl Default for OriginFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for OriginFn {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.boxed_fn, &other.boxed_fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for OriginFn {
|
impl fmt::Debug for OriginFn {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str("origin_fn")
|
f.write_str("origin_fn")
|
||||||
@ -45,7 +46,7 @@ pub(crate) fn header_value_try_into_method(hdr: &HeaderValue) -> Option<Method>
|
|||||||
.and_then(|meth| Method::try_from(meth).ok())
|
.and_then(|meth| Method::try_from(meth).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct Inner {
|
pub(crate) struct Inner {
|
||||||
pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>,
|
pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>,
|
||||||
pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>,
|
pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>,
|
||||||
@ -223,7 +224,7 @@ pub(crate) fn add_vary_header(headers: &mut HeaderMap) {
|
|||||||
val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
|
val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
|
||||||
|
|
||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
val.extend(b", Access-Control-Allow-Private-Network");
|
val.extend(b", Access-Control-Request-Private-Network");
|
||||||
|
|
||||||
val.try_into().unwrap()
|
val.try_into().unwrap()
|
||||||
}
|
}
|
||||||
@ -231,7 +232,7 @@ pub(crate) fn add_vary_header(headers: &mut HeaderMap) {
|
|||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
None => HeaderValue::from_static(
|
None => HeaderValue::from_static(
|
||||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, \
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, \
|
||||||
Access-Control-Allow-Private-Network",
|
Access-Control-Request-Private-Network",
|
||||||
),
|
),
|
||||||
|
|
||||||
#[cfg(not(feature = "draft-private-network-access"))]
|
#[cfg(not(feature = "draft-private-network-access"))]
|
||||||
@ -266,6 +267,7 @@ mod test {
|
|||||||
async fn test_validate_not_allowed_origin() {
|
async fn test_validate_not_allowed_origin() {
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
.allowed_origin("https://www.example.com")
|
.allowed_origin("https://www.example.com")
|
||||||
|
.block_on_origin_mismatch(true)
|
||||||
.new_transform(test::ok_service())
|
.new_transform(test::ok_service())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -49,7 +49,6 @@
|
|||||||
//! [Private Network Access]: https://wicg.github.io/private-network-access
|
//! [Private Network Access]: https://wicg.github.io/private-network-access
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
|
||||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
@ -61,8 +60,8 @@ mod error;
|
|||||||
mod inner;
|
mod inner;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
|
||||||
use all_or_some::AllOrSome;
|
use crate::{
|
||||||
pub use builder::Cors;
|
all_or_some::AllOrSome,
|
||||||
pub use error::CorsError;
|
inner::{Inner, OriginFn},
|
||||||
use inner::{Inner, OriginFn};
|
};
|
||||||
pub use middleware::CorsMiddleware;
|
pub use crate::{builder::Cors, error::CorsError, middleware::CorsMiddleware};
|
||||||
|
@ -272,7 +272,7 @@ async fn test_response() {
|
|||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
|
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
|
||||||
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network"[..]),
|
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"[..]),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::needless_collect)]
|
#[allow(clippy::needless_collect)]
|
||||||
@ -328,7 +328,7 @@ async fn test_response() {
|
|||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::VARY).map(HeaderValue::as_bytes).unwrap(),
|
resp.headers().get(header::VARY).map(HeaderValue::as_bytes).unwrap(),
|
||||||
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network",
|
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||||
);
|
);
|
||||||
|
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
@ -382,12 +382,13 @@ async fn test_blocks_mismatched_origin_by_default() {
|
|||||||
.to_srv_request();
|
.to_srv_request();
|
||||||
|
|
||||||
let res = test::call_service(&cors, req).await;
|
let res = test::call_service(&cors, req).await;
|
||||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
assert_eq!(res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN), None);
|
assert!(!res
|
||||||
assert!(res
|
|
||||||
.headers()
|
.headers()
|
||||||
.get(header::ACCESS_CONTROL_ALLOW_METHODS)
|
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||||
.is_none());
|
assert!(!res
|
||||||
|
.headers()
|
||||||
|
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
@ -494,7 +495,7 @@ async fn vary_header_on_all_handled_responses() {
|
|||||||
.expect("response should have Vary header")
|
.expect("response should have Vary header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network",
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||||
);
|
);
|
||||||
|
|
||||||
// follow-up regular request
|
// follow-up regular request
|
||||||
@ -520,7 +521,7 @@ async fn vary_header_on_all_handled_responses() {
|
|||||||
.expect("response should have Vary header")
|
.expect("response should have Vary header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network",
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||||
);
|
);
|
||||||
|
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
@ -529,16 +530,23 @@ async fn vary_header_on_all_handled_responses() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// regular request bad origin
|
// regular request OK with no CORS response headers
|
||||||
let req = TestRequest::default()
|
let req = TestRequest::default()
|
||||||
.method(Method::PUT)
|
.method(Method::PUT)
|
||||||
.insert_header((header::ORIGIN, "https://www.example.com"))
|
.insert_header((header::ORIGIN, "https://www.example.com"))
|
||||||
.to_srv_request();
|
.to_srv_request();
|
||||||
let resp = test::call_service(&cors, req).await;
|
let res = test::call_service(&cors, req).await;
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert!(!res
|
||||||
|
.headers()
|
||||||
|
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
|
||||||
|
assert!(!res
|
||||||
|
.headers()
|
||||||
|
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
|
||||||
|
|
||||||
#[cfg(not(feature = "draft-private-network-access"))]
|
#[cfg(not(feature = "draft-private-network-access"))]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers()
|
res.headers()
|
||||||
.get(header::VARY)
|
.get(header::VARY)
|
||||||
.expect("response should have Vary header")
|
.expect("response should have Vary header")
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -547,12 +555,12 @@ async fn vary_header_on_all_handled_responses() {
|
|||||||
);
|
);
|
||||||
#[cfg(feature = "draft-private-network-access")]
|
#[cfg(feature = "draft-private-network-access")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers()
|
res.headers()
|
||||||
.get(header::VARY)
|
.get(header::VARY)
|
||||||
.expect("response should have Vary header")
|
.expect("response should have Vary header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network",
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||||
);
|
);
|
||||||
|
|
||||||
// regular request no origin
|
// regular request no origin
|
||||||
@ -575,7 +583,7 @@ async fn vary_header_on_all_handled_responses() {
|
|||||||
.expect("response should have Vary header")
|
.expect("response should have Vary header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Private-Network",
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- Update `actix-session` dependency to `0.10`.
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Add `IdentityMiddlewareBuilder::{id_key, last_visit_unix_timestamp_key, login_unix_timestamp_key}()` methods for customizing keys used in session. Defaults remain the same as before.
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- Update `actix-session` dependency to `0.9`.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.6.0
|
## 0.6.0
|
||||||
|
|
||||||
- Add `error` module.
|
- Add `error` module.
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-identity"
|
name = "actix-identity"
|
||||||
version = "0.6.0"
|
version = "0.8.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Luca Palmieri <rust@lpalmieri.com>",
|
"Luca Palmieri <rust@lpalmieri.com>",
|
||||||
]
|
]
|
||||||
description = "Identity management for Actix Web"
|
description = "Identity management for Actix Web"
|
||||||
keywords = ["actix", "auth", "identity", "web", "security"]
|
keywords = ["actix", "auth", "identity", "web", "security"]
|
||||||
homepage = "https://actix.rs"
|
repository.workspace = true
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
homepage.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -19,20 +19,23 @@ all-features = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-session = "0.8"
|
actix-session = "0.10"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||||
|
|
||||||
derive_more = "0.99.7"
|
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||||
futures-core = "0.3.7"
|
futures-core = "0.3.17"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] }
|
||||||
actix-session = { version = "0.8", features = ["redis-rs-session", "cookie-session"] }
|
actix-session = { version = "0.10", features = ["redis-session", "cookie-session"] }
|
||||||
|
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["cookies", "json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -2,12 +2,105 @@
|
|||||||
|
|
||||||
> Identity management for Actix Web.
|
> Identity management for Actix Web.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-identity)
|
[](https://crates.io/crates/actix-identity)
|
||||||
[](https://docs.rs/actix-identity/0.6.0)
|
[](https://docs.rs/actix-identity/0.8.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-identity/0.6.0)
|
[](https://deps.rs/crate/actix-identity/0.8.0)
|
||||||
|
|
||||||
## Documentation & community resources
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-identity)
|
<!-- cargo-rdme start -->
|
||||||
- Minimum Supported Rust Version (MSRV): 1.57
|
|
||||||
|
Identity management for Actix Web.
|
||||||
|
|
||||||
|
`actix-identity` can be used to track identity of a user across multiple requests. It is built on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To start using identity management in your Actix Web application you must register [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
|
||||||
|
use actix_identity::IdentityMiddleware;
|
||||||
|
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
// When using `Key::generate()` it is important to initialize outside of the
|
||||||
|
// `HttpServer::new` closure. When deployed the secret key should be read from a
|
||||||
|
// configuration file or environment variables.
|
||||||
|
let secret_key = Key::generate();
|
||||||
|
|
||||||
|
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
// Install the identity framework first.
|
||||||
|
.wrap(IdentityMiddleware::default())
|
||||||
|
// The identity system is built on top of sessions. You must install the session
|
||||||
|
// middleware to leverage `actix-identity`. The session middleware must be mounted
|
||||||
|
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
||||||
|
// order of registration when it receives an incoming request.
|
||||||
|
.wrap(SessionMiddleware::new(
|
||||||
|
redis_store.clone(),
|
||||||
|
secret_key.clone(),
|
||||||
|
))
|
||||||
|
// Your request handlers [...]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
User identities can be created, accessed and destroyed using the [`Identity`] extractor in your request handlers:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_session::storage::RedisSessionStore;
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(user: Option<Identity>) -> impl Responder {
|
||||||
|
if let Some(user) = user {
|
||||||
|
format!("Welcome! {}", user.id().unwrap())
|
||||||
|
} else {
|
||||||
|
"Welcome Anonymous!".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login")]
|
||||||
|
async fn login(request: HttpRequest) -> impl Responder {
|
||||||
|
// Some kind of authentication should happen here
|
||||||
|
// e.g. password-based, biometric, etc.
|
||||||
|
// [...]
|
||||||
|
|
||||||
|
// attach a verified user identity to the active session
|
||||||
|
Identity::login(&request.extensions(), "User1".into()).unwrap();
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/logout")]
|
||||||
|
async fn logout(user: Option<Identity>) -> impl Responder {
|
||||||
|
if let Some(user) = user {
|
||||||
|
user.logout();
|
||||||
|
}
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced configuration
|
||||||
|
|
||||||
|
By default, `actix-identity` does not automatically log out users. You can change this behaviour by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
|
||||||
|
|
||||||
|
In particular, you can automatically log out users who:
|
||||||
|
|
||||||
|
- have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]);
|
||||||
|
- logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
|
||||||
|
|
||||||
|
[`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
|
||||||
|
[`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
|
||||||
|
|
||||||
|
<!-- cargo-rdme end -->
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
//! http -v --session=identity GET localhost:8080/
|
//! http -v --session=identity GET localhost:8080/
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::io;
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
use actix_identity::{Identity, IdentityMiddleware};
|
use actix_identity::{Identity, IdentityMiddleware};
|
||||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
|
cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
|
||||||
HttpServer, Responder,
|
HttpServer, Responder,
|
||||||
@ -28,16 +28,25 @@ async fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
let secret_key = Key::generate();
|
let secret_key = Key::generate();
|
||||||
|
|
||||||
|
let expiration = Duration::from_secs(24 * 60 * 60);
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let session_mw =
|
let session_mw =
|
||||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||||
// disable secure cookie for local testing
|
// disable secure cookie for local testing
|
||||||
.cookie_secure(false)
|
.cookie_secure(false)
|
||||||
|
// Set a ttl for the cookie if the identity should live longer than the user session
|
||||||
|
.session_lifecycle(
|
||||||
|
PersistentSession::default().session_ttl(expiration.try_into().unwrap()),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
let identity_mw = IdentityMiddleware::builder()
|
||||||
|
.visit_deadline(Some(expiration))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
// Install the identity framework first.
|
// Install the identity framework first.
|
||||||
.wrap(IdentityMiddleware::default())
|
.wrap(identity_mw)
|
||||||
// The identity system is built on top of sessions. You must install the session
|
// The identity system is built on top of sessions. You must install the session
|
||||||
// middleware to leverage `actix-identity`. The session middleware must be mounted
|
// middleware to leverage `actix-identity`. The session middleware must be mounted
|
||||||
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
||||||
|
@ -9,6 +9,9 @@ pub(crate) struct Configuration {
|
|||||||
pub(crate) on_logout: LogoutBehaviour,
|
pub(crate) on_logout: LogoutBehaviour,
|
||||||
pub(crate) login_deadline: Option<Duration>,
|
pub(crate) login_deadline: Option<Duration>,
|
||||||
pub(crate) visit_deadline: Option<Duration>,
|
pub(crate) visit_deadline: Option<Duration>,
|
||||||
|
pub(crate) id_key: &'static str,
|
||||||
|
pub(crate) last_visit_unix_timestamp_key: &'static str,
|
||||||
|
pub(crate) login_unix_timestamp_key: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Configuration {
|
impl Default for Configuration {
|
||||||
@ -17,6 +20,9 @@ impl Default for Configuration {
|
|||||||
on_logout: LogoutBehaviour::PurgeSession,
|
on_logout: LogoutBehaviour::PurgeSession,
|
||||||
login_deadline: None,
|
login_deadline: None,
|
||||||
visit_deadline: None,
|
visit_deadline: None,
|
||||||
|
id_key: "actix_identity.user_id",
|
||||||
|
last_visit_unix_timestamp_key: "actix_identity.last_visited_at",
|
||||||
|
login_unix_timestamp_key: "actix_identity.logged_in_at",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,6 +64,24 @@ impl IdentityMiddlewareBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a custom key to identify the user in the session.
|
||||||
|
pub fn id_key(mut self, key: &'static str) -> Self {
|
||||||
|
self.configuration.id_key = key;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a custom key to store the last visited unix timestamp.
|
||||||
|
pub fn last_visit_unix_timestamp_key(mut self, key: &'static str) -> Self {
|
||||||
|
self.configuration.last_visit_unix_timestamp_key = key;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a custom key to store the login unix timestamp.
|
||||||
|
pub fn login_unix_timestamp_key(mut self, key: &'static str) -> Self {
|
||||||
|
self.configuration.login_unix_timestamp_key = key;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session.
|
/// Determines how [`Identity::logout`](crate::Identity::logout) affects the current session.
|
||||||
///
|
///
|
||||||
/// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]).
|
/// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]).
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
use actix_session::{SessionGetError, SessionInsertError};
|
use actix_session::{SessionGetError, SessionInsertError};
|
||||||
use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError};
|
use actix_web::{cookie::time::error::ComponentRange, http::StatusCode, ResponseError};
|
||||||
use derive_more::{Display, Error, From};
|
use derive_more::derive::{Display, Error, From};
|
||||||
|
|
||||||
/// Error that can occur during login attempts.
|
/// Error that can occur during login attempts.
|
||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
pub struct LoginError(SessionInsertError);
|
pub struct LoginError(SessionInsertError);
|
||||||
|
|
||||||
impl ResponseError for LoginError {
|
impl ResponseError for LoginError {
|
||||||
@ -17,7 +17,7 @@ impl ResponseError for LoginError {
|
|||||||
|
|
||||||
/// Error encountered when working with a session that has expired.
|
/// Error encountered when working with a session that has expired.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[display(fmt = "The given session has expired and is no longer valid")]
|
#[display("The given session has expired and is no longer valid")]
|
||||||
pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
|
pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
|
||||||
|
|
||||||
/// The identity information has been lost.
|
/// The identity information has been lost.
|
||||||
@ -25,7 +25,7 @@ pub struct SessionExpiryError(#[error(not(source))] pub(crate) ComponentRange);
|
|||||||
/// Seeing this error in user code indicates a bug in actix-identity.
|
/// Seeing this error in user code indicates a bug in actix-identity.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[display(
|
#[display(
|
||||||
fmt = "The identity information in the current session has disappeared after having been \
|
"The identity information in the current session has disappeared after having been \
|
||||||
successfully validated. This is likely to be a bug."
|
successfully validated. This is likely to be a bug."
|
||||||
)]
|
)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@ -33,7 +33,7 @@ pub struct LostIdentityError;
|
|||||||
|
|
||||||
/// There is no identity information attached to the current session.
|
/// There is no identity information attached to the current session.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[display(fmt = "There is no identity information attached to the current session")]
|
#[display("There is no identity information attached to the current session")]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct MissingIdentityError;
|
pub struct MissingIdentityError;
|
||||||
|
|
||||||
@ -42,21 +42,21 @@ pub struct MissingIdentityError;
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum GetIdentityError {
|
pub enum GetIdentityError {
|
||||||
/// The session has expired.
|
/// The session has expired.
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
SessionExpiryError(SessionExpiryError),
|
SessionExpiryError(SessionExpiryError),
|
||||||
|
|
||||||
/// No identity is found in a session.
|
/// No identity is found in a session.
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
MissingIdentityError(MissingIdentityError),
|
MissingIdentityError(MissingIdentityError),
|
||||||
|
|
||||||
/// Failed to accessing the session store.
|
/// Failed to accessing the session store.
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
SessionGetError(SessionGetError),
|
SessionGetError(SessionGetError),
|
||||||
|
|
||||||
/// Identity info was lost after being validated.
|
/// Identity info was lost after being validated.
|
||||||
///
|
///
|
||||||
/// Seeing this error indicates a bug in actix-identity.
|
/// Seeing this error indicates a bug in actix-identity.
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
LostIdentityError(LostIdentityError),
|
LostIdentityError(LostIdentityError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +82,9 @@ pub(crate) struct IdentityInner {
|
|||||||
pub(crate) logout_behaviour: LogoutBehaviour,
|
pub(crate) logout_behaviour: LogoutBehaviour,
|
||||||
pub(crate) is_login_deadline_enabled: bool,
|
pub(crate) is_login_deadline_enabled: bool,
|
||||||
pub(crate) is_visit_deadline_enabled: bool,
|
pub(crate) is_visit_deadline_enabled: bool,
|
||||||
|
pub(crate) id_key: &'static str,
|
||||||
|
pub(crate) last_visit_unix_timestamp_key: &'static str,
|
||||||
|
pub(crate) login_unix_timestamp_key: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdentityInner {
|
impl IdentityInner {
|
||||||
@ -101,15 +104,11 @@ impl IdentityInner {
|
|||||||
/// Retrieve the user id attached to the current session.
|
/// Retrieve the user id attached to the current session.
|
||||||
fn get_identity(&self) -> Result<String, GetIdentityError> {
|
fn get_identity(&self) -> Result<String, GetIdentityError> {
|
||||||
self.session
|
self.session
|
||||||
.get::<String>(ID_KEY)?
|
.get::<String>(self.id_key)?
|
||||||
.ok_or_else(|| MissingIdentityError.into())
|
.ok_or_else(|| MissingIdentityError.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const ID_KEY: &str = "actix_identity.user_id";
|
|
||||||
pub(crate) const LAST_VISIT_UNIX_TIMESTAMP_KEY: &str = "actix_identity.last_visited_at";
|
|
||||||
pub(crate) const LOGIN_UNIX_TIMESTAMP_KEY: &str = "actix_identity.logged_in_at";
|
|
||||||
|
|
||||||
impl Identity {
|
impl Identity {
|
||||||
/// Return the user id associated to the current session.
|
/// Return the user id associated to the current session.
|
||||||
///
|
///
|
||||||
@ -130,7 +129,7 @@ impl Identity {
|
|||||||
pub fn id(&self) -> Result<String, GetIdentityError> {
|
pub fn id(&self) -> Result<String, GetIdentityError> {
|
||||||
self.0
|
self.0
|
||||||
.session
|
.session
|
||||||
.get(ID_KEY)?
|
.get(self.0.id_key)?
|
||||||
.ok_or_else(|| LostIdentityError.into())
|
.ok_or_else(|| LostIdentityError.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,13 +152,15 @@ impl Identity {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn login(ext: &Extensions, id: String) -> Result<Self, LoginError> {
|
pub fn login(ext: &Extensions, id: String) -> Result<Self, LoginError> {
|
||||||
let inner = IdentityInner::extract(ext);
|
let inner = IdentityInner::extract(ext);
|
||||||
inner.session.insert(ID_KEY, id)?;
|
inner.session.insert(inner.id_key, id)?;
|
||||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||||
if inner.is_login_deadline_enabled {
|
if inner.is_login_deadline_enabled {
|
||||||
inner.session.insert(LOGIN_UNIX_TIMESTAMP_KEY, now)?;
|
inner.session.insert(inner.login_unix_timestamp_key, now)?;
|
||||||
}
|
}
|
||||||
if inner.is_visit_deadline_enabled {
|
if inner.is_visit_deadline_enabled {
|
||||||
inner.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
|
inner
|
||||||
|
.session
|
||||||
|
.insert(inner.last_visit_unix_timestamp_key, now)?;
|
||||||
}
|
}
|
||||||
inner.session.renew();
|
inner.session.renew();
|
||||||
Ok(Self(inner))
|
Ok(Self(inner))
|
||||||
@ -191,12 +192,12 @@ impl Identity {
|
|||||||
self.0.session.purge();
|
self.0.session.purge();
|
||||||
}
|
}
|
||||||
LogoutBehaviour::DeleteIdentityKeys => {
|
LogoutBehaviour::DeleteIdentityKeys => {
|
||||||
self.0.session.remove(ID_KEY);
|
self.0.session.remove(self.0.id_key);
|
||||||
if self.0.is_login_deadline_enabled {
|
if self.0.is_login_deadline_enabled {
|
||||||
self.0.session.remove(LOGIN_UNIX_TIMESTAMP_KEY);
|
self.0.session.remove(self.0.login_unix_timestamp_key);
|
||||||
}
|
}
|
||||||
if self.0.is_visit_deadline_enabled {
|
if self.0.is_visit_deadline_enabled {
|
||||||
self.0.session.remove(LAST_VISIT_UNIX_TIMESTAMP_KEY);
|
self.0.session.remove(self.0.last_visit_unix_timestamp_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +213,7 @@ impl Identity {
|
|||||||
Ok(self
|
Ok(self
|
||||||
.0
|
.0
|
||||||
.session
|
.session
|
||||||
.get(LOGIN_UNIX_TIMESTAMP_KEY)?
|
.get(self.0.login_unix_timestamp_key)?
|
||||||
.map(OffsetDateTime::from_unix_timestamp)
|
.map(OffsetDateTime::from_unix_timestamp)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(SessionExpiryError)?)
|
.map_err(SessionExpiryError)?)
|
||||||
@ -222,7 +223,7 @@ impl Identity {
|
|||||||
Ok(self
|
Ok(self
|
||||||
.0
|
.0
|
||||||
.session
|
.session
|
||||||
.get(LAST_VISIT_UNIX_TIMESTAMP_KEY)?
|
.get(self.0.last_visit_unix_timestamp_key)?
|
||||||
.map(OffsetDateTime::from_unix_timestamp)
|
.map(OffsetDateTime::from_unix_timestamp)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(SessionExpiryError)?)
|
.map_err(SessionExpiryError)?)
|
||||||
@ -230,7 +231,9 @@ impl Identity {
|
|||||||
|
|
||||||
pub(crate) fn set_last_visited_at(&self) -> Result<(), LoginError> {
|
pub(crate) fn set_last_visited_at(&self) -> Result<(), LoginError> {
|
||||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||||
self.0.session.insert(LAST_VISIT_UNIX_TIMESTAMP_KEY, now)?;
|
self.0
|
||||||
|
.session
|
||||||
|
.insert(self.0.last_visit_unix_timestamp_key, now)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ impl IdentityExt for ServiceRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> IdentityExt for GuardContext<'a> {
|
impl IdentityExt for GuardContext<'_> {
|
||||||
fn get_identity(&self) -> Result<Identity, GetIdentityError> {
|
fn get_identity(&self) -> Result<Identity, GetIdentityError> {
|
||||||
Identity::extract(&self.req_data())
|
Identity::extract(&self.req_data())
|
||||||
}
|
}
|
||||||
|
@ -1,94 +1,104 @@
|
|||||||
//! Identity management for Actix Web.
|
/*!
|
||||||
//!
|
Identity management for Actix Web.
|
||||||
//! `actix-identity` can be used to track identity of a user across multiple requests. It is built
|
|
||||||
//! on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
|
`actix-identity` can be used to track identity of a user across multiple requests. It is built
|
||||||
//!
|
on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
|
||||||
//! # Getting started
|
|
||||||
//! To start using identity management in your Actix Web application you must register
|
# Getting started
|
||||||
//! [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
|
To start using identity management in your Actix Web application you must register
|
||||||
//!
|
[`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
|
||||||
//! ```no_run
|
|
||||||
//! # use actix_web::web;
|
```no_run
|
||||||
//! use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
|
# use actix_web::web;
|
||||||
//! use actix_identity::IdentityMiddleware;
|
use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
|
||||||
//! use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
use actix_identity::IdentityMiddleware;
|
||||||
//!
|
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
||||||
//! #[actix_web::main]
|
|
||||||
//! async fn main() {
|
#[actix_web::main]
|
||||||
//! let secret_key = Key::generate();
|
async fn main() {
|
||||||
//! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
|
// When using `Key::generate()` it is important to initialize outside of the
|
||||||
//! .await
|
// `HttpServer::new` closure. When deployed the secret key should be read from a
|
||||||
//! .unwrap();
|
// configuration file or environment variables.
|
||||||
//!
|
let secret_key = Key::generate();
|
||||||
//! HttpServer::new(move || {
|
|
||||||
//! App::new()
|
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||||
//! // Install the identity framework first.
|
.await
|
||||||
//! .wrap(IdentityMiddleware::default())
|
.unwrap();
|
||||||
//! // The identity system is built on top of sessions. You must install the session
|
|
||||||
//! // middleware to leverage `actix-identity`. The session middleware must be mounted
|
HttpServer::new(move || {
|
||||||
//! // AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
App::new()
|
||||||
//! // order of registration when it receives an incoming request.
|
// Install the identity framework first.
|
||||||
//! .wrap(SessionMiddleware::new(
|
.wrap(IdentityMiddleware::default())
|
||||||
//! redis_store.clone(),
|
// The identity system is built on top of sessions. You must install the session
|
||||||
//! secret_key.clone()
|
// middleware to leverage `actix-identity`. The session middleware must be mounted
|
||||||
//! ))
|
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
|
||||||
//! // Your request handlers [...]
|
// order of registration when it receives an incoming request.
|
||||||
//! # .default_service(web::to(|| HttpResponse::Ok()))
|
.wrap(SessionMiddleware::new(
|
||||||
//! })
|
redis_store.clone(),
|
||||||
//! # ;
|
secret_key.clone(),
|
||||||
//! }
|
))
|
||||||
//! ```
|
// Your request handlers [...]
|
||||||
//!
|
# .default_service(web::to(|| HttpResponse::Ok()))
|
||||||
//! User identities can be created, accessed and destroyed using the [`Identity`] extractor in your
|
})
|
||||||
//! request handlers:
|
# ;
|
||||||
//!
|
}
|
||||||
//! ```no_run
|
```
|
||||||
//! use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
|
|
||||||
//! use actix_identity::Identity;
|
User identities can be created, accessed and destroyed using the [`Identity`] extractor in your
|
||||||
//! use actix_session::storage::RedisSessionStore;
|
request handlers:
|
||||||
//!
|
|
||||||
//! #[get("/")]
|
```no_run
|
||||||
//! async fn index(user: Option<Identity>) -> impl Responder {
|
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
|
||||||
//! if let Some(user) = user {
|
use actix_identity::Identity;
|
||||||
//! format!("Welcome! {}", user.id().unwrap())
|
use actix_session::storage::RedisSessionStore;
|
||||||
//! } else {
|
|
||||||
//! "Welcome Anonymous!".to_owned()
|
#[get("/")]
|
||||||
//! }
|
async fn index(user: Option<Identity>) -> impl Responder {
|
||||||
//! }
|
if let Some(user) = user {
|
||||||
//!
|
format!("Welcome! {}", user.id().unwrap())
|
||||||
//! #[post("/login")]
|
} else {
|
||||||
//! async fn login(request: HttpRequest) -> impl Responder {
|
"Welcome Anonymous!".to_owned()
|
||||||
//! // Some kind of authentication should happen here
|
}
|
||||||
//! // e.g. password-based, biometric, etc.
|
}
|
||||||
//! // [...]
|
|
||||||
//!
|
#[post("/login")]
|
||||||
//! // attach a verified user identity to the active session
|
async fn login(request: HttpRequest) -> impl Responder {
|
||||||
//! Identity::login(&request.extensions(), "User1".into()).unwrap();
|
// Some kind of authentication should happen here
|
||||||
//!
|
// e.g. password-based, biometric, etc.
|
||||||
//! HttpResponse::Ok()
|
// [...]
|
||||||
//! }
|
|
||||||
//!
|
// attach a verified user identity to the active session
|
||||||
//! #[post("/logout")]
|
Identity::login(&request.extensions(), "User1".into()).unwrap();
|
||||||
//! async fn logout(user: Identity) -> impl Responder {
|
|
||||||
//! user.logout();
|
HttpResponse::Ok()
|
||||||
//! HttpResponse::Ok()
|
}
|
||||||
//! }
|
|
||||||
//! ```
|
#[post("/logout")]
|
||||||
//!
|
async fn logout(user: Option<Identity>) -> impl Responder {
|
||||||
//! # Advanced configuration
|
if let Some(user) = user {
|
||||||
//! By default, `actix-identity` does not automatically log out users. You can change this behaviour
|
user.logout();
|
||||||
//! by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
|
}
|
||||||
//!
|
HttpResponse::Ok()
|
||||||
//! In particular, you can automatically log out users who:
|
}
|
||||||
//! - have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`];
|
```
|
||||||
//! - logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
|
|
||||||
//!
|
# Advanced configuration
|
||||||
//! [`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
|
By default, `actix-identity` does not automatically log out users. You can change this behaviour
|
||||||
//! [`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
|
by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
|
||||||
|
|
||||||
|
In particular, you can automatically log out users who:
|
||||||
|
- have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]);
|
||||||
|
- logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
|
||||||
|
|
||||||
|
[`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
|
||||||
|
[`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
|
||||||
|
*/
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style, missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![warn(future_incompatible)]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
@ -114,6 +114,9 @@ where
|
|||||||
logout_behaviour: configuration.on_logout.clone(),
|
logout_behaviour: configuration.on_logout.clone(),
|
||||||
is_login_deadline_enabled: configuration.login_deadline.is_some(),
|
is_login_deadline_enabled: configuration.login_deadline.is_some(),
|
||||||
is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
|
is_visit_deadline_enabled: configuration.visit_deadline.is_some(),
|
||||||
|
id_key: configuration.id_key,
|
||||||
|
last_visit_unix_timestamp_key: configuration.last_visit_unix_timestamp_key,
|
||||||
|
login_unix_timestamp_key: configuration.login_unix_timestamp_key,
|
||||||
};
|
};
|
||||||
req.extensions_mut().insert(identity_inner);
|
req.extensions_mut().insert(identity_inner);
|
||||||
enforce_policies(&req, &configuration);
|
enforce_policies(&req, &configuration);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
|
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
|
||||||
use actix_web::http::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
use crate::{fixtures::user_id, test_app::TestApp};
|
use crate::{fixtures::user_id, test_app::TestApp};
|
||||||
|
|
||||||
@ -28,6 +28,33 @@ async fn login_works() {
|
|||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn custom_keys_work_as_expected() {
|
||||||
|
let custom_id_key = "custom.user_id";
|
||||||
|
let custom_last_visited_key = "custom.last_visited_at";
|
||||||
|
let custom_logged_in_key = "custom.logged_in_at";
|
||||||
|
|
||||||
|
let app = TestApp::spawn_with_config(
|
||||||
|
IdentityMiddleware::builder()
|
||||||
|
.id_key(custom_id_key)
|
||||||
|
.last_visit_unix_timestamp_key(custom_last_visited_key)
|
||||||
|
.login_unix_timestamp_key(custom_logged_in_key),
|
||||||
|
);
|
||||||
|
let user_id = user_id();
|
||||||
|
|
||||||
|
let body = app.post_login(user_id.clone()).await;
|
||||||
|
assert_eq!(body.user_id, Some(user_id.clone()));
|
||||||
|
|
||||||
|
let response = app.get_identity_required().await;
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let response = app.post_logout().await;
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let response = app.get_identity_required().await;
|
||||||
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn logging_in_again_replaces_the_current_identity() {
|
async fn logging_in_again_replaces_the_current_identity() {
|
||||||
let app = TestApp::spawn();
|
let app = TestApp::spawn();
|
||||||
|
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Update `redis` dependency to `0.29`.
|
||||||
|
- Update `actix-session` dependency to `0.9`.
|
||||||
|
|
||||||
|
## 0.5.1
|
||||||
|
|
||||||
|
- No significant changes since `0.5.0`.
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
- Update `redis` dependency to `0.23`.
|
- Update `redis` dependency to `0.23`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-limitation"
|
name = "actix-limitation"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
authors = [
|
authors = [
|
||||||
"0xmad <0xmad@users.noreply.github.com>",
|
"0xmad <0xmad@users.noreply.github.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@ -8,7 +8,7 @@ authors = [
|
|||||||
description = "Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web"
|
description = "Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web"
|
||||||
keywords = ["actix-web", "rate-api", "rate-limit", "limitation"]
|
keywords = ["actix-web", "rate-api", "rate-limit", "limitation"]
|
||||||
categories = ["asynchronous", "web-programming"]
|
categories = ["asynchronous", "web-programming"]
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
repository = "https://github.com/actix/actix-extras"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -23,18 +23,21 @@ session = ["actix-session"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
|
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
derive_more = "0.99.7"
|
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
redis = { version = "0.23", default-features = false, features = ["tokio-comp"] }
|
redis = { version = "0.29", default-features = false, features = ["tokio-comp"] }
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
|
|
||||||
# session
|
# session
|
||||||
actix-session = { version = "0.8", optional = true }
|
actix-session = { version = "0.10", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -3,10 +3,14 @@
|
|||||||
> Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web.
|
> Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web.
|
||||||
> Originally based on <https://github.com/fnichol/limitation>.
|
> Originally based on <https://github.com/fnichol/limitation>.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-limitation)
|
[](https://crates.io/crates/actix-limitation)
|
||||||
[](https://docs.rs/actix-limitation/0.5.0)
|
[](https://docs.rs/actix-limitation/0.5.1)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-limitation/0.5.0)
|
[](https://deps.rs/crate/actix-limitation/0.5.1)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use derive_more::{Display, Error, From};
|
use derive_more::derive::{Display, Error, From};
|
||||||
|
|
||||||
use crate::status::Status;
|
use crate::status::Status;
|
||||||
|
|
||||||
@ -6,20 +6,20 @@ use crate::status::Status;
|
|||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Redis client failed to connect or run a query.
|
/// Redis client failed to connect or run a query.
|
||||||
#[display(fmt = "Redis client failed to connect or run a query")]
|
#[display("Redis client failed to connect or run a query")]
|
||||||
Client(redis::RedisError),
|
Client(redis::RedisError),
|
||||||
|
|
||||||
/// Limit is exceeded for a key.
|
/// Limit is exceeded for a key.
|
||||||
#[display(fmt = "Limit is exceeded for a key")]
|
#[display("Limit is exceeded for a key")]
|
||||||
#[from(ignore)]
|
#[from(ignore)]
|
||||||
LimitExceeded(#[error(not(source))] Status),
|
LimitExceeded(#[error(not(source))] Status),
|
||||||
|
|
||||||
/// Time conversion failed.
|
/// Time conversion failed.
|
||||||
#[display(fmt = "Time conversion failed")]
|
#[display("Time conversion failed")]
|
||||||
Time(time::error::ComponentRange),
|
Time(time::error::ComponentRange),
|
||||||
|
|
||||||
/// Generic error.
|
/// Generic error.
|
||||||
#[display(fmt = "Generic error")]
|
#[display("Generic error")]
|
||||||
#[from(ignore)]
|
#[from(ignore)]
|
||||||
Other(#[error(not(source))] String),
|
Other(#[error(not(source))] String),
|
||||||
}
|
}
|
||||||
|
@ -45,10 +45,10 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![warn(missing_docs, missing_debug_implementations)]
|
||||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
use std::{borrow::Cow, fmt, sync::Arc, time::Duration};
|
use std::{borrow::Cow, fmt, sync::Arc, time::Duration};
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ impl Limiter {
|
|||||||
let key = key.into();
|
let key = key.into();
|
||||||
let expires = self.period.as_secs();
|
let expires = self.period.as_secs();
|
||||||
|
|
||||||
let mut connection = self.client.get_tokio_connection().await?;
|
let mut connection = self.client.get_multiplexed_tokio_connection().await?;
|
||||||
|
|
||||||
// The seed of this approach is outlined Atul R in a blog post about rate limiting using
|
// The seed of this approach is outlined Atul R in a blog post about rate limiting using
|
||||||
// NodeJS and Redis. For more details, see https://blog.atulr.com/rate-limiter
|
// NodeJS and Redis. For more details, see https://blog.atulr.com/rate-limiter
|
||||||
|
@ -53,7 +53,7 @@ where
|
|||||||
forward_ready!(service);
|
forward_ready!(service);
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
// A mis-configuration of the Actix App will result in a **runtime** failure, so the expect
|
// A misconfiguration of the Actix App will result in a **runtime** failure, so the expect
|
||||||
// method description is important context for the developer.
|
// method description is important context for the developer.
|
||||||
let limiter = req
|
let limiter = req
|
||||||
.app_data::<web::Data<Limiter>>()
|
.app_data::<web::Data<Limiter>>()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{convert::TryInto, ops::Add, time::Duration};
|
use std::{ops::Add, time::Duration};
|
||||||
|
|
||||||
use chrono::SubsecRound as _;
|
use chrono::SubsecRound as _;
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ impl Status {
|
|||||||
/// Constructs status limit status from parts.
|
/// Constructs status limit status from parts.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
|
pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
|
||||||
let remaining = if count >= limit { 0 } else { limit - count };
|
let remaining = limit.saturating_sub(count);
|
||||||
|
|
||||||
Status {
|
Status {
|
||||||
limit,
|
limit,
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
- Updated `prost` dependency to `0.13`.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.10.0
|
## 0.10.0
|
||||||
|
|
||||||
- Updated `prost` dependency to `0.12`.
|
- Updated `prost` dependency to `0.12`.
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-protobuf"
|
name = "actix-protobuf"
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
authors = [
|
authors = [
|
||||||
"kingxsp <jin.hb.zh@outlook.com>",
|
"kingxsp <jin.hb.zh@outlook.com>",
|
||||||
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
||||||
]
|
]
|
||||||
description = "Protobuf payload extractor for Actix Web"
|
description = "Protobuf payload extractor for Actix Web"
|
||||||
keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
|
keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
|
||||||
homepage = "https://actix.rs"
|
repository.workspace = true
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
homepage.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -19,10 +19,13 @@ all-features = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
derive_more = "0.99.7"
|
derive_more = { version = "2", features = ["display"] }
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
prost = { version = "0.12", default-features = false }
|
prost = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4", default-features = false, features = ["macros"] }
|
actix-web = { version = "4", default-features = false, features = ["macros"] }
|
||||||
prost = { version = "0.12", default-features = false, features = ["prost-derive"] }
|
prost = { version = "0.13", default-features = false, features = ["prost-derive"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
> Protobuf payload extractor for Actix Web.
|
> Protobuf payload extractor for Actix Web.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-protobuf)
|
[](https://crates.io/crates/actix-protobuf)
|
||||||
[](https://docs.rs/actix-protobuf/0.10.0)
|
[](https://docs.rs/actix-protobuf/0.11.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-protobuf/0.10.0)
|
[](https://deps.rs/crate/actix-protobuf/0.11.0)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
//! Protobuf payload extractor for Actix Web.
|
//! Protobuf payload extractor for Actix Web.
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![warn(future_incompatible)]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
@ -21,7 +22,7 @@ use actix_web::{
|
|||||||
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder,
|
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder,
|
||||||
ResponseError,
|
ResponseError,
|
||||||
};
|
};
|
||||||
use derive_more::Display;
|
use derive_more::derive::Display;
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
future::{FutureExt as _, LocalBoxFuture},
|
future::{FutureExt as _, LocalBoxFuture},
|
||||||
stream::StreamExt as _,
|
stream::StreamExt as _,
|
||||||
@ -31,26 +32,28 @@ use prost::{DecodeError as ProtoBufDecodeError, EncodeError as ProtoBufEncodeErr
|
|||||||
#[derive(Debug, Display)]
|
#[derive(Debug, Display)]
|
||||||
pub enum ProtoBufPayloadError {
|
pub enum ProtoBufPayloadError {
|
||||||
/// Payload size is bigger than 256k
|
/// Payload size is bigger than 256k
|
||||||
#[display(fmt = "Payload size is bigger than 256k")]
|
#[display("Payload size is bigger than 256k")]
|
||||||
Overflow,
|
Overflow,
|
||||||
|
|
||||||
/// Content type error
|
/// Content type error
|
||||||
#[display(fmt = "Content type error")]
|
#[display("Content type error")]
|
||||||
ContentType,
|
ContentType,
|
||||||
|
|
||||||
/// Serialize error
|
/// Serialize error
|
||||||
#[display(fmt = "ProtoBuf serialize error: {_0}")]
|
#[display("ProtoBuf serialize error: {_0}")]
|
||||||
Serialize(ProtoBufEncodeError),
|
Serialize(ProtoBufEncodeError),
|
||||||
|
|
||||||
/// Deserialize error
|
/// Deserialize error
|
||||||
#[display(fmt = "ProtoBuf deserialize error: {_0}")]
|
#[display("ProtoBuf deserialize error: {_0}")]
|
||||||
Deserialize(ProtoBufDecodeError),
|
Deserialize(ProtoBufDecodeError),
|
||||||
|
|
||||||
/// Payload error
|
/// Payload error
|
||||||
#[display(fmt = "Error that occur during reading payload: {_0}")]
|
#[display("Error that occur during reading payload: {_0}")]
|
||||||
Payload(PayloadError),
|
Payload(PayloadError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: impl error for ProtoBufPayloadError
|
||||||
|
|
||||||
impl ResponseError for ProtoBufPayloadError {
|
impl ResponseError for ProtoBufPayloadError {
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn error_response(&self) -> HttpResponse {
|
||||||
match *self {
|
match *self {
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
# Changes
|
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
## 0.13.0
|
|
||||||
|
|
||||||
- Update `redis-async` dependency to `0.16`.
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.68.
|
|
||||||
|
|
||||||
## 0.12.0
|
|
||||||
|
|
||||||
- Update `actix` dependency to `0.13`.
|
|
||||||
- Update `redis-async` dependency to `0.13`.
|
|
||||||
- Update `tokio-util` dependency to `0.7`.
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
|
||||||
|
|
||||||
## 0.11.0
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- `RedisSession` has been removed. Check out `RedisActorSessionStore` in `actix-session` for a session store backed by Redis using `actix-redis`. [#212]
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Update `redis-async` dependency to `0.12`. [#212]
|
|
||||||
|
|
||||||
[#212]: https://github.com/actix/actix-extras/pull/212
|
|
||||||
|
|
||||||
## 0.10.0
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to `4`.
|
|
||||||
|
|
||||||
## 0.10.0-beta.6
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to `4.0.0-rc.1`.
|
|
||||||
|
|
||||||
## 0.10.0-beta.5
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to `4.0.0.beta-18`. [#218]
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
|
||||||
|
|
||||||
[#218]: https://github.com/actix/actix-extras/pull/218
|
|
||||||
|
|
||||||
## 0.10.0-beta.4
|
|
||||||
|
|
||||||
- A session will be created in Redis if and only if there is some data inside the session state. This reduces the performance impact of `RedisSession` on routes that do not leverage sessions. [#207]
|
|
||||||
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
|
|
||||||
|
|
||||||
[#207]: https://github.com/actix/actix-extras/pull/207
|
|
||||||
[#209]: https://github.com/actix/actix-extras/pull/209
|
|
||||||
|
|
||||||
## 0.10.0-beta.3
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to v4.0.0-beta.10. [#203]
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
|
||||||
|
|
||||||
[#203]: https://github.com/actix/actix-extras/pull/203
|
|
||||||
|
|
||||||
## 0.10.0-beta.2
|
|
||||||
|
|
||||||
- No notable changes.
|
|
||||||
|
|
||||||
## 0.10.0-beta.1
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to 4.0.0 beta.
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.46.0.
|
|
||||||
|
|
||||||
## 0.9.2
|
|
||||||
|
|
||||||
- Implement `std::error::Error` for `Error` [#135]
|
|
||||||
- Allow the removal of `Max-Age` for session-only cookies. [#161]
|
|
||||||
|
|
||||||
[#135]: https://github.com/actix/actix-extras/pull/135
|
|
||||||
[#161]: https://github.com/actix/actix-extras/pull/161
|
|
||||||
|
|
||||||
## 0.9.1
|
|
||||||
|
|
||||||
- Enforce minimum redis-async version of 0.6.3 to workaround breaking patch change.
|
|
||||||
|
|
||||||
## 0.9.0
|
|
||||||
|
|
||||||
- Update `actix-web` dependency to 3.0.0.
|
|
||||||
- Minimize `futures` dependency.
|
|
||||||
|
|
||||||
## 0.9.0-alpha.2
|
|
||||||
|
|
||||||
- Add `cookie_http_only` functionality to RedisSession builder, setting this
|
|
||||||
to false allows JavaScript to access cookies. Defaults to true.
|
|
||||||
- Change type of parameter of ttl method to u32.
|
|
||||||
- Update `actix` to 0.10.0-alpha.3
|
|
||||||
- Update `tokio-util` to 0.3
|
|
||||||
- Minimum supported Rust version(MSRV) is now 1.40.0.
|
|
||||||
|
|
||||||
## 0.9.0-alpha.1
|
|
||||||
|
|
||||||
- Update `actix` to 0.10.0-alpha.2
|
|
||||||
- Update `actix-session` to 0.4.0-alpha.1
|
|
||||||
- Update `actix-web` to 3.0.0-alpha.1
|
|
||||||
- Update `time` to 0.2.9
|
|
||||||
|
|
||||||
## 0.8.1
|
|
||||||
|
|
||||||
- Move `env_logger` dependency to dev-dependencies and update to 0.7
|
|
||||||
- Update `actix_web` to 2.0.0 from 2.0.0-rc
|
|
||||||
- Move repository to actix-extras
|
|
||||||
|
|
||||||
## 0.8.0 - 2019-12-20
|
|
||||||
|
|
||||||
- Release
|
|
||||||
|
|
||||||
## 0.8.0-alpha.1 - 2019-12-16
|
|
||||||
|
|
||||||
- Migrate to actix 0.9
|
|
||||||
|
|
||||||
## 0.7.0 - 2019-09-25
|
|
||||||
|
|
||||||
- added cache_keygen functionality to RedisSession builder, enabling support for
|
|
||||||
customizable cache key creation
|
|
||||||
|
|
||||||
## 0.6.1 - 2019-07-19
|
|
||||||
|
|
||||||
- remove ClonableService usage
|
|
||||||
- added comprehensive tests for session workflow
|
|
||||||
|
|
||||||
## 0.6.0 - 2019-07-08
|
|
||||||
|
|
||||||
- actix-web 1.0.0 compatibility
|
|
||||||
- Upgraded logic that evaluates session state, including new SessionStatus field,
|
|
||||||
and introduced `session.renew()` and `session.purge()` functionality.
|
|
||||||
Use `renew()` to cycle the session key at successful login. `renew()` keeps a
|
|
||||||
session's state while replacing the old cookie and session key with new ones.
|
|
||||||
Use `purge()` at logout to invalidate the session cookie and remove the
|
|
||||||
session's redis cache entry.
|
|
||||||
|
|
||||||
## 0.5.1 - 2018-08-02
|
|
||||||
|
|
||||||
- Use cookie 0.11
|
|
||||||
|
|
||||||
## 0.5.0 - 2018-07-21
|
|
||||||
|
|
||||||
- Session cookie configuration
|
|
||||||
- Actix/Actix-web 0.7 compatibility
|
|
||||||
|
|
||||||
## 0.4.0 - 2018-05-08
|
|
||||||
|
|
||||||
- Actix web 0.6 compatibility
|
|
||||||
|
|
||||||
## 0.3.0 - 2018-04-10
|
|
||||||
|
|
||||||
- Actix web 0.5 compatibility
|
|
||||||
|
|
||||||
## 0.2.0 - 2018-02-28
|
|
||||||
|
|
||||||
- Use resolver actor from actix
|
|
||||||
- Use actix web 0.5
|
|
||||||
|
|
||||||
## 0.1.0 - 2018-01-23
|
|
||||||
|
|
||||||
- First release
|
|
@ -1,48 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "actix-redis"
|
|
||||||
version = "0.13.0"
|
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
|
||||||
description = "Actor-based Redis client"
|
|
||||||
keywords = ["actix", "redis", "async"]
|
|
||||||
homepage = "https://actix.rs"
|
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
|
||||||
categories = ["network-programming", "asynchronous"]
|
|
||||||
license.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
rust-version.workspace = true
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
all-features = true
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "actix_redis"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["web"]
|
|
||||||
|
|
||||||
# actix-web integration
|
|
||||||
web = ["actix-web"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
actix = { version = "0.13", default-features = false }
|
|
||||||
actix-rt = { version = "2.1", default-features = false }
|
|
||||||
actix-service = "2"
|
|
||||||
actix-tls = { version = "3", default-features = false, features = ["connect"] }
|
|
||||||
|
|
||||||
log = "0.4.6"
|
|
||||||
backoff = "0.4.0"
|
|
||||||
derive_more = "0.99.7"
|
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
|
||||||
redis-async = "0.16"
|
|
||||||
time = "0.3"
|
|
||||||
tokio = { version = "1.18.4", features = ["sync"] }
|
|
||||||
tokio-util = "0.7"
|
|
||||||
actix-web = { version = "4", default-features = false, optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
actix-test = "0.1.0-beta.12"
|
|
||||||
actix-web = { version = "4", default-features = false, features = ["macros"] }
|
|
||||||
env_logger = "0.10"
|
|
||||||
serde = { version = "1.0.101", features = ["derive"] }
|
|
@ -1,14 +0,0 @@
|
|||||||
# actix-redis
|
|
||||||
|
|
||||||
> Actor-based Redis client.
|
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-redis)
|
|
||||||
[](https://docs.rs/actix-redis/0.13.0)
|
|
||||||

|
|
||||||
[](https://deps.rs/crate/actix-redis/0.13.0)
|
|
||||||
|
|
||||||
## Documentation & Resources
|
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-redis)
|
|
||||||
- [Example Project](https://github.com/actix/examples/tree/master/auth/redis-session)
|
|
||||||
- Minimum Supported Rust Version (MSRV): 1.57
|
|
@ -1,29 +0,0 @@
|
|||||||
//! Redis integration for `actix`.
|
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
|
||||||
#![warn(future_incompatible)]
|
|
||||||
|
|
||||||
use derive_more::{Display, Error, From};
|
|
||||||
pub use redis_async::{error::Error as RespError, resp::RespValue, resp_array};
|
|
||||||
|
|
||||||
mod redis;
|
|
||||||
pub use self::redis::{Command, RedisActor};
|
|
||||||
|
|
||||||
/// General purpose `actix-redis` error.
|
|
||||||
#[derive(Debug, Display, Error, From)]
|
|
||||||
pub enum Error {
|
|
||||||
#[display(fmt = "Redis error: {_0}")]
|
|
||||||
Redis(redis_async::error::Error),
|
|
||||||
|
|
||||||
/// Receiving message during reconnecting.
|
|
||||||
#[display(fmt = "Redis: Not connected")]
|
|
||||||
NotConnected,
|
|
||||||
|
|
||||||
/// Cancel all waiters when connection is dropped.
|
|
||||||
#[display(fmt = "Redis: Disconnected")]
|
|
||||||
Disconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
impl actix_web::ResponseError for Error {}
|
|
@ -1,143 +0,0 @@
|
|||||||
use std::{collections::VecDeque, io};
|
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_rt::net::TcpStream;
|
|
||||||
use actix_service::boxed::{self, BoxService};
|
|
||||||
use actix_tls::connect::{ConnectError, ConnectInfo, Connection, ConnectorService};
|
|
||||||
use backoff::{backoff::Backoff, ExponentialBackoff};
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use redis_async::{
|
|
||||||
error::Error as RespError,
|
|
||||||
resp::{RespCodec, RespValue},
|
|
||||||
};
|
|
||||||
use tokio::{
|
|
||||||
io::{split, WriteHalf},
|
|
||||||
sync::oneshot,
|
|
||||||
};
|
|
||||||
use tokio_util::codec::FramedRead;
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
|
|
||||||
/// Command for sending data to Redis.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Command(pub RespValue);
|
|
||||||
|
|
||||||
impl Message for Command {
|
|
||||||
type Result = Result<RespValue, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redis communication actor.
|
|
||||||
pub struct RedisActor {
|
|
||||||
addr: String,
|
|
||||||
connector: BoxService<ConnectInfo<String>, Connection<String, TcpStream>, ConnectError>,
|
|
||||||
backoff: ExponentialBackoff,
|
|
||||||
cell: Option<actix::io::FramedWrite<RespValue, WriteHalf<TcpStream>, RespCodec>>,
|
|
||||||
queue: VecDeque<oneshot::Sender<Result<RespValue, Error>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RedisActor {
|
|
||||||
/// Start new `Supervisor` with `RedisActor`.
|
|
||||||
pub fn start<S: Into<String>>(addr: S) -> Addr<RedisActor> {
|
|
||||||
let addr = addr.into();
|
|
||||||
|
|
||||||
let backoff = ExponentialBackoff {
|
|
||||||
max_elapsed_time: None,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Supervisor::start(|_| RedisActor {
|
|
||||||
addr,
|
|
||||||
connector: boxed::service(ConnectorService::default()),
|
|
||||||
cell: None,
|
|
||||||
backoff,
|
|
||||||
queue: VecDeque::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for RedisActor {
|
|
||||||
type Context = Context<Self>;
|
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Context<Self>) {
|
|
||||||
let req = ConnectInfo::new(self.addr.to_owned());
|
|
||||||
self.connector
|
|
||||||
.call(req)
|
|
||||||
.into_actor(self)
|
|
||||||
.map(|res, act, ctx| match res {
|
|
||||||
Ok(conn) => {
|
|
||||||
let stream = conn.into_parts().0;
|
|
||||||
info!("Connected to redis server: {}", act.addr);
|
|
||||||
|
|
||||||
let (r, w) = split(stream);
|
|
||||||
|
|
||||||
// configure write side of the connection
|
|
||||||
let framed = actix::io::FramedWrite::new(w, RespCodec, ctx);
|
|
||||||
act.cell = Some(framed);
|
|
||||||
|
|
||||||
// read side of the connection
|
|
||||||
ctx.add_stream(FramedRead::new(r, RespCodec));
|
|
||||||
|
|
||||||
act.backoff.reset();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("Can not connect to redis server: {}", err);
|
|
||||||
// re-connect with backoff time.
|
|
||||||
// we stop current context, supervisor will restart it.
|
|
||||||
if let Some(timeout) = act.backoff.next_backoff() {
|
|
||||||
ctx.run_later(timeout, |_, ctx| ctx.stop());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Supervised for RedisActor {
|
|
||||||
fn restarting(&mut self, _: &mut Self::Context) {
|
|
||||||
self.cell.take();
|
|
||||||
for tx in self.queue.drain(..) {
|
|
||||||
let _ = tx.send(Err(Error::Disconnected));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl actix::io::WriteHandler<io::Error> for RedisActor {
|
|
||||||
fn error(&mut self, err: io::Error, _: &mut Self::Context) -> Running {
|
|
||||||
warn!("Redis connection dropped: {} error: {}", self.addr, err);
|
|
||||||
Running::Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StreamHandler<Result<RespValue, RespError>> for RedisActor {
|
|
||||||
fn handle(&mut self, msg: Result<RespValue, RespError>, ctx: &mut Self::Context) {
|
|
||||||
match msg {
|
|
||||||
Err(e) => {
|
|
||||||
if let Some(tx) = self.queue.pop_front() {
|
|
||||||
let _ = tx.send(Err(e.into()));
|
|
||||||
}
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
Ok(val) => {
|
|
||||||
if let Some(tx) = self.queue.pop_front() {
|
|
||||||
let _ = tx.send(Ok(val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler<Command> for RedisActor {
|
|
||||||
type Result = ResponseFuture<Result<RespValue, Error>>;
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: Command, _: &mut Self::Context) -> Self::Result {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
if let Some(ref mut cell) = self.cell {
|
|
||||||
self.queue.push_back(tx);
|
|
||||||
cell.write(msg.0);
|
|
||||||
} else {
|
|
||||||
let _ = tx.send(Err(Error::NotConnected));
|
|
||||||
}
|
|
||||||
|
|
||||||
Box::pin(async move { rx.await.map_err(|_| Error::Disconnected)? })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
#[macro_use]
|
|
||||||
extern crate redis_async;
|
|
||||||
|
|
||||||
use actix_redis::{Command, Error, RedisActor, RespValue};
|
|
||||||
|
|
||||||
#[actix_web::test]
|
|
||||||
async fn test_error_connect() {
|
|
||||||
let addr = RedisActor::start("localhost:54000");
|
|
||||||
let _addr2 = addr.clone();
|
|
||||||
|
|
||||||
let res = addr.send(Command(resp_array!["GET", "test"])).await;
|
|
||||||
match res {
|
|
||||||
Ok(Err(Error::NotConnected)) => (),
|
|
||||||
_ => panic!("Should not happen {:?}", res),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::test]
|
|
||||||
async fn test_redis() {
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let addr = RedisActor::start("127.0.0.1:6379");
|
|
||||||
let res = addr
|
|
||||||
.send(Command(resp_array!["SET", "test", "value"]))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(Ok(resp)) => {
|
|
||||||
assert_eq!(resp, RespValue::SimpleString("OK".to_owned()));
|
|
||||||
|
|
||||||
let res = addr.send(Command(resp_array!["GET", "test"])).await;
|
|
||||||
match res {
|
|
||||||
Ok(Ok(resp)) => {
|
|
||||||
println!("RESP: {resp:?}");
|
|
||||||
assert_eq!(resp, RespValue::BulkString((&b"value"[..]).into()));
|
|
||||||
}
|
|
||||||
_ => panic!("Should not happen {:?}", res),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("Should not happen {:?}", res),
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add `Session::contains_key` method.
|
||||||
|
- Add `Session::update[_or]()` methods.
|
||||||
|
- Update `redis` dependency to `0.29`.
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
|
||||||
|
- Expose `storage::generate_session_key()` without needing to enable a crate feature.
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
- Add `redis-session-rustls` crate feature that enables `rustls`-secured Redis sessions.
|
||||||
|
- Add `redis-pool` crate feature (off-by-default) which enables `RedisSessionStore::{new, builder}_pooled()` constructors.
|
||||||
|
- Rename `redis-rs-session` crate feature to `redis-session`.
|
||||||
|
- Rename `redis-rs-tls-session` crate feature to `redis-session-native-tls`.
|
||||||
|
- Remove `redis-actor-session` crate feature (and, therefore, the `actix-redis` based storage backend).
|
||||||
|
- Expose `storage::generate_session_key()`.
|
||||||
|
- Update `redis` dependency to `0.26`.
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- Remove use of `async-trait` on `SessionStore` trait.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.8.0
|
## 0.8.0
|
||||||
|
|
||||||
- Set secure attribute when adding a session removal cookie.
|
- Set secure attribute when adding a session removal cookie.
|
||||||
@ -172,10 +195,7 @@
|
|||||||
|
|
||||||
## 0.2.0 - 2019-07-08
|
## 0.2.0 - 2019-07-08
|
||||||
|
|
||||||
- Enhanced `actix-session` to facilitate state changes. Use `Session.renew()`
|
- Enhanced `actix-session` to facilitate state changes. Use `Session.renew()` at successful login to cycle a session (new key/cookie but keeps state). Use `Session.purge()` at logout to invalid a session cookie (and remove from redis cache, if applicable).
|
||||||
at successful login to cycle a session (new key/cookie but keeps state).
|
|
||||||
Use `Session.purge()` at logout to invalid a session cookie (and remove
|
|
||||||
from redis cache, if applicable).
|
|
||||||
|
|
||||||
## 0.1.1 - 2019-06-03
|
## 0.1.1 - 2019-06-03
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-session"
|
name = "actix-session"
|
||||||
version = "0.8.0"
|
version = "0.10.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Luca Palmieri <rust@lpalmieri.com>",
|
"Luca Palmieri <rust@lpalmieri.com>",
|
||||||
]
|
]
|
||||||
description = "Session management for Actix We"
|
description = "Session management for Actix Web"
|
||||||
keywords = ["http", "web", "framework", "async", "session"]
|
keywords = ["http", "web", "framework", "async", "session"]
|
||||||
homepage = "https://actix.rs"
|
repository.workspace = true
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
homepage.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -20,9 +20,10 @@ all-features = true
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
cookie-session = []
|
cookie-session = []
|
||||||
redis-actor-session = ["actix-redis", "actix", "futures-core", "rand"]
|
redis-session = ["dep:redis"]
|
||||||
redis-rs-session = ["redis", "rand"]
|
redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"]
|
||||||
redis-rs-tls-session = ["redis-rs-session", "redis/tokio-native-tls-comp"]
|
redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"]
|
||||||
|
redis-pool = ["dep:deadpool-redis"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
@ -30,32 +31,30 @@ actix-utils = "3"
|
|||||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-trait = "0.1"
|
derive_more = { version = "2", features = ["display", "error", "from"] }
|
||||||
derive_more = "0.99.7"
|
rand = "0.9"
|
||||||
rand = { version = "0.8", optional = true }
|
|
||||||
serde = { version = "1" }
|
serde = { version = "1" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# redis-actor-session
|
# redis-session
|
||||||
actix = { version = "0.13", default-features = false, optional = true }
|
redis = { version = "0.29", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
|
||||||
actix-redis = { version = "0.12", optional = true }
|
deadpool-redis = { version = "0.20", optional = true }
|
||||||
futures-core = { version = "0.3.7", default-features = false, optional = true }
|
|
||||||
|
|
||||||
# redis-rs-session
|
|
||||||
redis = { version = "0.23", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-session = { path = ".", features = ["cookie-session", "redis-actor-session", "redis-rs-session"] }
|
actix-session = { path = ".", features = ["cookie-session", "redis-session"] }
|
||||||
actix-test = "0.1.0-beta.10"
|
actix-test = "0.1"
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] }
|
||||||
env_logger = "0.10"
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
log = "0.4"
|
tracing = "0.1.30"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "basic"
|
name = "basic"
|
||||||
required-features = ["redis-actor-session"]
|
required-features = ["redis-session"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "authentication"
|
name = "authentication"
|
||||||
required-features = ["redis-actor-session"]
|
required-features = ["redis-session"]
|
||||||
|
@ -2,13 +2,124 @@
|
|||||||
|
|
||||||
> Session management for Actix Web.
|
> Session management for Actix Web.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-session)
|
[](https://crates.io/crates/actix-session)
|
||||||
[](https://docs.rs/actix-session/0.8.0)
|
[](https://docs.rs/actix-session/0.10.1)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-session/0.8.0)
|
[](https://deps.rs/crate/actix-session/0.10.1)
|
||||||
|
|
||||||
## Documentation & Resources
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
- [API Documentation](https://docs.rs/actix-session)
|
<!-- cargo-rdme start -->
|
||||||
- [Example Projects](https://github.com/actix/examples/tree/master/auth/cookie-session)
|
|
||||||
- Minimum Supported Rust Version (MSRV): 1.57
|
Session management for Actix Web.
|
||||||
|
|
||||||
|
The HTTP protocol, at a first glance, is stateless: the client sends a request, the server parses its content, performs some processing and returns a response. The outcome is only influenced by the provided inputs (i.e. the request content) and whatever state the server queries while performing its processing.
|
||||||
|
|
||||||
|
Stateless systems are easier to reason about, but they are not quite as powerful as we need them to be - e.g. how do you authenticate a user? The user would be forced to authenticate **for every single request**. That is, for example, how 'Basic' Authentication works. While it may work for a machine user (i.e. an API client), it is impractical for a person—you do not want a login prompt on every single page you navigate to!
|
||||||
|
|
||||||
|
There is a solution - **sessions**. Using sessions the server can attach state to a set of requests coming from the same client. They are built on top of cookies - the server sets a cookie in the HTTP response (`Set-Cookie` header), the client (e.g. the browser) will store the cookie and play it back to the server when sending new requests (using the `Cookie` header).
|
||||||
|
|
||||||
|
We refer to the cookie used for sessions as a **session cookie**. Its content is called **session key** (or **session ID**), while the state attached to the session is referred to as **session state**.
|
||||||
|
|
||||||
|
`actix-session` provides an easy-to-use framework to manage sessions in applications built on top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality provided by `actix-session`; it takes care of all the session cookie handling and instructs the **storage backend** to create/delete/update the session state based on the operations performed against the active [`Session`].
|
||||||
|
|
||||||
|
`actix-session` provides some built-in storage backends: ([`CookieSessionStore`], [`RedisSessionStore`]) - you can create a custom storage backend by implementing the [`SessionStore`] trait.
|
||||||
|
|
||||||
|
Further reading on sessions:
|
||||||
|
|
||||||
|
- [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265);
|
||||||
|
- [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
To start using sessions in your Actix Web application you must register [`SessionMiddleware`] as a middleware on your `App`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::{web, App, HttpServer, HttpResponse, Error};
|
||||||
|
use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
|
||||||
|
use actix_web::cookie::Key;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// When using `Key::generate()` it is important to initialize outside of the
|
||||||
|
// `HttpServer::new` closure. When deployed the secret key should be read from a
|
||||||
|
// configuration file or environment variables.
|
||||||
|
let secret_key = Key::generate();
|
||||||
|
|
||||||
|
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
HttpServer::new(move ||
|
||||||
|
App::new()
|
||||||
|
// Add session management to your application using Redis for session state storage
|
||||||
|
.wrap(
|
||||||
|
SessionMiddleware::new(
|
||||||
|
redis_store.clone(),
|
||||||
|
secret_key.clone(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.default_service(web::to(|| HttpResponse::Ok())))
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The session state can be accessed and modified by your request handlers using the [`Session`] extractor. Note that this doesn't work in the stream of a streaming response.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::Error;
|
||||||
|
use actix_session::Session;
|
||||||
|
|
||||||
|
fn index(session: Session) -> Result<&'static str, Error> {
|
||||||
|
// access the session state
|
||||||
|
if let Some(count) = session.get::<i32>("counter")? {
|
||||||
|
println!("SESSION value: {}", count);
|
||||||
|
// modify the session state
|
||||||
|
session.insert("counter", count + 1)?;
|
||||||
|
} else {
|
||||||
|
session.insert("counter", 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("Welcome!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Choosing A Backend
|
||||||
|
|
||||||
|
By default, `actix-session` does not provide any storage backend to retrieve and save the state attached to your sessions. You can enable:
|
||||||
|
|
||||||
|
- a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature flag.
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo add actix-session --features=cookie-session
|
||||||
|
```
|
||||||
|
|
||||||
|
- a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the `redis-session` feature flag.
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo add actix-session --features=redis-session
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure connection (via the `native-tls` crate):
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo add actix-session --features=redis-session-native-tls
|
||||||
|
```
|
||||||
|
|
||||||
|
If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo add actix-session --features=redis-session-rustls
|
||||||
|
```
|
||||||
|
|
||||||
|
You can implement your own session storage backend using the [`SessionStore`] trait.
|
||||||
|
|
||||||
|
[`SessionStore`]: storage::SessionStore
|
||||||
|
[`CookieSessionStore`]: storage::CookieSessionStore
|
||||||
|
[`RedisSessionStore`]: storage::RedisSessionStore
|
||||||
|
|
||||||
|
<!-- cargo-rdme end -->
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use actix_session::{storage::RedisActorSessionStore, Session, SessionMiddleware};
|
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
cookie::{Key, SameSite},
|
cookie::{Key, SameSite},
|
||||||
error::InternalError,
|
error::InternalError,
|
||||||
middleware, web, App, Error, HttpResponse, HttpServer, Responder,
|
middleware, web, App, Error, HttpResponse, HttpServer, Responder,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Credentials {
|
struct Credentials {
|
||||||
@ -71,12 +73,21 @@ async fn secret(session: Session) -> Result<impl Responder, Error> {
|
|||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy(),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
// The signing key would usually be read from a configuration file/environment variables.
|
// The signing key would usually be read from a configuration file/environment variables.
|
||||||
let signing_key = Key::generate();
|
let signing_key = Key::generate();
|
||||||
|
|
||||||
log::info!("starting HTTP server at http://localhost:8080");
|
tracing::info!("setting up Redis session storage");
|
||||||
|
let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
|
||||||
|
|
||||||
|
tracing::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
@ -84,10 +95,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
// cookie session middleware
|
// cookie session middleware
|
||||||
.wrap(
|
.wrap(
|
||||||
SessionMiddleware::builder(
|
SessionMiddleware::builder(storage.clone(), signing_key.clone())
|
||||||
RedisActorSessionStore::new("127.0.0.1:6379"),
|
|
||||||
signing_key.clone(),
|
|
||||||
)
|
|
||||||
// allow the cookie to be accessed from javascript
|
// allow the cookie to be accessed from javascript
|
||||||
.cookie_http_only(false)
|
.cookie_http_only(false)
|
||||||
// allow the cookie only from the current domain
|
// allow the cookie only from the current domain
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use actix_session::{storage::RedisActorSessionStore, Session, SessionMiddleware};
|
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware};
|
||||||
use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder};
|
use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder};
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
/// simple handler
|
/// simple handler
|
||||||
async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> {
|
async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> {
|
||||||
@ -18,22 +20,28 @@ async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Err
|
|||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy(),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
// The signing key would usually be read from a configuration file/environment variables.
|
// The signing key would usually be read from a configuration file/environment variables.
|
||||||
let signing_key = Key::generate();
|
let signing_key = Key::generate();
|
||||||
|
|
||||||
log::info!("starting HTTP server at http://localhost:8080");
|
tracing::info!("setting up Redis session storage");
|
||||||
|
let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
|
||||||
|
|
||||||
|
tracing::info!("starting HTTP server at http://localhost:8080");
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
// enable logger
|
// enable logger
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
// cookie session middleware
|
// cookie session middleware
|
||||||
.wrap(SessionMiddleware::new(
|
.wrap(SessionMiddleware::new(storage.clone(), signing_key.clone()))
|
||||||
RedisActorSessionStore::new("127.0.0.1:6379"),
|
|
||||||
signing_key.clone(),
|
|
||||||
))
|
|
||||||
// register simple route, handle all methods
|
// register simple route, handle all methods
|
||||||
.service(web::resource("/").to(index))
|
.service(web::resource("/").to(index))
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Configuration options to tune the behaviour of [`SessionMiddleware`].
|
//! Configuration options to tune the behaviour of [`SessionMiddleware`].
|
||||||
|
|
||||||
use actix_web::cookie::{time::Duration, Key, SameSite};
|
use actix_web::cookie::{time::Duration, Key, SameSite};
|
||||||
use derive_more::From;
|
use derive_more::derive::From;
|
||||||
|
|
||||||
use crate::{storage::SessionStore, SessionMiddleware};
|
use crate::{storage::SessionStore, SessionMiddleware};
|
||||||
|
|
||||||
@ -131,6 +131,7 @@ impl PersistentSession {
|
|||||||
///
|
///
|
||||||
/// A persistent session can live more than the specified TTL if the TTL is extended.
|
/// A persistent session can live more than the specified TTL if the TTL is extended.
|
||||||
/// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
|
/// See [`session_ttl_extension_policy`](Self::session_ttl_extension_policy) for more details.
|
||||||
|
#[doc(alias = "max_age", alias = "max age", alias = "expires")]
|
||||||
pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
|
pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
|
||||||
self.session_ttl = session_ttl;
|
self.session_ttl = session_ttl;
|
||||||
self
|
self
|
||||||
|
@ -27,11 +27,11 @@
|
|||||||
//! against the active [`Session`].
|
//! against the active [`Session`].
|
||||||
//!
|
//!
|
||||||
//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
|
//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
|
||||||
//! [`RedisSessionStore`], and [`RedisActorSessionStore`]) - you can create a custom storage backend
|
//! [`RedisSessionStore`]) - you can create a custom storage backend by implementing the
|
||||||
//! by implementing the [`SessionStore`] trait.
|
//! [`SessionStore`] trait.
|
||||||
//!
|
//!
|
||||||
//! Further reading on sessions:
|
//! Further reading on sessions:
|
||||||
//! - [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265);
|
//! - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265);
|
||||||
//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
|
//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
|
||||||
//!
|
//!
|
||||||
//! # Getting started
|
//! # Getting started
|
||||||
@ -40,21 +40,27 @@
|
|||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use actix_web::{web, App, HttpServer, HttpResponse, Error};
|
//! use actix_web::{web, App, HttpServer, HttpResponse, Error};
|
||||||
//! use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
|
//! use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
|
||||||
//! use actix_web::cookie::Key;
|
//! use actix_web::cookie::Key;
|
||||||
//!
|
//!
|
||||||
//! #[actix_web::main]
|
//! #[actix_web::main]
|
||||||
//! async fn main() -> std::io::Result<()> {
|
//! async fn main() -> std::io::Result<()> {
|
||||||
//! // The secret key would usually be read from a configuration file/environment variables.
|
//! // When using `Key::generate()` it is important to initialize outside of the
|
||||||
|
//! // `HttpServer::new` closure. When deployed the secret key should be read from a
|
||||||
|
//! // configuration file or environment variables.
|
||||||
//! let secret_key = Key::generate();
|
//! let secret_key = Key::generate();
|
||||||
//! let redis_connection_string = "127.0.0.1:6379";
|
//!
|
||||||
|
//! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||||
|
//! .await
|
||||||
|
//! .unwrap();
|
||||||
|
//!
|
||||||
//! HttpServer::new(move ||
|
//! HttpServer::new(move ||
|
||||||
//! App::new()
|
//! App::new()
|
||||||
//! // Add session management to your application using Redis for session state storage
|
//! // Add session management to your application using Redis for session state storage
|
||||||
//! .wrap(
|
//! .wrap(
|
||||||
//! SessionMiddleware::new(
|
//! SessionMiddleware::new(
|
||||||
//! RedisActorSessionStore::new(redis_connection_string),
|
//! redis_store.clone(),
|
||||||
//! secret_key.clone()
|
//! secret_key.clone(),
|
||||||
//! )
|
//! )
|
||||||
//! )
|
//! )
|
||||||
//! .default_service(web::to(|| HttpResponse::Ok())))
|
//! .default_service(web::to(|| HttpResponse::Ok())))
|
||||||
@ -93,37 +99,28 @@
|
|||||||
//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
|
//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
|
||||||
//! flag.
|
//! flag.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```console
|
||||||
//! [dependencies]
|
//! cargo add actix-session --features=cookie-session
|
||||||
//! # ...
|
|
||||||
//! actix-session = { version = "...", features = ["cookie-session"] }
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! - a Redis-based backend via [`actix-redis`](https://docs.rs/actix-redis),
|
//! - a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the
|
||||||
//! [`RedisActorSessionStore`], using the `redis-actor-session` feature flag.
|
//! `redis-session` feature flag.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```console
|
||||||
//! [dependencies]
|
//! cargo add actix-session --features=redis-session
|
||||||
//! # ...
|
|
||||||
//! actix-session = { version = "...", features = ["redis-actor-session"] }
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! - a Redis-based backend via [`redis-rs`](https://docs.rs/redis-rs), [`RedisSessionStore`], using
|
//! Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure
|
||||||
//! the `redis-rs-session` feature flag.
|
//! connection (via the `native-tls` crate):
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```console
|
||||||
//! [dependencies]
|
//! cargo add actix-session --features=redis-session-native-tls
|
||||||
//! # ...
|
|
||||||
//! actix-session = { version = "...", features = ["redis-rs-session"] }
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Add the `redis-rs-tls-session` feature flag if you want to connect to Redis using a secured
|
//! If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
|
||||||
//! connection:
|
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```console
|
||||||
//! [dependencies]
|
//! cargo add actix-session --features=redis-session-rustls
|
||||||
//! # ...
|
|
||||||
//! actix-session = { version = "...", features = ["redis-rs-session", "redis-rs-tls-session"] }
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! You can implement your own session storage backend using the [`SessionStore`] trait.
|
//! You can implement your own session storage backend using the [`SessionStore`] trait.
|
||||||
@ -131,11 +128,9 @@
|
|||||||
//! [`SessionStore`]: storage::SessionStore
|
//! [`SessionStore`]: storage::SessionStore
|
||||||
//! [`CookieSessionStore`]: storage::CookieSessionStore
|
//! [`CookieSessionStore`]: storage::CookieSessionStore
|
||||||
//! [`RedisSessionStore`]: storage::RedisSessionStore
|
//! [`RedisSessionStore`]: storage::RedisSessionStore
|
||||||
//! [`RedisActorSessionStore`]: storage::RedisActorSessionStore
|
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![warn(missing_docs)]
|
||||||
#![warn(future_incompatible, missing_docs)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
@ -153,6 +148,7 @@ pub use self::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub mod test_helpers {
|
pub mod test_helpers {
|
||||||
use actix_web::cookie::Key;
|
use actix_web::cookie::Key;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, convert::TryInto, fmt, future::Future, pin::Pin, rc::Rc};
|
use std::{collections::HashMap, fmt, future::Future, pin::Pin, rc::Rc};
|
||||||
|
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@ -47,7 +47,7 @@ use crate::{
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
|
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
|
||||||
/// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
|
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
|
||||||
/// use actix_web::cookie::Key;
|
/// use actix_web::cookie::Key;
|
||||||
///
|
///
|
||||||
/// // The secret key would usually be read from a configuration file/environment variables.
|
/// // The secret key would usually be read from a configuration file/environment variables.
|
||||||
@ -59,17 +59,17 @@ use crate::{
|
|||||||
/// #[actix_web::main]
|
/// #[actix_web::main]
|
||||||
/// async fn main() -> std::io::Result<()> {
|
/// async fn main() -> std::io::Result<()> {
|
||||||
/// let secret_key = get_secret_key();
|
/// let secret_key = get_secret_key();
|
||||||
/// let redis_connection_string = "127.0.0.1:6379";
|
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
|
||||||
/// HttpServer::new(move ||
|
///
|
||||||
|
/// HttpServer::new(move || {
|
||||||
/// App::new()
|
/// App::new()
|
||||||
/// // Add session management to your application using Redis for session state storage
|
/// // Add session management to your application using Redis as storage
|
||||||
/// .wrap(
|
/// .wrap(SessionMiddleware::new(
|
||||||
/// SessionMiddleware::new(
|
/// storage.clone(),
|
||||||
/// RedisActorSessionStore::new(redis_connection_string),
|
/// secret_key.clone(),
|
||||||
/// secret_key.clone()
|
/// ))
|
||||||
/// )
|
/// .default_service(web::to(|| HttpResponse::Ok()))
|
||||||
/// )
|
/// })
|
||||||
/// .default_service(web::to(|| HttpResponse::Ok())))
|
|
||||||
/// .bind(("127.0.0.1", 8080))?
|
/// .bind(("127.0.0.1", 8080))?
|
||||||
/// .run()
|
/// .run()
|
||||||
/// .await
|
/// .await
|
||||||
@ -80,7 +80,7 @@ use crate::{
|
|||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
|
/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
|
||||||
/// use actix_session::{Session, SessionMiddleware, storage::RedisActorSessionStore};
|
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
|
||||||
/// use actix_session::config::PersistentSession;
|
/// use actix_session::config::PersistentSession;
|
||||||
///
|
///
|
||||||
/// // The secret key would usually be read from a configuration file/environment variables.
|
/// // The secret key would usually be read from a configuration file/environment variables.
|
||||||
@ -92,22 +92,20 @@ use crate::{
|
|||||||
/// #[actix_web::main]
|
/// #[actix_web::main]
|
||||||
/// async fn main() -> std::io::Result<()> {
|
/// async fn main() -> std::io::Result<()> {
|
||||||
/// let secret_key = get_secret_key();
|
/// let secret_key = get_secret_key();
|
||||||
/// let redis_connection_string = "127.0.0.1:6379";
|
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
|
||||||
/// HttpServer::new(move ||
|
///
|
||||||
|
/// HttpServer::new(move || {
|
||||||
/// App::new()
|
/// App::new()
|
||||||
/// // Customise session length!
|
/// // Customise session length!
|
||||||
/// .wrap(
|
/// .wrap(
|
||||||
/// SessionMiddleware::builder(
|
/// SessionMiddleware::builder(storage.clone(), secret_key.clone())
|
||||||
/// RedisActorSessionStore::new(redis_connection_string),
|
|
||||||
/// secret_key.clone()
|
|
||||||
/// )
|
|
||||||
/// .session_lifecycle(
|
/// .session_lifecycle(
|
||||||
/// PersistentSession::default()
|
/// PersistentSession::default().session_ttl(time::Duration::days(5)),
|
||||||
/// .session_ttl(time::Duration::days(5))
|
|
||||||
/// )
|
/// )
|
||||||
/// .build(),
|
/// .build(),
|
||||||
/// )
|
/// )
|
||||||
/// .default_service(web::to(|| HttpResponse::Ok())))
|
/// .default_service(web::to(|| HttpResponse::Ok()))
|
||||||
|
/// })
|
||||||
/// .bind(("127.0.0.1", 8080))?
|
/// .bind(("127.0.0.1", 8080))?
|
||||||
/// .run()
|
/// .run()
|
||||||
/// .await
|
/// .await
|
||||||
|
@ -14,7 +14,7 @@ use actix_web::{
|
|||||||
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use derive_more::{Display, From};
|
use derive_more::derive::{Display, From};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
/// The primary interface to access and modify session state.
|
/// The primary interface to access and modify session state.
|
||||||
@ -33,6 +33,9 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||||||
/// session.insert("counter", 1)?;
|
/// session.insert("counter", 1)?;
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// // or use the shorthand
|
||||||
|
/// session.update_or("counter", 1, |count: i32| count + 1);
|
||||||
|
///
|
||||||
/// Ok("Welcome!")
|
/// Ok("Welcome!")
|
||||||
/// }
|
/// }
|
||||||
/// # actix_web::web::to(index);
|
/// # actix_web::web::to(index);
|
||||||
@ -97,6 +100,11 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the session contains a value for the specified `key`.
|
||||||
|
pub fn contains_key(&self, key: &str) -> bool {
|
||||||
|
self.0.borrow().state.contains_key(key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all raw key-value data from the session.
|
/// Get all raw key-value data from the session.
|
||||||
///
|
///
|
||||||
/// Note that values are JSON encoded.
|
/// Note that values are JSON encoded.
|
||||||
@ -114,7 +122,9 @@ impl Session {
|
|||||||
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
|
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
|
||||||
/// only a reference to the value is taken.
|
/// only a reference to the value is taken.
|
||||||
///
|
///
|
||||||
/// It returns an error if it fails to serialize `value` to JSON.
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if JSON serialization of `value` fails.
|
||||||
pub fn insert<T: Serialize>(
|
pub fn insert<T: Serialize>(
|
||||||
&self,
|
&self,
|
||||||
key: impl Into<String>,
|
key: impl Into<String>,
|
||||||
@ -132,9 +142,8 @@ impl Session {
|
|||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to serialize the provided `{}` type instance as JSON in order to \
|
"Failed to serialize the provided `{}` type instance as JSON in order to \
|
||||||
attach as session data to the `{}` key",
|
attach as session data to the `{key}` key",
|
||||||
std::any::type_name::<T>(),
|
std::any::type_name::<T>(),
|
||||||
&key
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map_err(SessionInsertError)?;
|
.map_err(SessionInsertError)?;
|
||||||
@ -145,6 +154,83 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates a key-value pair into the session.
|
||||||
|
///
|
||||||
|
/// If the key exists then update it to the new value and place it back in. If the key does not
|
||||||
|
/// exist it will not be updated.
|
||||||
|
///
|
||||||
|
/// Any serializable value can be used and will be encoded as JSON in the session data, hence
|
||||||
|
/// why only a reference to the value is taken.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if JSON serialization of the value fails.
|
||||||
|
pub fn update<T: Serialize + DeserializeOwned, F>(
|
||||||
|
&self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
updater: F,
|
||||||
|
) -> Result<(), SessionUpdateError>
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> T,
|
||||||
|
{
|
||||||
|
let mut inner = self.0.borrow_mut();
|
||||||
|
let key_str = key.into();
|
||||||
|
|
||||||
|
if let Some(val_str) = inner.state.get(&key_str) {
|
||||||
|
let value = serde_json::from_str(val_str)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to deserialize the JSON-encoded session data attached to key \
|
||||||
|
`{key_str}` as a `{}` type",
|
||||||
|
std::any::type_name::<T>()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(SessionUpdateError)?;
|
||||||
|
|
||||||
|
let val = serde_json::to_string(&updater(value))
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to serialize the provided `{}` type instance as JSON in order to \
|
||||||
|
attach as session data to the `{key_str}` key",
|
||||||
|
std::any::type_name::<T>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(SessionUpdateError)?;
|
||||||
|
|
||||||
|
inner.state.insert(key_str, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates a key-value pair into the session, or inserts a default value.
|
||||||
|
///
|
||||||
|
/// If the key exists then update it to the new value and place it back in. If the key does not
|
||||||
|
/// exist the default value will be inserted instead.
|
||||||
|
///
|
||||||
|
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
|
||||||
|
/// only a reference to the value is taken.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns error if JSON serialization of a value fails.
|
||||||
|
pub fn update_or<T: Serialize + DeserializeOwned, F>(
|
||||||
|
&self,
|
||||||
|
key: &str,
|
||||||
|
default_value: T,
|
||||||
|
updater: F,
|
||||||
|
) -> Result<(), SessionUpdateError>
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> T,
|
||||||
|
{
|
||||||
|
if self.contains_key(key) {
|
||||||
|
self.update(key, updater)
|
||||||
|
} else {
|
||||||
|
self.insert(key, default_value)
|
||||||
|
.map_err(|err| SessionUpdateError(err.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove value from the session.
|
/// Remove value from the session.
|
||||||
///
|
///
|
||||||
/// If present, the JSON encoded value is returned.
|
/// If present, the JSON encoded value is returned.
|
||||||
@ -288,7 +374,7 @@ impl FromRequest for Session {
|
|||||||
|
|
||||||
/// Error returned by [`Session::get`].
|
/// Error returned by [`Session::get`].
|
||||||
#[derive(Debug, Display, From)]
|
#[derive(Debug, Display, From)]
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
pub struct SessionGetError(anyhow::Error);
|
pub struct SessionGetError(anyhow::Error);
|
||||||
|
|
||||||
impl StdError for SessionGetError {
|
impl StdError for SessionGetError {
|
||||||
@ -305,7 +391,7 @@ impl ResponseError for SessionGetError {
|
|||||||
|
|
||||||
/// Error returned by [`Session::insert`].
|
/// Error returned by [`Session::insert`].
|
||||||
#[derive(Debug, Display, From)]
|
#[derive(Debug, Display, From)]
|
||||||
#[display(fmt = "{_0}")]
|
#[display("{_0}")]
|
||||||
pub struct SessionInsertError(anyhow::Error);
|
pub struct SessionInsertError(anyhow::Error);
|
||||||
|
|
||||||
impl StdError for SessionInsertError {
|
impl StdError for SessionInsertError {
|
||||||
@ -319,3 +405,20 @@ impl ResponseError for SessionInsertError {
|
|||||||
HttpResponse::new(self.status_code())
|
HttpResponse::new(self.status_code())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error returned by [`Session::update`].
|
||||||
|
#[derive(Debug, Display, From)]
|
||||||
|
#[display("{_0}")]
|
||||||
|
pub struct SessionUpdateError(anyhow::Error);
|
||||||
|
|
||||||
|
impl StdError for SessionUpdateError {
|
||||||
|
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||||
|
Some(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for SessionUpdateError {
|
||||||
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
|
HttpResponse::new(self.status_code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ impl SessionExt for ServiceResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SessionExt for GuardContext<'a> {
|
impl SessionExt for GuardContext<'_> {
|
||||||
fn get_session(&self) -> Session {
|
fn get_session(&self) -> Session {
|
||||||
Session::get_session(&mut self.req_data_mut())
|
Session::get_session(&mut self.req_data_mut())
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
use actix_web::cookie::time::Duration;
|
use actix_web::cookie::time::Duration;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
@ -51,7 +49,6 @@ use crate::storage::{
|
|||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct CookieSessionStore;
|
pub struct CookieSessionStore;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl SessionStore for CookieSessionStore {
|
impl SessionStore for CookieSessionStore {
|
||||||
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
||||||
serde_json::from_str(session_key.as_ref())
|
serde_json::from_str(session_key.as_ref())
|
||||||
@ -69,10 +66,10 @@ impl SessionStore for CookieSessionStore {
|
|||||||
.map_err(anyhow::Error::new)
|
.map_err(anyhow::Error::new)
|
||||||
.map_err(SaveError::Serialization)?;
|
.map_err(SaveError::Serialization)?;
|
||||||
|
|
||||||
Ok(session_key
|
session_key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.map_err(SaveError::Other)?)
|
.map_err(SaveError::Other)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, future::Future};
|
||||||
|
|
||||||
use actix_web::cookie::time::Duration;
|
use actix_web::cookie::time::Duration;
|
||||||
use derive_more::Display;
|
use derive_more::derive::Display;
|
||||||
|
|
||||||
use super::SessionKey;
|
use super::SessionKey;
|
||||||
|
|
||||||
@ -10,41 +10,39 @@ pub(crate) type SessionState = HashMap<String, String>;
|
|||||||
/// The interface to retrieve and save the current session data from/to the chosen storage backend.
|
/// The interface to retrieve and save the current session data from/to the chosen storage backend.
|
||||||
///
|
///
|
||||||
/// You can provide your own custom session store backend by implementing this trait.
|
/// You can provide your own custom session store backend by implementing this trait.
|
||||||
///
|
|
||||||
/// [`async-trait`](https://docs.rs/async-trait) is used for this trait's definition. Therefore, it
|
|
||||||
/// is required for implementations, too. In particular, we use the send-optional variant:
|
|
||||||
/// `#[async_trait(?Send)]`.
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
pub trait SessionStore {
|
pub trait SessionStore {
|
||||||
/// Loads the session state associated to a session key.
|
/// Loads the session state associated to a session key.
|
||||||
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError>;
|
fn load(
|
||||||
|
&self,
|
||||||
|
session_key: &SessionKey,
|
||||||
|
) -> impl Future<Output = Result<Option<SessionState>, LoadError>>;
|
||||||
|
|
||||||
/// Persist the session state for a newly created session.
|
/// Persist the session state for a newly created session.
|
||||||
///
|
///
|
||||||
/// Returns the corresponding session key.
|
/// Returns the corresponding session key.
|
||||||
async fn save(
|
fn save(
|
||||||
&self,
|
&self,
|
||||||
session_state: SessionState,
|
session_state: SessionState,
|
||||||
ttl: &Duration,
|
ttl: &Duration,
|
||||||
) -> Result<SessionKey, SaveError>;
|
) -> impl Future<Output = Result<SessionKey, SaveError>>;
|
||||||
|
|
||||||
/// Updates the session state associated to a pre-existing session key.
|
/// Updates the session state associated to a pre-existing session key.
|
||||||
async fn update(
|
fn update(
|
||||||
&self,
|
&self,
|
||||||
session_key: SessionKey,
|
session_key: SessionKey,
|
||||||
session_state: SessionState,
|
session_state: SessionState,
|
||||||
ttl: &Duration,
|
ttl: &Duration,
|
||||||
) -> Result<SessionKey, UpdateError>;
|
) -> impl Future<Output = Result<SessionKey, UpdateError>>;
|
||||||
|
|
||||||
/// Updates the TTL of the session state associated to a pre-existing session key.
|
/// Updates the TTL of the session state associated to a pre-existing session key.
|
||||||
async fn update_ttl(
|
fn update_ttl(
|
||||||
&self,
|
&self,
|
||||||
session_key: &SessionKey,
|
session_key: &SessionKey,
|
||||||
ttl: &Duration,
|
ttl: &Duration,
|
||||||
) -> Result<(), anyhow::Error>;
|
) -> impl Future<Output = Result<(), anyhow::Error>>;
|
||||||
|
|
||||||
/// Deletes a session from the store.
|
/// Deletes a session from the store.
|
||||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error>;
|
fn delete(&self, session_key: &SessionKey) -> impl Future<Output = Result<(), anyhow::Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We cannot derive the `Error` implementation using `derive_more` for our custom errors:
|
// We cannot derive the `Error` implementation using `derive_more` for our custom errors:
|
||||||
@ -55,11 +53,11 @@ pub trait SessionStore {
|
|||||||
#[derive(Debug, Display)]
|
#[derive(Debug, Display)]
|
||||||
pub enum LoadError {
|
pub enum LoadError {
|
||||||
/// Failed to deserialize session state.
|
/// Failed to deserialize session state.
|
||||||
#[display(fmt = "Failed to deserialize session state")]
|
#[display("Failed to deserialize session state")]
|
||||||
Deserialization(anyhow::Error),
|
Deserialization(anyhow::Error),
|
||||||
|
|
||||||
/// Something went wrong when retrieving the session state.
|
/// Something went wrong when retrieving the session state.
|
||||||
#[display(fmt = "Something went wrong when retrieving the session state")]
|
#[display("Something went wrong when retrieving the session state")]
|
||||||
Other(anyhow::Error),
|
Other(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,11 +74,11 @@ impl std::error::Error for LoadError {
|
|||||||
#[derive(Debug, Display)]
|
#[derive(Debug, Display)]
|
||||||
pub enum SaveError {
|
pub enum SaveError {
|
||||||
/// Failed to serialize session state.
|
/// Failed to serialize session state.
|
||||||
#[display(fmt = "Failed to serialize session state")]
|
#[display("Failed to serialize session state")]
|
||||||
Serialization(anyhow::Error),
|
Serialization(anyhow::Error),
|
||||||
|
|
||||||
/// Something went wrong when persisting the session state.
|
/// Something went wrong when persisting the session state.
|
||||||
#[display(fmt = "Something went wrong when persisting the session state")]
|
#[display("Something went wrong when persisting the session state")]
|
||||||
Other(anyhow::Error),
|
Other(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +95,11 @@ impl std::error::Error for SaveError {
|
|||||||
/// Possible failures modes for [`SessionStore::update`].
|
/// Possible failures modes for [`SessionStore::update`].
|
||||||
pub enum UpdateError {
|
pub enum UpdateError {
|
||||||
/// Failed to serialize session state.
|
/// Failed to serialize session state.
|
||||||
#[display(fmt = "Failed to serialize session state")]
|
#[display("Failed to serialize session state")]
|
||||||
Serialization(anyhow::Error),
|
Serialization(anyhow::Error),
|
||||||
|
|
||||||
/// Something went wrong when updating the session state.
|
/// Something went wrong when updating the session state.
|
||||||
#[display(fmt = "Something went wrong when updating the session state.")]
|
#[display("Something went wrong when updating the session state.")]
|
||||||
Other(anyhow::Error),
|
Other(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,19 @@
|
|||||||
//! Pluggable storage backends for session state.
|
//! Pluggable storage backends for session state.
|
||||||
|
|
||||||
mod interface;
|
|
||||||
mod session_key;
|
|
||||||
|
|
||||||
pub use self::{
|
|
||||||
interface::{LoadError, SaveError, SessionStore, UpdateError},
|
|
||||||
session_key::SessionKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "cookie-session")]
|
#[cfg(feature = "cookie-session")]
|
||||||
mod cookie;
|
mod cookie;
|
||||||
|
mod interface;
|
||||||
#[cfg(feature = "redis-actor-session")]
|
#[cfg(feature = "redis-session")]
|
||||||
mod redis_actor;
|
|
||||||
|
|
||||||
#[cfg(feature = "redis-rs-session")]
|
|
||||||
mod redis_rs;
|
mod redis_rs;
|
||||||
|
mod session_key;
|
||||||
#[cfg(any(feature = "redis-actor-session", feature = "redis-rs-session"))]
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[cfg(feature = "cookie-session")]
|
#[cfg(feature = "cookie-session")]
|
||||||
pub use cookie::CookieSessionStore;
|
pub use self::cookie::CookieSessionStore;
|
||||||
#[cfg(feature = "redis-actor-session")]
|
#[cfg(feature = "redis-session")]
|
||||||
pub use redis_actor::{RedisActorSessionStore, RedisActorSessionStoreBuilder};
|
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
|
||||||
#[cfg(feature = "redis-rs-session")]
|
pub use self::{
|
||||||
pub use redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
|
interface::{LoadError, SaveError, SessionStore, UpdateError},
|
||||||
|
session_key::SessionKey,
|
||||||
|
utils::generate_session_key,
|
||||||
|
};
|
||||||
|
@ -118,7 +118,6 @@ impl RedisActorSessionStoreBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl SessionStore for RedisActorSessionStore {
|
impl SessionStore for RedisActorSessionStore {
|
||||||
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
@ -278,8 +277,6 @@ impl SessionStore for RedisActorSessionStore {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix_web::cookie::time::Duration;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_helpers::acceptance_test_suite;
|
use crate::test_helpers::acceptance_test_suite;
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::{convert::TryInto, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_web::cookie::time::Duration;
|
use actix_web::cookie::time::Duration;
|
||||||
use anyhow::{Context, Error};
|
use anyhow::Error;
|
||||||
use redis::{aio::ConnectionManager, AsyncCommands, Cmd, FromRedisValue, RedisResult, Value};
|
use redis::{aio::ConnectionManager, AsyncCommands, Client, Cmd, FromRedisValue, Value};
|
||||||
|
|
||||||
use super::SessionKey;
|
use super::SessionKey;
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
@ -44,7 +44,7 @@ use crate::storage::{
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # TLS support
|
/// # TLS support
|
||||||
/// Add the `redis-rs-tls-session` feature flag to enable TLS support. You can then establish a TLS
|
/// Add the `redis-session-native-tls` or `redis-session-rustls` feature flag to enable TLS support. You can then establish a TLS
|
||||||
/// connection to Redis using the `rediss://` URL scheme:
|
/// connection to Redis using the `rediss://` URL scheme:
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@ -56,14 +56,38 @@ use crate::storage::{
|
|||||||
/// # })
|
/// # })
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Implementation notes
|
/// # Pooled Redis Connections
|
||||||
/// `RedisSessionStore` leverages [`redis-rs`] as Redis client.
|
|
||||||
///
|
///
|
||||||
/// [`redis-rs`]: https://github.com/mitsuhiko/redis-rs
|
/// When the `redis-pool` crate feature is enabled, a pre-existing pool from [`deadpool_redis`] can
|
||||||
|
/// be provided.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use actix_session::storage::RedisSessionStore;
|
||||||
|
/// use deadpool_redis::{Config, Runtime};
|
||||||
|
///
|
||||||
|
/// let redis_cfg = Config::from_url("redis://127.0.0.1:6379");
|
||||||
|
/// let redis_pool = redis_cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
|
||||||
|
///
|
||||||
|
/// let store = RedisSessionStore::new_pooled(redis_pool);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Implementation notes
|
||||||
|
///
|
||||||
|
/// `RedisSessionStore` leverages the [`redis`] crate as the underlying Redis client.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RedisSessionStore {
|
pub struct RedisSessionStore {
|
||||||
configuration: CacheConfiguration,
|
configuration: CacheConfiguration,
|
||||||
client: ConnectionManager,
|
client: RedisSessionConn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum RedisSessionConn {
|
||||||
|
/// Single connection.
|
||||||
|
Single(ConnectionManager),
|
||||||
|
|
||||||
|
/// Connection pool.
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
Pool(deadpool_redis::Pool),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -80,34 +104,77 @@ impl Default for CacheConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RedisSessionStore {
|
impl RedisSessionStore {
|
||||||
/// A fluent API to configure [`RedisSessionStore`].
|
/// Returns a fluent API builder to configure [`RedisSessionStore`].
|
||||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
|
///
|
||||||
/// connection string for Redis.
|
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||||
pub fn builder<S: Into<String>>(connection_string: S) -> RedisSessionStoreBuilder {
|
/// - a connection string for Redis.
|
||||||
|
pub fn builder(connection_string: impl Into<String>) -> RedisSessionStoreBuilder {
|
||||||
RedisSessionStoreBuilder {
|
RedisSessionStoreBuilder {
|
||||||
configuration: CacheConfiguration::default(),
|
configuration: CacheConfiguration::default(),
|
||||||
connection_string: connection_string.into(),
|
conn_builder: RedisSessionConnBuilder::Single(connection_string.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new instance of [`RedisSessionStore`] using the default configuration.
|
/// Returns a fluent API builder to configure [`RedisSessionStore`].
|
||||||
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`] - a
|
///
|
||||||
/// connection string for Redis.
|
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||||
pub async fn new<S: Into<String>>(
|
/// - a pool object for Redis.
|
||||||
connection_string: S,
|
#[cfg(feature = "redis-pool")]
|
||||||
) -> Result<RedisSessionStore, anyhow::Error> {
|
pub fn builder_pooled(pool: impl Into<deadpool_redis::Pool>) -> RedisSessionStoreBuilder {
|
||||||
|
RedisSessionStoreBuilder {
|
||||||
|
configuration: CacheConfiguration::default(),
|
||||||
|
conn_builder: RedisSessionConnBuilder::Pool(pool.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
|
||||||
|
///
|
||||||
|
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||||
|
/// - a connection string for Redis.
|
||||||
|
pub async fn new(connection_string: impl Into<String>) -> Result<RedisSessionStore, Error> {
|
||||||
Self::builder(connection_string).build().await
|
Self::builder(connection_string).build().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
|
||||||
|
///
|
||||||
|
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
|
||||||
|
/// - a pool object for Redis.
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
pub async fn new_pooled(
|
||||||
|
pool: impl Into<deadpool_redis::Pool>,
|
||||||
|
) -> anyhow::Result<RedisSessionStore> {
|
||||||
|
Self::builder_pooled(pool).build().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
|
/// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
|
||||||
/// parameters.
|
/// parameters.
|
||||||
///
|
|
||||||
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct RedisSessionStoreBuilder {
|
pub struct RedisSessionStoreBuilder {
|
||||||
connection_string: String,
|
|
||||||
configuration: CacheConfiguration,
|
configuration: CacheConfiguration,
|
||||||
|
conn_builder: RedisSessionConnBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RedisSessionConnBuilder {
|
||||||
|
/// Single connection string.
|
||||||
|
Single(String),
|
||||||
|
|
||||||
|
/// Pre-built connection pool.
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
Pool(deadpool_redis::Pool),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisSessionConnBuilder {
|
||||||
|
async fn into_client(self) -> anyhow::Result<RedisSessionConn> {
|
||||||
|
Ok(match self {
|
||||||
|
RedisSessionConnBuilder::Single(conn_string) => {
|
||||||
|
RedisSessionConn::Single(ConnectionManager::new(Client::open(conn_string)?).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
RedisSessionConnBuilder::Pool(pool) => RedisSessionConn::Pool(pool),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedisSessionStoreBuilder {
|
impl RedisSessionStoreBuilder {
|
||||||
@ -120,11 +187,10 @@ impl RedisSessionStoreBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finalise the builder and return a [`RedisActorSessionStore`] instance.
|
/// Finalises builder and returns a [`RedisSessionStore`] instance.
|
||||||
///
|
pub async fn build(self) -> anyhow::Result<RedisSessionStore> {
|
||||||
/// [`RedisActorSessionStore`]: crate::storage::RedisActorSessionStore
|
let client = self.conn_builder.into_client().await?;
|
||||||
pub async fn build(self) -> Result<RedisSessionStore, anyhow::Error> {
|
|
||||||
let client = ConnectionManager::new(redis::Client::open(self.connection_string)?).await?;
|
|
||||||
Ok(RedisSessionStore {
|
Ok(RedisSessionStore {
|
||||||
configuration: self.configuration,
|
configuration: self.configuration,
|
||||||
client,
|
client,
|
||||||
@ -132,7 +198,6 @@ impl RedisSessionStoreBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl SessionStore for RedisSessionStore {
|
impl SessionStore for RedisSessionStore {
|
||||||
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
@ -140,7 +205,6 @@ impl SessionStore for RedisSessionStore {
|
|||||||
let value: Option<String> = self
|
let value: Option<String> = self
|
||||||
.execute_command(redis::cmd("GET").arg(&[&cache_key]))
|
.execute_command(redis::cmd("GET").arg(&[&cache_key]))
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
|
||||||
.map_err(LoadError::Other)?;
|
.map_err(LoadError::Other)?;
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
@ -162,15 +226,19 @@ impl SessionStore for RedisSessionStore {
|
|||||||
let session_key = generate_session_key();
|
let session_key = generate_session_key();
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
|
|
||||||
self.execute_command(redis::cmd("SET").arg(&[
|
self.execute_command::<()>(
|
||||||
&cache_key,
|
redis::cmd("SET")
|
||||||
&body,
|
.arg(&[
|
||||||
"NX", // NX: only set the key if it does not already exist
|
&cache_key, // key
|
||||||
"EX", // EX: set expiry
|
&body, // value
|
||||||
&format!("{}", ttl.whole_seconds()),
|
"NX", // only set the key if it does not already exist
|
||||||
]))
|
"EX", // set expiry / TTL
|
||||||
|
])
|
||||||
|
.arg(
|
||||||
|
ttl.whole_seconds(), // EXpiry in seconds
|
||||||
|
),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
|
||||||
.map_err(SaveError::Other)?;
|
.map_err(SaveError::Other)?;
|
||||||
|
|
||||||
Ok(session_key)
|
Ok(session_key)
|
||||||
@ -188,7 +256,7 @@ impl SessionStore for RedisSessionStore {
|
|||||||
|
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
|
|
||||||
let v: redis::Value = self
|
let v: Value = self
|
||||||
.execute_command(redis::cmd("SET").arg(&[
|
.execute_command(redis::cmd("SET").arg(&[
|
||||||
&cache_key,
|
&cache_key,
|
||||||
&body,
|
&body,
|
||||||
@ -197,7 +265,6 @@ impl SessionStore for RedisSessionStore {
|
|||||||
&format!("{}", ttl.whole_seconds()),
|
&format!("{}", ttl.whole_seconds()),
|
||||||
]))
|
]))
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
|
||||||
.map_err(UpdateError::Other)?;
|
.map_err(UpdateError::Other)?;
|
||||||
|
|
||||||
match v {
|
match v {
|
||||||
@ -213,7 +280,7 @@ impl SessionStore for RedisSessionStore {
|
|||||||
SaveError::Other(err) => UpdateError::Other(err),
|
SaveError::Other(err) => UpdateError::Other(err),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Value::Int(_) | Value::Okay | Value::Status(_) => Ok(session_key),
|
Value::Int(_) | Value::Okay | Value::SimpleString(_) => Ok(session_key),
|
||||||
val => Err(UpdateError::Other(anyhow::anyhow!(
|
val => Err(UpdateError::Other(anyhow::anyhow!(
|
||||||
"Failed to update session state. {:?}",
|
"Failed to update session state. {:?}",
|
||||||
val
|
val
|
||||||
@ -221,26 +288,33 @@ impl SessionStore for RedisSessionStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
|
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> anyhow::Result<()> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
|
|
||||||
self.client
|
match self.client {
|
||||||
.clone()
|
RedisSessionConn::Single(ref conn) => {
|
||||||
.expire(
|
conn.clone()
|
||||||
&cache_key,
|
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
|
||||||
ttl.whole_seconds().try_into().context(
|
|
||||||
"Failed to convert the state TTL into the number of whole seconds remaining",
|
|
||||||
)?,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
RedisSessionConn::Pool(ref pool) => {
|
||||||
|
pool.get()
|
||||||
|
.await?
|
||||||
|
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
|
async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> {
|
||||||
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
|
||||||
self.execute_command(redis::cmd("DEL").arg(&[&cache_key]))
|
|
||||||
|
self.execute_command::<()>(redis::cmd("DEL").arg(&[&cache_key]))
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
|
||||||
.map_err(UpdateError::Other)?;
|
.map_err(UpdateError::Other)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -262,11 +336,15 @@ impl RedisSessionStore {
|
|||||||
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
|
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
|
||||||
/// a different more meaningful reason).
|
/// a different more meaningful reason).
|
||||||
#[allow(clippy::needless_pass_by_ref_mut)]
|
#[allow(clippy::needless_pass_by_ref_mut)]
|
||||||
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> RedisResult<T> {
|
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<T> {
|
||||||
let mut can_retry = true;
|
let mut can_retry = true;
|
||||||
|
|
||||||
|
match self.client {
|
||||||
|
RedisSessionConn::Single(ref conn) => {
|
||||||
|
let mut conn = conn.clone();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match cmd.query_async(&mut self.client.clone()).await {
|
match cmd.query_async(&mut conn).await {
|
||||||
Ok(value) => return Ok(value),
|
Ok(value) => return Ok(value),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if can_retry && err.is_connection_dropped() {
|
if can_retry && err.is_connection_dropped() {
|
||||||
@ -279,7 +357,34 @@ impl RedisSessionStore {
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
return Err(err);
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
RedisSessionConn::Pool(ref pool) => {
|
||||||
|
let mut conn = pool.get().await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match cmd.query_async(&mut conn).await {
|
||||||
|
Ok(value) => return Ok(value),
|
||||||
|
Err(err) => {
|
||||||
|
if can_retry && err.is_connection_dropped() {
|
||||||
|
tracing::debug!(
|
||||||
|
"Connection dropped while trying to talk to Redis. Retrying."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retry at most once
|
||||||
|
can_retry = false;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,17 +397,29 @@ mod tests {
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix_web::cookie::time;
|
use actix_web::cookie::time;
|
||||||
use redis::AsyncCommands;
|
#[cfg(not(feature = "redis-session"))]
|
||||||
|
use deadpool_redis::{Config, Runtime};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_helpers::acceptance_test_suite;
|
use crate::test_helpers::acceptance_test_suite;
|
||||||
|
|
||||||
async fn redis_store() -> RedisSessionStore {
|
async fn redis_store() -> RedisSessionStore {
|
||||||
|
#[cfg(feature = "redis-session")]
|
||||||
|
{
|
||||||
RedisSessionStore::new("redis://127.0.0.1:6379")
|
RedisSessionStore::new("redis://127.0.0.1:6379")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "redis-session"))]
|
||||||
|
{
|
||||||
|
let redis_pool = Config::from_url("redis://127.0.0.1:6379")
|
||||||
|
.create_pool(Some(Runtime::Tokio1))
|
||||||
|
.unwrap();
|
||||||
|
RedisSessionStore::new(redis_pool.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_session_workflow() {
|
async fn test_session_workflow() {
|
||||||
let redis_store = redis_store().await;
|
let redis_store = redis_store().await;
|
||||||
@ -320,12 +437,25 @@ mod tests {
|
|||||||
async fn loading_an_invalid_session_state_returns_deserialization_error() {
|
async fn loading_an_invalid_session_state_returns_deserialization_error() {
|
||||||
let store = redis_store().await;
|
let store = redis_store().await;
|
||||||
let session_key = generate_session_key();
|
let session_key = generate_session_key();
|
||||||
store
|
|
||||||
.client
|
match store.client {
|
||||||
|
RedisSessionConn::Single(ref conn) => conn
|
||||||
.clone()
|
.clone()
|
||||||
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||||
.await
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
|
||||||
|
#[cfg(feature = "redis-pool")]
|
||||||
|
RedisSessionConn::Pool(ref pool) => {
|
||||||
|
pool.get()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.load(&session_key).await.unwrap_err(),
|
store.load(&session_key).await.unwrap_err(),
|
||||||
LoadError::Deserialization(_),
|
LoadError::Deserialization(_),
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
use std::convert::TryFrom;
|
use derive_more::derive::{Display, From};
|
||||||
|
|
||||||
use derive_more::{Display, From};
|
|
||||||
|
|
||||||
/// A session key, the string stored in a client-side cookie to associate a user with its session
|
/// A session key, the string stored in a client-side cookie to associate a user with its session
|
||||||
/// state on the backend.
|
/// state on the backend.
|
||||||
@ -9,8 +7,7 @@ use derive_more::{Display, From};
|
|||||||
/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
|
/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
|
||||||
/// required to be smaller than 4064 bytes.
|
/// required to be smaller than 4064 bytes.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```
|
||||||
/// # use std::convert::TryInto;
|
|
||||||
/// use actix_session::storage::SessionKey;
|
/// use actix_session::storage::SessionKey;
|
||||||
///
|
///
|
||||||
/// let key: String = std::iter::repeat('a').take(4065).collect();
|
/// let key: String = std::iter::repeat('a').take(4065).collect();
|
||||||
@ -48,7 +45,7 @@ impl From<SessionKey> for String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Display, From)]
|
#[derive(Debug, Display, From)]
|
||||||
#[display(fmt = "The provided string is not a valid session key")]
|
#[display("The provided string is not a valid session key")]
|
||||||
pub struct InvalidSessionKeyError(anyhow::Error);
|
pub struct InvalidSessionKeyError(anyhow::Error);
|
||||||
|
|
||||||
impl std::error::Error for InvalidSessionKeyError {
|
impl std::error::Error for InvalidSessionKeyError {
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
use std::convert::TryInto;
|
use rand::distr::{Alphanumeric, SampleString as _};
|
||||||
|
|
||||||
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
|
|
||||||
|
|
||||||
use crate::storage::SessionKey;
|
use crate::storage::SessionKey;
|
||||||
|
|
||||||
/// Session key generation routine that follows [OWASP recommendations].
|
/// Session key generation routine that follows [OWASP recommendations].
|
||||||
///
|
///
|
||||||
/// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
|
/// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
|
||||||
pub(crate) fn generate_session_key() -> SessionKey {
|
pub fn generate_session_key() -> SessionKey {
|
||||||
let value = std::iter::repeat(())
|
Alphanumeric
|
||||||
.map(|()| OsRng.sample(Alphanumeric))
|
.sample_string(&mut rand::rng(), 64)
|
||||||
.take(64)
|
.try_into()
|
||||||
.collect::<Vec<_>>();
|
.expect("generated string should be within size range for a session key")
|
||||||
|
|
||||||
// These unwraps will never panic because pre-conditions are always verified
|
|
||||||
// (i.e. length and character set)
|
|
||||||
String::from_utf8(value).unwrap().try_into().unwrap()
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::HashMap, convert::TryInto};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use actix_session::{
|
use actix_session::{
|
||||||
storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError},
|
storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError},
|
||||||
@ -44,7 +44,6 @@ async fn errors_are_opaque() {
|
|||||||
|
|
||||||
struct MockStore;
|
struct MockStore;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl SessionStore for MockStore {
|
impl SessionStore for MockStore {
|
||||||
async fn load(
|
async fn load(
|
||||||
&self,
|
&self,
|
||||||
|
@ -69,6 +69,16 @@ async fn session_entries() {
|
|||||||
map.contains_key("test_num");
|
map.contains_key("test_num");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn session_contains_key() {
|
||||||
|
let req = test::TestRequest::default().to_srv_request();
|
||||||
|
let session = req.get_session();
|
||||||
|
session.insert("test_str", "val").unwrap();
|
||||||
|
session.insert("test_str", 1).unwrap();
|
||||||
|
assert!(session.contains_key("test_str"));
|
||||||
|
assert!(!session.contains_key("test_num"));
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn insert_session_after_renew() {
|
async fn insert_session_after_renew() {
|
||||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||||
@ -83,6 +93,35 @@ async fn insert_session_after_renew() {
|
|||||||
assert_eq!(session.status(), SessionStatus::Renewed);
|
assert_eq!(session.status(), SessionStatus::Renewed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn update_session() {
|
||||||
|
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||||
|
|
||||||
|
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||||
|
assert_eq!(session.status(), SessionStatus::Unchanged);
|
||||||
|
|
||||||
|
session.insert("test_val", 0).unwrap();
|
||||||
|
assert_eq!(session.status(), SessionStatus::Changed);
|
||||||
|
|
||||||
|
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||||
|
assert_eq!(session.get("test_val").unwrap(), Some(1));
|
||||||
|
|
||||||
|
session.update("test_val", |c: u32| c + 1).unwrap();
|
||||||
|
assert_eq!(session.get("test_val").unwrap(), Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn update_or_session() {
|
||||||
|
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||||
|
|
||||||
|
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
|
||||||
|
assert_eq!(session.status(), SessionStatus::Changed);
|
||||||
|
assert_eq!(session.get("test_val").unwrap(), Some(1));
|
||||||
|
|
||||||
|
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
|
||||||
|
assert_eq!(session.get("test_val").unwrap(), Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn remove_session_after_renew() {
|
async fn remove_session_after_renew() {
|
||||||
let session = test::TestRequest::default().to_srv_request().get_session();
|
let session = test::TestRequest::default().to_srv_request().get_session();
|
||||||
|
@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- Add `openssl` crate feature for TLS settings using OpenSSL.
|
||||||
|
- Add `ApplySettings::try_apply_settings()`.
|
||||||
|
- Implement TLS logic for `ApplySettings::try_apply_settings()`.
|
||||||
|
- Add `Tls::get_ssl_acceptor_builder()` function to build `openssl::ssl::SslAcceptorBuilder`.
|
||||||
|
- Deprecate `ApplySettings::apply_settings()`.
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Fix doc examples.
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
- The `ApplySettings` trait now includes a type parameter, allowing multiple types to be implemented per configuration target.
|
||||||
|
- Implement `ApplySettings` for `ActixSettings`.
|
||||||
|
- `BasicSettings::from_default_template()` is now infallible.
|
||||||
- Rename `AtError => Error`.
|
- Rename `AtError => Error`.
|
||||||
- Remove `AtResult` type alias.
|
- Remove `AtResult` type alias.
|
||||||
- Update `toml` dependency to `0.8`.
|
- Update `toml` dependency to `0.8`.
|
||||||
|
@ -1,28 +1,37 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-settings"
|
name = "actix-settings"
|
||||||
version = "0.6.0"
|
version = "0.8.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Joey Ezechiels <joey.ezechiels@gmail.com>",
|
"Joey Ezechiels <joey.ezechiels@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
]
|
]
|
||||||
description = "Easily manage Actix Web's settings from a TOML file and environment variables"
|
description = "Easily manage Actix Web's settings from a TOML file and environment variables"
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
all-features = true
|
|
||||||
|
[features]
|
||||||
|
openssl = ["dep:openssl", "actix-web/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-http = "3"
|
actix-http = "3"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-web = "4"
|
actix-web = { version = "4", default-features = false }
|
||||||
derive_more = "0.99.7"
|
derive_more = { version = "2", features = ["display", "error"] }
|
||||||
once_cell = "1.13"
|
once_cell = "1.21"
|
||||||
regex = "1.5.5"
|
openssl = { version = "0.10", features = ["v110"], optional = true }
|
||||||
|
regex = "1.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.10"
|
actix-web = "4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
> Easily manage Actix Web's settings from a TOML file and environment variables.
|
> Easily manage Actix Web's settings from a TOML file and environment variables.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-settings)
|
[](https://crates.io/crates/actix-settings)
|
||||||
[](https://docs.rs/actix-settings/0.6.0)
|
[](https://docs.rs/actix-settings/0.8.0)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-settings/0.6.0)
|
[](https://deps.rs/crate/actix-settings/0.8.0)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
@ -19,10 +23,6 @@ There is a way to extend the available settings. This can be used to combine the
|
|||||||
|
|
||||||
Have a look at [the usage example][usage] to see how.
|
Have a look at [the usage example][usage] to see how.
|
||||||
|
|
||||||
## WIP
|
|
||||||
|
|
||||||
Configuration options for TLS set up are not yet implemented.
|
|
||||||
|
|
||||||
## Special Thanks
|
## Special Thanks
|
||||||
|
|
||||||
This crate was made possible by support from Accept B.V and [@jjpe].
|
This crate was made possible by support from Accept B.V and [@jjpe].
|
||||||
|
@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// apply the `Settings` to Actix Web's `HttpServer`
|
// apply the `Settings` to Actix Web's `HttpServer`
|
||||||
.apply_settings(&settings)
|
.try_apply_settings(&settings)?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError};
|
use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError};
|
||||||
|
|
||||||
use derive_more::{Display, Error};
|
use derive_more::derive::{Display, Error};
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
use openssl::error::ErrorStack as OpenSSLError;
|
||||||
use toml::de::Error as TomlError;
|
use toml::de::Error as TomlError;
|
||||||
|
|
||||||
/// Errors that can be returned from methods in this crate.
|
/// Errors that can be returned from methods in this crate.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Environment variable does not exists or is invalid.
|
/// Environment variable does not exists or is invalid.
|
||||||
#[display(fmt = "Env var error: {_0}")]
|
#[display("Env var error: {_0}")]
|
||||||
EnvVarError(VarError),
|
EnvVarError(VarError),
|
||||||
|
|
||||||
/// File already exists on disk.
|
/// File already exists on disk.
|
||||||
#[display(fmt = "File exists: {}", "_0.display()")]
|
#[display("File exists: {}", _0.display())]
|
||||||
FileExists(#[error(not(source))] PathBuf),
|
FileExists(#[error(not(source))] PathBuf),
|
||||||
|
|
||||||
/// Invalid value.
|
/// Invalid value.
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[display(fmt = "Expected {expected}, got {got} (@ {file}:{line}:{column})")]
|
#[display("Expected {expected}, got {got} (@ {file}:{line}:{column})")]
|
||||||
InvalidValue {
|
InvalidValue {
|
||||||
expected: &'static str,
|
expected: &'static str,
|
||||||
got: String,
|
got: String,
|
||||||
@ -26,23 +28,28 @@ pub enum Error {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// I/O error.
|
/// I/O error.
|
||||||
#[display(fmt = "")]
|
#[display("I/O error: {_0}")]
|
||||||
IoError(io::Error),
|
IoError(io::Error),
|
||||||
|
|
||||||
|
/// OpenSSL Error.
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
#[display("OpenSSL error: {_0}")]
|
||||||
|
OpenSSLError(OpenSSLError),
|
||||||
|
|
||||||
/// Value is not a boolean.
|
/// Value is not a boolean.
|
||||||
#[display(fmt = "Failed to parse boolean: {_0}")]
|
#[display("Failed to parse boolean: {_0}")]
|
||||||
ParseBoolError(ParseBoolError),
|
ParseBoolError(ParseBoolError),
|
||||||
|
|
||||||
/// Value is not an integer.
|
/// Value is not an integer.
|
||||||
#[display(fmt = "Failed to parse integer: {_0}")]
|
#[display("Failed to parse integer: {_0}")]
|
||||||
ParseIntError(ParseIntError),
|
ParseIntError(ParseIntError),
|
||||||
|
|
||||||
/// Value is not an address.
|
/// Value is not an address.
|
||||||
#[display(fmt = "Failed to parse address: {_0}")]
|
#[display("Failed to parse address: {_0}")]
|
||||||
ParseAddressError(#[error(not(source))] String),
|
ParseAddressError(#[error(not(source))] String),
|
||||||
|
|
||||||
/// Error deserializing as TOML.
|
/// Error deserializing as TOML.
|
||||||
#[display(fmt = "TOML error: {_0}")]
|
#[display("TOML error: {_0}")]
|
||||||
TomlError(TomlError),
|
TomlError(TomlError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +71,13 @@ impl From<io::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
impl From<OpenSSLError> for Error {
|
||||||
|
fn from(err: OpenSSLError) -> Self {
|
||||||
|
Self::OpenSSLError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<ParseBoolError> for Error {
|
impl From<ParseBoolError> for Error {
|
||||||
fn from(err: ParseBoolError) -> Self {
|
fn from(err: ParseBoolError) -> Self {
|
||||||
Self::ParseBoolError(err)
|
Self::ParseBoolError(err)
|
||||||
@ -101,6 +115,9 @@ impl From<Error> for io::Error {
|
|||||||
|
|
||||||
Error::IoError(io_error) => io_error,
|
Error::IoError(io_error) => io_error,
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
Error::OpenSSLError(ossl_error) => io::Error::new(io::ErrorKind::Other, ossl_error),
|
||||||
|
|
||||||
Error::ParseBoolError(_) => {
|
Error::ParseBoolError(_) => {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
|
io::Error::new(io::ErrorKind::InvalidInput, err.to_string())
|
||||||
}
|
}
|
||||||
|
@ -54,17 +54,17 @@
|
|||||||
//! }
|
//! }
|
||||||
//! })
|
//! })
|
||||||
//! // apply the `Settings` to Actix Web's `HttpServer`
|
//! // apply the `Settings` to Actix Web's `HttpServer`
|
||||||
//! .apply_settings(&settings)
|
//! .try_apply_settings(&settings)?
|
||||||
//! .run()
|
//! .run()
|
||||||
//! .await
|
//! .await
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![warn(missing_docs, missing_debug_implementations)]
|
||||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env, fmt,
|
env, fmt,
|
||||||
@ -89,12 +89,14 @@ mod error;
|
|||||||
mod parse;
|
mod parse;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
pub use self::settings::Tls;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
error::Error,
|
error::Error,
|
||||||
parse::Parse,
|
parse::Parse,
|
||||||
settings::{
|
settings::{
|
||||||
ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode,
|
ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode,
|
||||||
NumWorkers, Timeout, Tls,
|
NumWorkers, Timeout,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -154,14 +156,11 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an instance of `Self` straight from the default TOML template.
|
/// Parse an instance of `Self` straight from the default TOML template.
|
||||||
// TODO: make infallible
|
pub fn from_default_template() -> Self {
|
||||||
// TODO: consider "template" rename
|
Self::from_template(Self::DEFAULT_TOML_TEMPLATE).unwrap()
|
||||||
pub fn from_default_template() -> AsResult<Self> {
|
|
||||||
Self::from_template(Self::DEFAULT_TOML_TEMPLATE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an instance of `Self` straight from the default TOML template.
|
/// Parse an instance of `Self` straight from the default TOML template.
|
||||||
// TODO: consider "template" rename
|
|
||||||
pub fn from_template(template: &str) -> AsResult<Self> {
|
pub fn from_template(template: &str) -> AsResult<Self> {
|
||||||
Ok(toml::from_str(template)?)
|
Ok(toml::from_str(template)?)
|
||||||
}
|
}
|
||||||
@ -196,7 +195,7 @@ where
|
|||||||
/// use actix_settings::{Settings, Mode};
|
/// use actix_settings::{Settings, Mode};
|
||||||
///
|
///
|
||||||
/// # fn inner() -> Result<(), actix_settings::Error> {
|
/// # fn inner() -> Result<(), actix_settings::Error> {
|
||||||
/// let mut settings = Settings::from_default_template()?;
|
/// let mut settings = Settings::from_default_template();
|
||||||
/// assert_eq!(settings.actix.mode, Mode::Development);
|
/// assert_eq!(settings.actix.mode, Mode::Development);
|
||||||
///
|
///
|
||||||
/// Settings::override_field(&mut settings.actix.mode, "production")?;
|
/// Settings::override_field(&mut settings.actix.mode, "production")?;
|
||||||
@ -221,7 +220,7 @@ where
|
|||||||
/// std::env::set_var("OVERRIDE__MODE", "production");
|
/// std::env::set_var("OVERRIDE__MODE", "production");
|
||||||
///
|
///
|
||||||
/// # fn inner() -> Result<(), actix_settings::Error> {
|
/// # fn inner() -> Result<(), actix_settings::Error> {
|
||||||
/// let mut settings = Settings::from_default_template()?;
|
/// let mut settings = Settings::from_default_template();
|
||||||
/// assert_eq!(settings.actix.mode, Mode::Development);
|
/// assert_eq!(settings.actix.mode, Mode::Development);
|
||||||
///
|
///
|
||||||
/// Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE")?;
|
/// Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE")?;
|
||||||
@ -242,17 +241,31 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extension trait for applying parsed settings to the server object.
|
/// Extension trait for applying parsed settings to the server object.
|
||||||
pub trait ApplySettings {
|
pub trait ApplySettings<S>: Sized {
|
||||||
/// Apply a [`BasicSettings`] value to `self`.
|
/// Applies some settings object value to `self`.
|
||||||
///
|
///
|
||||||
/// [`BasicSettings`]: ./struct.BasicSettings.html
|
/// The default implementation calls [`try_apply_settings()`].
|
||||||
#[must_use]
|
///
|
||||||
fn apply_settings<A>(self, settings: &BasicSettings<A>) -> Self
|
/// # Panics
|
||||||
where
|
///
|
||||||
A: de::DeserializeOwned;
|
/// May panic if settings are invalid or cannot be applied.
|
||||||
|
///
|
||||||
|
/// [`try_apply_settings()`]: ApplySettings::try_apply_settings().
|
||||||
|
#[deprecated = "Prefer `try_apply_settings()`."]
|
||||||
|
fn apply_settings(self, settings: &S) -> Self {
|
||||||
|
self.try_apply_settings(settings)
|
||||||
|
.expect("Could not apply settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies some settings object value to `self`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// May return error if settings are invalid or cannot be applied.
|
||||||
|
fn try_apply_settings(self, settings: &S) -> AsResult<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F, I, S, B> ApplySettings for HttpServer<F, I, S, B>
|
impl<F, I, S, B> ApplySettings<ActixSettings> for HttpServer<F, I, S, B>
|
||||||
where
|
where
|
||||||
F: Fn() -> I + Send + Clone + 'static,
|
F: Fn() -> I + Send + Clone + 'static,
|
||||||
I: IntoServiceFactory<S, Request>,
|
I: IntoServiceFactory<S, Request>,
|
||||||
@ -263,51 +276,58 @@ where
|
|||||||
S::Future: 'static,
|
S::Future: 'static,
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn apply_settings<A>(mut self, settings: &BasicSettings<A>) -> Self
|
fn apply_settings(self, settings: &ActixSettings) -> Self {
|
||||||
where
|
self.try_apply_settings(settings).unwrap()
|
||||||
A: de::DeserializeOwned,
|
}
|
||||||
|
|
||||||
|
fn try_apply_settings(mut self, settings: &ActixSettings) -> AsResult<Self> {
|
||||||
|
for Address { host, port } in &settings.hosts {
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
{
|
{
|
||||||
if settings.actix.tls.enabled {
|
if settings.tls.enabled {
|
||||||
// for Address { host, port } in &settings.actix.hosts {
|
self = self.bind_openssl(
|
||||||
// self = self.bind(format!("{}:{}", host, port))
|
format!("{}:{}", host, port),
|
||||||
// .unwrap(/*TODO*/);
|
settings.tls.get_ssl_acceptor_builder()?,
|
||||||
// }
|
)?;
|
||||||
todo!("[ApplySettings] TLS support has not been implemented yet.");
|
|
||||||
} else {
|
} else {
|
||||||
for Address { host, port } in &settings.actix.hosts {
|
self = self.bind(format!("{host}:{port}"))?;
|
||||||
self = self.bind(format!("{host}:{port}"))
|
|
||||||
.unwrap(/*TODO*/);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self = match settings.actix.num_workers {
|
#[cfg(not(feature = "openssl"))]
|
||||||
|
{
|
||||||
|
self = self.bind(format!("{host}:{port}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self = match settings.num_workers {
|
||||||
NumWorkers::Default => self,
|
NumWorkers::Default => self,
|
||||||
NumWorkers::Manual(n) => self.workers(n),
|
NumWorkers::Manual(n) => self.workers(n),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.backlog {
|
self = match settings.backlog {
|
||||||
Backlog::Default => self,
|
Backlog::Default => self,
|
||||||
Backlog::Manual(n) => self.backlog(n as u32),
|
Backlog::Manual(n) => self.backlog(n as u32),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.max_connections {
|
self = match settings.max_connections {
|
||||||
MaxConnections::Default => self,
|
MaxConnections::Default => self,
|
||||||
MaxConnections::Manual(n) => self.max_connections(n),
|
MaxConnections::Manual(n) => self.max_connections(n),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.max_connection_rate {
|
self = match settings.max_connection_rate {
|
||||||
MaxConnectionRate::Default => self,
|
MaxConnectionRate::Default => self,
|
||||||
MaxConnectionRate::Manual(n) => self.max_connection_rate(n),
|
MaxConnectionRate::Manual(n) => self.max_connection_rate(n),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.keep_alive {
|
self = match settings.keep_alive {
|
||||||
KeepAlive::Default => self,
|
KeepAlive::Default => self,
|
||||||
KeepAlive::Disabled => self.keep_alive(ActixKeepAlive::Disabled),
|
KeepAlive::Disabled => self.keep_alive(ActixKeepAlive::Disabled),
|
||||||
KeepAlive::Os => self.keep_alive(ActixKeepAlive::Os),
|
KeepAlive::Os => self.keep_alive(ActixKeepAlive::Os),
|
||||||
KeepAlive::Seconds(n) => self.keep_alive(Duration::from_secs(n as u64)),
|
KeepAlive::Seconds(n) => self.keep_alive(Duration::from_secs(n as u64)),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.client_timeout {
|
self = match settings.client_timeout {
|
||||||
Timeout::Default => self,
|
Timeout::Default => self,
|
||||||
Timeout::Milliseconds(n) => {
|
Timeout::Milliseconds(n) => {
|
||||||
self.client_request_timeout(Duration::from_millis(n as u64))
|
self.client_request_timeout(Duration::from_millis(n as u64))
|
||||||
@ -315,7 +335,7 @@ where
|
|||||||
Timeout::Seconds(n) => self.client_request_timeout(Duration::from_secs(n as u64)),
|
Timeout::Seconds(n) => self.client_request_timeout(Duration::from_secs(n as u64)),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.client_shutdown {
|
self = match settings.client_shutdown {
|
||||||
Timeout::Default => self,
|
Timeout::Default => self,
|
||||||
Timeout::Milliseconds(n) => {
|
Timeout::Milliseconds(n) => {
|
||||||
self.client_disconnect_timeout(Duration::from_millis(n as u64))
|
self.client_disconnect_timeout(Duration::from_millis(n as u64))
|
||||||
@ -323,13 +343,34 @@ where
|
|||||||
Timeout::Seconds(n) => self.client_disconnect_timeout(Duration::from_secs(n as u64)),
|
Timeout::Seconds(n) => self.client_disconnect_timeout(Duration::from_secs(n as u64)),
|
||||||
};
|
};
|
||||||
|
|
||||||
self = match settings.actix.shutdown_timeout {
|
self = match settings.shutdown_timeout {
|
||||||
Timeout::Default => self,
|
Timeout::Default => self,
|
||||||
Timeout::Milliseconds(_) => self.shutdown_timeout(1),
|
Timeout::Milliseconds(_) => self.shutdown_timeout(1),
|
||||||
Timeout::Seconds(n) => self.shutdown_timeout(n as u64),
|
Timeout::Seconds(n) => self.shutdown_timeout(n as u64),
|
||||||
};
|
};
|
||||||
|
|
||||||
self
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, I, S, B, A> ApplySettings<BasicSettings<A>> for HttpServer<F, I, S, B>
|
||||||
|
where
|
||||||
|
F: Fn() -> I + Send + Clone + 'static,
|
||||||
|
I: IntoServiceFactory<S, Request>,
|
||||||
|
S: ServiceFactory<Request, Config = AppConfig> + 'static,
|
||||||
|
S::Error: Into<WebError> + 'static,
|
||||||
|
S::InitError: fmt::Debug,
|
||||||
|
S::Response: Into<Response<B>> + 'static,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: MessageBody + 'static,
|
||||||
|
A: de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn apply_settings(self, settings: &BasicSettings<A>) -> Self {
|
||||||
|
self.try_apply_settings(&settings.actix).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_apply_settings(self, settings: &BasicSettings<A>) -> AsResult<Self> {
|
||||||
|
self.try_apply_settings(&settings.actix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,12 +383,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn apply_settings() {
|
fn apply_settings() {
|
||||||
let settings = Settings::parse_toml("Server.toml").unwrap();
|
let settings = Settings::parse_toml("Server.toml").unwrap();
|
||||||
let _ = HttpServer::new(App::new).apply_settings(&settings);
|
let server = HttpServer::new(App::new).try_apply_settings(&settings);
|
||||||
|
assert!(server.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_hosts() {
|
fn override_field_hosts() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.hosts,
|
settings.actix.hosts,
|
||||||
@ -383,7 +425,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_hosts() {
|
fn override_field_with_env_var_hosts() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.hosts,
|
settings.actix.hosts,
|
||||||
@ -421,7 +463,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_mode() {
|
fn override_field_mode() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.mode, Mode::Development);
|
assert_eq!(settings.actix.mode, Mode::Development);
|
||||||
Settings::override_field(&mut settings.actix.mode, "production").unwrap();
|
Settings::override_field(&mut settings.actix.mode, "production").unwrap();
|
||||||
assert_eq!(settings.actix.mode, Mode::Production);
|
assert_eq!(settings.actix.mode, Mode::Production);
|
||||||
@ -429,7 +471,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_mode() {
|
fn override_field_with_env_var_mode() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.mode, Mode::Development);
|
assert_eq!(settings.actix.mode, Mode::Development);
|
||||||
std::env::set_var("OVERRIDE__MODE", "production");
|
std::env::set_var("OVERRIDE__MODE", "production");
|
||||||
Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE").unwrap();
|
Settings::override_field_with_env_var(&mut settings.actix.mode, "OVERRIDE__MODE").unwrap();
|
||||||
@ -438,7 +480,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_enable_compression() {
|
fn override_field_enable_compression() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(settings.actix.enable_compression);
|
assert!(settings.actix.enable_compression);
|
||||||
Settings::override_field(&mut settings.actix.enable_compression, "false").unwrap();
|
Settings::override_field(&mut settings.actix.enable_compression, "false").unwrap();
|
||||||
assert!(!settings.actix.enable_compression);
|
assert!(!settings.actix.enable_compression);
|
||||||
@ -446,7 +488,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_enable_compression() {
|
fn override_field_with_env_var_enable_compression() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(settings.actix.enable_compression);
|
assert!(settings.actix.enable_compression);
|
||||||
std::env::set_var("OVERRIDE__ENABLE_COMPRESSION", "false");
|
std::env::set_var("OVERRIDE__ENABLE_COMPRESSION", "false");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -459,7 +501,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_enable_log() {
|
fn override_field_enable_log() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(settings.actix.enable_log);
|
assert!(settings.actix.enable_log);
|
||||||
Settings::override_field(&mut settings.actix.enable_log, "false").unwrap();
|
Settings::override_field(&mut settings.actix.enable_log, "false").unwrap();
|
||||||
assert!(!settings.actix.enable_log);
|
assert!(!settings.actix.enable_log);
|
||||||
@ -467,7 +509,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_enable_log() {
|
fn override_field_with_env_var_enable_log() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(settings.actix.enable_log);
|
assert!(settings.actix.enable_log);
|
||||||
std::env::set_var("OVERRIDE__ENABLE_LOG", "false");
|
std::env::set_var("OVERRIDE__ENABLE_LOG", "false");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -480,7 +522,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_num_workers() {
|
fn override_field_num_workers() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.num_workers, NumWorkers::Default);
|
assert_eq!(settings.actix.num_workers, NumWorkers::Default);
|
||||||
Settings::override_field(&mut settings.actix.num_workers, "42").unwrap();
|
Settings::override_field(&mut settings.actix.num_workers, "42").unwrap();
|
||||||
assert_eq!(settings.actix.num_workers, NumWorkers::Manual(42));
|
assert_eq!(settings.actix.num_workers, NumWorkers::Manual(42));
|
||||||
@ -488,7 +530,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_num_workers() {
|
fn override_field_with_env_var_num_workers() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.num_workers, NumWorkers::Default);
|
assert_eq!(settings.actix.num_workers, NumWorkers::Default);
|
||||||
std::env::set_var("OVERRIDE__NUM_WORKERS", "42");
|
std::env::set_var("OVERRIDE__NUM_WORKERS", "42");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -501,7 +543,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_backlog() {
|
fn override_field_backlog() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.backlog, Backlog::Default);
|
assert_eq!(settings.actix.backlog, Backlog::Default);
|
||||||
Settings::override_field(&mut settings.actix.backlog, "42").unwrap();
|
Settings::override_field(&mut settings.actix.backlog, "42").unwrap();
|
||||||
assert_eq!(settings.actix.backlog, Backlog::Manual(42));
|
assert_eq!(settings.actix.backlog, Backlog::Manual(42));
|
||||||
@ -509,7 +551,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_backlog() {
|
fn override_field_with_env_var_backlog() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.backlog, Backlog::Default);
|
assert_eq!(settings.actix.backlog, Backlog::Default);
|
||||||
std::env::set_var("OVERRIDE__BACKLOG", "42");
|
std::env::set_var("OVERRIDE__BACKLOG", "42");
|
||||||
Settings::override_field_with_env_var(&mut settings.actix.backlog, "OVERRIDE__BACKLOG")
|
Settings::override_field_with_env_var(&mut settings.actix.backlog, "OVERRIDE__BACKLOG")
|
||||||
@ -519,7 +561,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_max_connections() {
|
fn override_field_max_connections() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.max_connections, MaxConnections::Default);
|
assert_eq!(settings.actix.max_connections, MaxConnections::Default);
|
||||||
Settings::override_field(&mut settings.actix.max_connections, "42").unwrap();
|
Settings::override_field(&mut settings.actix.max_connections, "42").unwrap();
|
||||||
assert_eq!(settings.actix.max_connections, MaxConnections::Manual(42));
|
assert_eq!(settings.actix.max_connections, MaxConnections::Manual(42));
|
||||||
@ -527,7 +569,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_max_connections() {
|
fn override_field_with_env_var_max_connections() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.max_connections, MaxConnections::Default);
|
assert_eq!(settings.actix.max_connections, MaxConnections::Default);
|
||||||
std::env::set_var("OVERRIDE__MAX_CONNECTIONS", "42");
|
std::env::set_var("OVERRIDE__MAX_CONNECTIONS", "42");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -540,7 +582,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_max_connection_rate() {
|
fn override_field_max_connection_rate() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.max_connection_rate,
|
settings.actix.max_connection_rate,
|
||||||
MaxConnectionRate::Default
|
MaxConnectionRate::Default
|
||||||
@ -554,7 +596,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_max_connection_rate() {
|
fn override_field_with_env_var_max_connection_rate() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.max_connection_rate,
|
settings.actix.max_connection_rate,
|
||||||
MaxConnectionRate::Default
|
MaxConnectionRate::Default
|
||||||
@ -573,7 +615,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_keep_alive() {
|
fn override_field_keep_alive() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
|
assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
|
||||||
Settings::override_field(&mut settings.actix.keep_alive, "42 seconds").unwrap();
|
Settings::override_field(&mut settings.actix.keep_alive, "42 seconds").unwrap();
|
||||||
assert_eq!(settings.actix.keep_alive, KeepAlive::Seconds(42));
|
assert_eq!(settings.actix.keep_alive, KeepAlive::Seconds(42));
|
||||||
@ -581,7 +623,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_keep_alive() {
|
fn override_field_with_env_var_keep_alive() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
|
assert_eq!(settings.actix.keep_alive, KeepAlive::Default);
|
||||||
std::env::set_var("OVERRIDE__KEEP_ALIVE", "42 seconds");
|
std::env::set_var("OVERRIDE__KEEP_ALIVE", "42 seconds");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -594,7 +636,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_client_timeout() {
|
fn override_field_client_timeout() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.client_timeout, Timeout::Default);
|
assert_eq!(settings.actix.client_timeout, Timeout::Default);
|
||||||
Settings::override_field(&mut settings.actix.client_timeout, "42 seconds").unwrap();
|
Settings::override_field(&mut settings.actix.client_timeout, "42 seconds").unwrap();
|
||||||
assert_eq!(settings.actix.client_timeout, Timeout::Seconds(42));
|
assert_eq!(settings.actix.client_timeout, Timeout::Seconds(42));
|
||||||
@ -602,7 +644,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_client_timeout() {
|
fn override_field_with_env_var_client_timeout() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.client_timeout, Timeout::Default);
|
assert_eq!(settings.actix.client_timeout, Timeout::Default);
|
||||||
std::env::set_var("OVERRIDE__CLIENT_TIMEOUT", "42 seconds");
|
std::env::set_var("OVERRIDE__CLIENT_TIMEOUT", "42 seconds");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -615,7 +657,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_client_shutdown() {
|
fn override_field_client_shutdown() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.client_shutdown, Timeout::Default);
|
assert_eq!(settings.actix.client_shutdown, Timeout::Default);
|
||||||
Settings::override_field(&mut settings.actix.client_shutdown, "42 seconds").unwrap();
|
Settings::override_field(&mut settings.actix.client_shutdown, "42 seconds").unwrap();
|
||||||
assert_eq!(settings.actix.client_shutdown, Timeout::Seconds(42));
|
assert_eq!(settings.actix.client_shutdown, Timeout::Seconds(42));
|
||||||
@ -623,7 +665,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_client_shutdown() {
|
fn override_field_with_env_var_client_shutdown() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.client_shutdown, Timeout::Default);
|
assert_eq!(settings.actix.client_shutdown, Timeout::Default);
|
||||||
std::env::set_var("OVERRIDE__CLIENT_SHUTDOWN", "42 seconds");
|
std::env::set_var("OVERRIDE__CLIENT_SHUTDOWN", "42 seconds");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -636,7 +678,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_shutdown_timeout() {
|
fn override_field_shutdown_timeout() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
|
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
|
||||||
Settings::override_field(&mut settings.actix.shutdown_timeout, "42 seconds").unwrap();
|
Settings::override_field(&mut settings.actix.shutdown_timeout, "42 seconds").unwrap();
|
||||||
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
|
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
|
||||||
@ -644,7 +686,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_shutdown_timeout() {
|
fn override_field_with_env_var_shutdown_timeout() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
|
assert_eq!(settings.actix.shutdown_timeout, Timeout::Default);
|
||||||
std::env::set_var("OVERRIDE__SHUTDOWN_TIMEOUT", "42 seconds");
|
std::env::set_var("OVERRIDE__SHUTDOWN_TIMEOUT", "42 seconds");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -655,17 +697,19 @@ mod tests {
|
|||||||
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
|
assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_tls_enabled() {
|
fn override_field_tls_enabled() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(!settings.actix.tls.enabled);
|
assert!(!settings.actix.tls.enabled);
|
||||||
Settings::override_field(&mut settings.actix.tls.enabled, "true").unwrap();
|
Settings::override_field(&mut settings.actix.tls.enabled, "true").unwrap();
|
||||||
assert!(settings.actix.tls.enabled);
|
assert!(settings.actix.tls.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_tls_enabled() {
|
fn override_field_with_env_var_tls_enabled() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert!(!settings.actix.tls.enabled);
|
assert!(!settings.actix.tls.enabled);
|
||||||
std::env::set_var("OVERRIDE__TLS_ENABLED", "true");
|
std::env::set_var("OVERRIDE__TLS_ENABLED", "true");
|
||||||
Settings::override_field_with_env_var(
|
Settings::override_field_with_env_var(
|
||||||
@ -676,9 +720,10 @@ mod tests {
|
|||||||
assert!(settings.actix.tls.enabled);
|
assert!(settings.actix.tls.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_tls_certificate() {
|
fn override_field_tls_certificate() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.tls.certificate,
|
settings.actix.tls.certificate,
|
||||||
Path::new("path/to/cert/cert.pem")
|
Path::new("path/to/cert/cert.pem")
|
||||||
@ -694,9 +739,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_tls_certificate() {
|
fn override_field_with_env_var_tls_certificate() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.tls.certificate,
|
settings.actix.tls.certificate,
|
||||||
Path::new("path/to/cert/cert.pem")
|
Path::new("path/to/cert/cert.pem")
|
||||||
@ -716,9 +762,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_tls_private_key() {
|
fn override_field_tls_private_key() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.tls.private_key,
|
settings.actix.tls.private_key,
|
||||||
Path::new("path/to/cert/key.pem")
|
Path::new("path/to/cert/key.pem")
|
||||||
@ -734,9 +781,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
#[test]
|
#[test]
|
||||||
fn override_field_with_env_var_tls_private_key() {
|
fn override_field_with_env_var_tls_private_key() {
|
||||||
let mut settings = Settings::from_default_template().unwrap();
|
let mut settings = Settings::from_default_template();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings.actix.tls.private_key,
|
settings.actix.tls.private_key,
|
||||||
Path::new("path/to/cert/key.pem")
|
Path::new("path/to/cert/key.pem")
|
||||||
|
@ -43,7 +43,7 @@ impl<'de> de::Deserialize<'de> for Backlog {
|
|||||||
{
|
{
|
||||||
struct BacklogVisitor;
|
struct BacklogVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for BacklogVisitor {
|
impl de::Visitor<'_> for BacklogVisitor {
|
||||||
type Value = Backlog;
|
type Value = Backlog;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -68,7 +68,7 @@ impl<'de> de::Deserialize<'de> for KeepAlive {
|
|||||||
{
|
{
|
||||||
struct KeepAliveVisitor;
|
struct KeepAliveVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for KeepAliveVisitor {
|
impl de::Visitor<'_> for KeepAliveVisitor {
|
||||||
type Value = KeepAlive;
|
type Value = KeepAlive;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnectionRate {
|
|||||||
{
|
{
|
||||||
struct MaxConnectionRateVisitor;
|
struct MaxConnectionRateVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for MaxConnectionRateVisitor {
|
impl de::Visitor<'_> for MaxConnectionRateVisitor {
|
||||||
type Value = MaxConnectionRate;
|
type Value = MaxConnectionRate;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -40,7 +40,7 @@ impl<'de> de::Deserialize<'de> for MaxConnections {
|
|||||||
{
|
{
|
||||||
struct MaxConnectionsVisitor;
|
struct MaxConnectionsVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for MaxConnectionsVisitor {
|
impl de::Visitor<'_> for MaxConnectionsVisitor {
|
||||||
type Value = MaxConnections;
|
type Value = MaxConnections;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -8,12 +8,15 @@ mod max_connections;
|
|||||||
mod mode;
|
mod mode;
|
||||||
mod num_workers;
|
mod num_workers;
|
||||||
mod timeout;
|
mod timeout;
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
pub use self::tls::Tls;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
address::Address, backlog::Backlog, keep_alive::KeepAlive,
|
address::Address, backlog::Backlog, keep_alive::KeepAlive,
|
||||||
max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode,
|
max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode,
|
||||||
num_workers::NumWorkers, timeout::Timeout, tls::Tls,
|
num_workers::NumWorkers, timeout::Timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Settings types for Actix Web.
|
/// Settings types for Actix Web.
|
||||||
@ -26,7 +29,7 @@ pub struct ActixSettings {
|
|||||||
/// Marker of intended deployment environment.
|
/// Marker of intended deployment environment.
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
|
|
||||||
/// True if the [`Compress`](actix_web::middleware::Compress) middleware should be enabled.
|
/// True if the `Compress` middleware should be enabled.
|
||||||
pub enable_compression: bool,
|
pub enable_compression: bool,
|
||||||
|
|
||||||
/// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled.
|
/// True if the [`Logger`](actix_web::middleware::Logger) middleware should be enabled.
|
||||||
@ -57,5 +60,6 @@ pub struct ActixSettings {
|
|||||||
pub shutdown_timeout: Timeout,
|
pub shutdown_timeout: Timeout,
|
||||||
|
|
||||||
/// TLS (HTTPS) configuration.
|
/// TLS (HTTPS) configuration.
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
pub tls: Tls,
|
pub tls: Tls,
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ impl<'de> de::Deserialize<'de> for NumWorkers {
|
|||||||
{
|
{
|
||||||
struct NumWorkersVisitor;
|
struct NumWorkersVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for NumWorkersVisitor {
|
impl de::Visitor<'_> for NumWorkersVisitor {
|
||||||
type Value = NumWorkers;
|
type Value = NumWorkers;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -71,7 +71,7 @@ impl<'de> de::Deserialize<'de> for Timeout {
|
|||||||
{
|
{
|
||||||
struct TimeoutVisitor;
|
struct TimeoutVisitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for TimeoutVisitor {
|
impl de::Visitor<'_> for TimeoutVisitor {
|
||||||
type Value = Timeout;
|
type Value = Timeout;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::AsResult;
|
||||||
|
|
||||||
/// TLS (HTTPS) configuration.
|
/// TLS (HTTPS) configuration.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[doc(alias = "ssl", alias = "https")]
|
#[doc(alias = "ssl", alias = "https")]
|
||||||
pub struct Tls {
|
pub struct Tls {
|
||||||
/// Tru if accepting TLS connections should be enabled.
|
/// True if accepting TLS connections should be enabled.
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
||||||
/// Path to certificate `.pem` file.
|
/// Path to certificate `.pem` file.
|
||||||
@ -16,3 +19,39 @@ pub struct Tls {
|
|||||||
/// Path to private key `.pem` file.
|
/// Path to private key `.pem` file.
|
||||||
pub private_key: PathBuf,
|
pub private_key: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Tls {
|
||||||
|
/// Returns an [`SslAcceptorBuilder`] with the configured settings.
|
||||||
|
///
|
||||||
|
/// The result is often used with [`actix_web::HttpServer::bind_openssl()`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::io;
|
||||||
|
/// use actix_settings::{ApplySettings as _, Settings};
|
||||||
|
/// use actix_web::{get, web, App, HttpServer, Responder};
|
||||||
|
///
|
||||||
|
/// #[actix_web::main]
|
||||||
|
/// async fn main() -> io::Result<()> {
|
||||||
|
/// let settings = Settings::from_default_template();
|
||||||
|
///
|
||||||
|
/// HttpServer::new(|| {
|
||||||
|
/// App::new().route("/", web::to(|| async { "Hello, World!" }))
|
||||||
|
/// })
|
||||||
|
/// .try_apply_settings(&settings)?
|
||||||
|
/// .bind(("127.0.0.1", 8080))?
|
||||||
|
/// .bind_openssl(("127.0.0.1", 8443), settings.actix.tls.get_ssl_acceptor_builder()?)?
|
||||||
|
/// .run()
|
||||||
|
/// .await
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn get_ssl_acceptor_builder(&self) -> AsResult<SslAcceptorBuilder> {
|
||||||
|
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
|
||||||
|
builder.set_certificate_chain_file(&self.certificate)?;
|
||||||
|
builder.set_private_key_file(&self.private_key, SslFiletype::PEM)?;
|
||||||
|
builder.check_private_key()?;
|
||||||
|
|
||||||
|
Ok(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.8.2
|
||||||
|
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.75.
|
||||||
|
|
||||||
## 0.8.1
|
## 0.8.1
|
||||||
|
|
||||||
- Implement `From<Basic>` for `BasicAuth`.
|
- Implement `From<Basic>` for `BasicAuth`.
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-httpauth"
|
name = "actix-web-httpauth"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
|
description = "HTTP authentication schemes for Actix Web"
|
||||||
|
categories = ["web-programming"]
|
||||||
|
keywords = ["http", "web", "framework", "authentication", "security"]
|
||||||
authors = [
|
authors = [
|
||||||
"svartalf <self@svartalf.info>",
|
"svartalf <self@svartalf.info>",
|
||||||
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
"Yuki Okushi <huyuumi.dev@gmail.com>",
|
||||||
]
|
]
|
||||||
description = "HTTP authentication schemes for Actix Web"
|
repository.workspace = true
|
||||||
keywords = ["http", "web", "framework", "authentication", "security"]
|
homepage.workspace = true
|
||||||
homepage = "https://actix.rs"
|
|
||||||
repository = "https://github.com/actix/actix-extras.git"
|
|
||||||
categories = ["web-programming::http-server"]
|
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@ -22,13 +22,18 @@ all-features = true
|
|||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.1", default-features = false }
|
actix-web = { version = "4.1", default-features = false }
|
||||||
|
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
futures-core = "0.3.7"
|
futures-core = "0.3.17"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-cors = "0.6"
|
actix-cors = "0.7"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-web = { version = "4.1", default-features = false, features = ["macros"] }
|
actix-web = { version = "4.1", default-features = false, features = ["macros"] }
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing = "0.1.30"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
> HTTP authentication schemes for [Actix Web](https://actix.rs).
|
> HTTP authentication schemes for [Actix Web](https://actix.rs).
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-httpauth)
|
[](https://crates.io/crates/actix-web-httpauth)
|
||||||
[](https://docs.rs/actix-web-httpauth/0.8.1)
|
[](https://docs.rs/actix-web-httpauth/0.8.2)
|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web-httpauth/0.8.1)
|
[](https://deps.rs/crate/actix-web-httpauth/0.8.2)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
## Documentation & Resources
|
## Documentation & Resources
|
||||||
|
|
||||||
|
@ -1,24 +1,57 @@
|
|||||||
use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpServer};
|
use actix_web::{
|
||||||
use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication};
|
dev::ServiceRequest, error, get, middleware::Logger, App, Error, HttpServer, Responder,
|
||||||
|
};
|
||||||
|
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication};
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
/// Validator that:
|
||||||
|
/// - accepts Bearer auth;
|
||||||
|
/// - returns a custom response for requests without a valid Bearer Authorization header;
|
||||||
|
/// - rejects tokens containing an "x" (for quick testing using command line HTTP clients).
|
||||||
async fn validator(
|
async fn validator(
|
||||||
req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
_credentials: BasicAuth,
|
credentials: Option<BearerAuth>,
|
||||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||||
|
let Some(credentials) = credentials else {
|
||||||
|
return Err((error::ErrorBadRequest("no bearer header"), req));
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("{credentials:?}");
|
||||||
|
|
||||||
|
if credentials.token().contains('x') {
|
||||||
|
return Err((error::ErrorBadRequest("token contains x"), req));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(auth: BearerAuth) -> impl Responder {
|
||||||
|
format!("authenticated for token: {}", auth.token().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy(),
|
||||||
|
)
|
||||||
|
.without_time()
|
||||||
|
.init();
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
let auth = HttpAuthentication::basic(validator);
|
let auth = HttpAuthentication::with_fn(validator);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(middleware::Logger::default())
|
.service(index)
|
||||||
.wrap(auth)
|
.wrap(auth)
|
||||||
.service(web::resource("/").to(|| async { "Test\r\n" }))
|
.wrap(Logger::default().log_target("@"))
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:8080")?
|
.bind("127.0.0.1:8080")?
|
||||||
.workers(1)
|
.workers(2)
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use super::AuthenticationError;
|
|||||||
use crate::headers::www_authenticate::Challenge;
|
use crate::headers::www_authenticate::Challenge;
|
||||||
|
|
||||||
/// Trait implemented for types that provides configuration for the authentication
|
/// Trait implemented for types that provides configuration for the authentication
|
||||||
/// [extractors](super::AuthExtractor).
|
/// [extractors](crate::extractors).
|
||||||
pub trait AuthExtractorConfig {
|
pub trait AuthExtractorConfig {
|
||||||
/// Associated challenge type.
|
/// Associated challenge type.
|
||||||
type Inner: Challenge;
|
type Inner: Challenge;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{convert::From, error::Error, fmt, str};
|
use std::{error::Error, fmt, str};
|
||||||
|
|
||||||
use actix_web::http::header;
|
use actix_web::http::header;
|
||||||
|
|
||||||
|
@ -9,9 +9,10 @@ use crate::headers::authorization::{errors::ParseError, scheme::Scheme};
|
|||||||
|
|
||||||
/// Credentials for `Bearer` authentication scheme, defined in [RFC 6750].
|
/// Credentials for `Bearer` authentication scheme, defined in [RFC 6750].
|
||||||
///
|
///
|
||||||
/// Should be used in combination with [`Authorization`](super::Authorization) header.
|
/// Should be used in combination with [`Authorization`] header.
|
||||||
///
|
///
|
||||||
/// [RFC 6750]: https://tools.ietf.org/html/rfc6750
|
/// [RFC 6750]: https://tools.ietf.org/html/rfc6750
|
||||||
|
/// [`Authorization`]: crate::headers::authorization::Authorization
|
||||||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub struct Bearer {
|
pub struct Bearer {
|
||||||
token: Cow<'static, str>,
|
token: Cow<'static, str>,
|
||||||
|
@ -15,8 +15,10 @@
|
|||||||
//! [Middleware]: self::middleware
|
//! [Middleware]: self::middleware
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![warn(missing_docs)]
|
||||||
#![warn(future_incompatible, missing_docs)]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
|
@ -43,6 +43,55 @@ where
|
|||||||
{
|
{
|
||||||
/// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
|
/// Construct `HttpAuthentication` middleware with the provided auth extractor `T` and
|
||||||
/// validation callback `F`.
|
/// validation callback `F`.
|
||||||
|
///
|
||||||
|
/// This function can be used to implement optional authentication and/or custom responses to
|
||||||
|
/// missing authentication.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ## Required Basic Auth
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||||
|
/// # use actix_web::dev::ServiceRequest;
|
||||||
|
/// async fn validator(
|
||||||
|
/// req: ServiceRequest,
|
||||||
|
/// credentials: BasicAuth,
|
||||||
|
/// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
|
||||||
|
/// eprintln!("{credentials:?}");
|
||||||
|
///
|
||||||
|
/// if credentials.user_id().contains('x') {
|
||||||
|
/// return Err((actix_web::error::ErrorBadRequest("user ID contains x"), req));
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Ok(req)
|
||||||
|
/// }
|
||||||
|
/// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Optional Bearer Auth
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
|
/// # use actix_web::dev::ServiceRequest;
|
||||||
|
/// async fn validator(
|
||||||
|
/// req: ServiceRequest,
|
||||||
|
/// credentials: Option<BearerAuth>,
|
||||||
|
/// ) -> Result<ServiceRequest, (actix_web::Error, ServiceRequest)> {
|
||||||
|
/// let Some(credentials) = credentials else {
|
||||||
|
/// return Err((actix_web::error::ErrorBadRequest("no bearer header"), req));
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// eprintln!("{credentials:?}");
|
||||||
|
///
|
||||||
|
/// if credentials.token().contains('x') {
|
||||||
|
/// return Err((actix_web::error::ErrorBadRequest("token contains x"), req));
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Ok(req)
|
||||||
|
/// }
|
||||||
|
/// # actix_web_httpauth::middleware::HttpAuthentication::with_fn(validator);
|
||||||
|
/// ```
|
||||||
pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
|
pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
|
||||||
HttpAuthentication {
|
HttpAuthentication {
|
||||||
process_fn: Arc::new(process_fn),
|
process_fn: Arc::new(process_fn),
|
||||||
@ -243,7 +292,6 @@ where
|
|||||||
mod tests {
|
mod tests {
|
||||||
use actix_service::into_service;
|
use actix_service::into_service;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
dev::Service,
|
|
||||||
error::{self, ErrorForbidden},
|
error::{self, ErrorForbidden},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
test::TestRequest,
|
test::TestRequest,
|
||||||
|
@ -12,8 +12,8 @@ struct Quoted<'a> {
|
|||||||
state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Quoted<'a> {
|
impl Quoted<'_> {
|
||||||
pub fn new(s: &'a str) -> Quoted<'_> {
|
pub fn new(s: &str) -> Quoted<'_> {
|
||||||
Quoted {
|
Quoted {
|
||||||
inner: s.split('"').peekable(),
|
inner: s.split('"').peekable(),
|
||||||
state: State::YieldStr,
|
state: State::YieldStr,
|
||||||
|
18
actix-ws/CHANGELOG.md
Normal file
18
actix-ws/CHANGELOG.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Ensure TCP connection is properly shut down when session is dropped.
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
- Add `AggregatedMessage[Stream]` types.
|
||||||
|
- Add `MessageStream::max_frame_size()` setter method.
|
||||||
|
- Add `Session::continuation()` method.
|
||||||
|
- The `Session::text()` method now receives an `impl Into<ByteString>`, making broadcasting text messages more efficient.
|
||||||
|
- Remove type parameters from `Session::{text, binary}()` methods, replacing with equivalent `impl Trait` parameters.
|
||||||
|
- Reduce memory usage by `take`-ing (rather than `split`-ing) the encoded buffer when yielding bytes in the response stream.
|
||||||
|
|
||||||
|
## 0.2.5
|
||||||
|
|
||||||
|
- Adopted into @actix org from <https://git.asonix.dog/asonix/actix-actorless-websockets>.
|
33
actix-ws/Cargo.toml
Normal file
33
actix-ws/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "actix-ws"
|
||||||
|
version = "0.3.0"
|
||||||
|
description = "WebSockets for Actix Web, without actors"
|
||||||
|
categories = ["web-programming::websocket"]
|
||||||
|
keywords = ["actix", "web", "websocket", "websockets", "streaming"]
|
||||||
|
authors = [
|
||||||
|
"asonix <asonix@asonix.dog>",
|
||||||
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
|
]
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-codec = "0.5"
|
||||||
|
actix-http = { version = "3", default-features = false, features = ["ws"] }
|
||||||
|
actix-web = { version = "4", default-features = false }
|
||||||
|
bytestring = "1"
|
||||||
|
futures-core = "0.3.17"
|
||||||
|
tokio = { version = "1.24", features = ["sync"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-web = "4.8"
|
||||||
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
|
tokio = { version = "1.24", features = ["sync", "rt", "macros"] }
|
||||||
|
tracing = "0.1.30"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
74
actix-ws/README.md
Normal file
74
actix-ws/README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# `actix-ws`
|
||||||
|
|
||||||
|
> WebSockets for Actix Web, without actors.
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
|
||||||
|
[](https://crates.io/crates/actix-ws)
|
||||||
|
[](https://docs.rs/actix-ws/0.3.0)
|
||||||
|

|
||||||
|

|
||||||
|
<br />
|
||||||
|
[](https://deps.rs/crate/actix-ws/0.3.0)
|
||||||
|
[](https://crates.io/crates/actix-ws)
|
||||||
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Responder};
|
||||||
|
use actix_ws::Message;
|
||||||
|
|
||||||
|
async fn ws(req: HttpRequest, body: web::Payload) -> actix_web::Result<impl Responder> {
|
||||||
|
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
|
||||||
|
|
||||||
|
actix_web::rt::spawn(async move {
|
||||||
|
while let Some(Ok(msg)) = msg_stream.recv().await {
|
||||||
|
match msg {
|
||||||
|
Message::Ping(bytes) => {
|
||||||
|
if session.pong(&bytes).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Text(msg) => println!("Got text: {msg}"),
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = session.close(None).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.route("/ws", web::get().to(ws))
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8080")?
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [API Documentation](https://docs.rs/actix-ws)
|
||||||
|
- [Example Chat Project](https://github.com/actix/examples/tree/master/websockets/chat-actorless)
|
||||||
|
- Minimum Supported Rust Version (MSRV): 1.75
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under either of
|
||||||
|
|
||||||
|
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
69
actix-ws/examples/chat.html
Normal file
69
actix-ws/examples/chat.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Chat</title>
|
||||||
|
<script>
|
||||||
|
function onLoad() {
|
||||||
|
console.log("BOOTING");
|
||||||
|
const socket = new WebSocket("ws://localhost:8080/ws");
|
||||||
|
const input = document.getElementById("chat-input");
|
||||||
|
const logs = document.getElementById("chat-logs");
|
||||||
|
|
||||||
|
if (!input || !logs) {
|
||||||
|
alert("Couldn't find required elements");
|
||||||
|
console.err("Couldn't find required elements");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener(
|
||||||
|
"keyup",
|
||||||
|
(event) => {
|
||||||
|
if (event.isComposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key != "Enter") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.send(input.value);
|
||||||
|
input.value = "";
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
const newNode = document.createElement("li");
|
||||||
|
newNode.textContent = event.data;
|
||||||
|
|
||||||
|
let firstChild = null;
|
||||||
|
for (const n of logs.childNodes.values()) {
|
||||||
|
if (n.nodeType == 1) {
|
||||||
|
firstChild = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChild) {
|
||||||
|
logs.insertBefore(newNode, firstChild);
|
||||||
|
} else {
|
||||||
|
logs.appendChild(newNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
onLoad();
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", onLoad, false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input id="chat-input" type="test" />
|
||||||
|
<ul id="chat-logs"></ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user