1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-04-21 17:46:49 +02:00

Compare commits

...

604 Commits

Author SHA1 Message Date
dependabot[bot]
c04cc19e73
build(deps): bump anyhow from 1.0.97 to 1.0.98 (#526)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.97 to 1.0.98.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.97...1.0.98)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.98
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 15:19:05 +00:00
Rob Ede
6a13b3b182
chore: update deps 2025-04-09 21:52:16 +01:00
dependabot[bot]
d994912ac2
build(deps): bump openssl from 0.10.71 to 0.10.72 (#521)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-05 21:20:00 +00:00
dependabot[bot]
5f6f20cf37
build(deps): bump taiki-e/install-action from 2.49.9 to 2.49.42 (#520)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.49.9 to 2.49.42.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.49.9...v2.49.42)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 16:34:01 +00:00
dependabot[bot]
5145924410
build(deps): bump once_cell from 1.21.1 to 1.21.3 (#519)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.1 to 1.21.3.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.1...v1.21.3)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 16:44:02 +00:00
dependabot[bot]
b20dec36ac
build(deps): bump time from 0.3.39 to 0.3.41 (#515)
Bumps [time](https://github.com/time-rs/time) from 0.3.39 to 0.3.41.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.39...v0.3.41)

---
updated-dependencies:
- dependency-name: time
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 00:37:20 +00:00
dependabot[bot]
f6e45d487b
build(deps): bump reqwest from 0.12.14 to 0.12.15 (#516)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.14 to 0.12.15.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.14...v0.12.15)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 00:37:07 +00:00
dependabot[bot]
c53e198ea7
build(deps): bump redis from 0.29.1 to 0.29.2 (#517)
Bumps [redis](https://github.com/redis-rs/redis-rs) from 0.29.1 to 0.29.2.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.29.1...redis-0.29.2)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 00:36:54 +00:00
dependabot[bot]
4d9984ee76
build(deps): bump log from 0.4.26 to 0.4.27 (#518)
Bumps [log](https://github.com/rust-lang/log) from 0.4.26 to 0.4.27.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.26...0.4.27)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 00:36:08 +00:00
dependabot[bot]
9a08090709
build(deps): bump once_cell from 1.21.0 to 1.21.1 (#511)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.0 to 1.21.1.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 02:04:49 +00:00
dependabot[bot]
7d3348bb29
build(deps): bump reqwest from 0.12.12 to 0.12.14 (#512)
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.12 to 0.12.14.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/v0.12.14/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.12...v0.12.14)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 02:04:41 +00:00
dependabot[bot]
c0fa63af39
build(deps): bump uuid from 1.15.1 to 1.16.0 (#513)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.15.1...v1.16.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 02:04:39 +00:00
Rob Ede
0b5e2b3647
chore: check in lockfile 2025-03-11 03:12:27 +00:00
Rob Ede
b95595b9cd
chore(actix-cors): prepare release 0.7.1 2025-03-11 02:55:33 +00:00
dependabot[bot]
4b3f87e915
build(deps): update redis requirement from 0.28 to 0.29 (#510)
* build(deps): update redis requirement from 0.28 to 0.29

Updates the requirements on [redis](https://github.com/redis-rs/redis-rs) to permit the latest version.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.28.0...redis-0.29.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update deadpool-redis to 0.20

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-03-11 02:38:37 +00:00
dependabot[bot]
144c7f92b9
build(deps): bump taiki-e/install-action from 2.47.32 to 2.49.9 (#505)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.47.32 to 2.49.9.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.47.32...v2.49.9)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-01 21:13:49 +00:00
dependabot[bot]
c71b9dd443
build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (#506)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.3.1...v5.4.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-01 21:13:17 +00:00
dependabot[bot]
282d56e96b
build(deps): bump actions-rust-lang/setup-rust-toolchain from 1.10.1 to 1.11.0 (#507)
* build(deps): bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* build: lower msrv deps

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-03-01 21:01:02 +00:00
dependabot[bot]
d514ad3af5
build(deps): update rand requirement from 0.8 to 0.9 (#498)
* build(deps): update rand requirement from 0.8 to 0.9

Updates the requirements on [rand](https://github.com/rust-random/rand) to permit the latest version.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.0...0.9.0)

---
updated-dependencies:
- dependency-name: rand
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: fix rand upgrade items

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-02-23 18:57:16 +00:00
Rob Ede
109e6a4793
ci: fix test-msrv recipe 2025-02-23 18:53:00 +00:00
Rob Ede
bb0c7f21d9
ci: fix msrv by downgrading native-tls 2025-02-23 18:49:47 +00:00
Rob Ede
3f7a479a76
chore: update redis dependency to 0.28 2025-02-23 18:42:55 +00:00
Rob Ede
fc4b656c3b
chore: address clippy lints 2025-02-23 18:29:35 +00:00
dependabot[bot]
0f35de7da1
build(deps): update derive_more requirement from 1 to 2 (#502)
Updates the requirements on [derive_more](https://github.com/JelteF/derive_more) to permit the latest version.
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v1.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: derive_more
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 17:17:28 +00:00
dependabot[bot]
8294fcc645
build(deps): bump taiki-e/install-action from 2.47.2 to 2.47.32 (#499)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.47.2 to 2.47.32.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.47.2...v2.47.32)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 18:27:04 +00:00
dependabot[bot]
3de6b03711
build(deps): bump codecov/codecov-action from 5.1.2 to 5.3.1 (#500)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...v5.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 18:26:46 +00:00
Wren Turkal
64931189c7
Improve logout example (#496)
The current example for logout is not show a complete example.

I have added a couple lines to handle the case where a logged out user tries to logout again.
2025-01-16 11:56:12 +00:00
Keith Cirkel
265b213123
implement contains_key, update, update_or (#459)
* implement contains_key, update, update_or

* docs(session): update docs for new methods

* docs(session): clarify errors

* test(session): fix doctest

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-01-14 00:51:40 +00:00
Frank Elsinga
695369f02f
feat: added PartialEq to Cors (#486)
* added PartialEq to Cors

* added a changelog entry

* re-ran rustfmt

* removed a subtle bug in the new testcase

* removed a not so subtle bug in the new testcase

* ci: rm public-api-diff job

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2025-01-13 23:22:00 +00:00
laurens-dg
87d9e51112
docs(readme): fix typo and consistent dots in table (#494) 2025-01-10 13:31:31 +00:00
dependabot[bot]
8c11d37dda
build(deps): bump codecov/codecov-action from 5.0.7 to 5.1.2 (#492)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.0.7 to 5.1.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.0.7...v5.1.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-03 16:11:53 +00:00
dependabot[bot]
d97b36652a
build(deps): bump taiki-e/install-action from 2.45.13 to 2.47.2 (#493)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.45.13 to 2.47.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.45.13...v2.47.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-03 16:11:43 +00:00
Alex Wied
98847b9279
fix: ensure TCP connection is properly shut down when Session is dropped (#476)
* Ensure TCP connection is properly shut down when session is dropped

* Update CHANGELOG.md

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-12-29 16:57:56 +00:00
vgwidt
cd1b77134e
docs: fix redis tls feature names (#487)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-12-29 16:54:31 +00:00
Rob Ede
105932706d
chore: address clippy lints 2024-12-29 16:19:36 +00:00
Necdet Arda Etiman
18f94fa8b5
Added actix-jwt-cookies and actix-ws-broadcaster to README (#482)
* Added ctix-jwt-cookies to README

* added actix-ws-broadcaster to Community Crates

---------

Co-authored-by: Necdet Arda Etiman <necdetarda123@gmail.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-12-02 08:31:00 +00:00
dependabot[bot]
66b82f0f30
build(deps): bump actions-rust-lang/setup-rust-toolchain (#478)
Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 08:28:15 +00:00
dependabot[bot]
d67abde5f3
build(deps): bump codecov/codecov-action from 4.6.0 to 5.0.7 (#483)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.6.0 to 5.0.7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.6.0...v5.0.7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 08:28:00 +00:00
Rob Ede
3eafe7f5ce
ci: fix public api diff 2024-12-02 10:21:30 +02:00
Rob Ede
3b5f7ae68c
docs: update readme 2024-12-02 10:21:30 +02:00
dependabot[bot]
036af488fd
build(deps): bump taiki-e/install-action from 2.44.15 to 2.45.13 (#484)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.44.15 to 2.45.13.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.44.15...v2.45.13)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 07:55:04 +00:00
Ross
77406cbb71
Added actix-web-validation to README (#474) 2024-10-13 16:32:52 +00:00
dependabot[bot]
2ede588693
build(deps): update redis requirement from 0.26 to 0.27 (#463)
* build(deps): update redis requirement from 0.26 to 0.27

Updates the requirements on [redis](https://github.com/redis-rs/redis-rs) to permit the latest version.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.26.0...redis-0.27.2)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelogs

* ci: fix doc

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-10-11 13:10:39 +00:00
jwiesler
21680e0ebe
Include cookie expiration and ttl in the identity example (#473) 2024-10-07 22:57:22 +00:00
dependabot[bot]
370f9d3033
build(deps): bump actions-rust-lang/setup-rust-toolchain (#469)
Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 22:27:47 +00:00
dependabot[bot]
8f4fb348b3
build(deps): bump taiki-e/install-action from 2.42.37 to 2.44.15 (#471)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.37 to 2.44.15.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.37...v2.44.15)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 22:27:34 +00:00
dependabot[bot]
ff4b173716
build(deps): bump codecov/codecov-action from 4.5.0 to 4.6.0 (#470)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 22:27:21 +00:00
William Desportes
49aacfce9f
Update README.md (#466) 2024-09-28 00:45:02 +00:00
Rob Ede
dd20ebb6cb
chore: allow missing docs on test mod 2024-09-12 15:10:28 -04:00
Rob Ede
a3211b73d3
chore(actix-identity): prepare release 0.8.0 2024-09-12 15:07:36 -04:00
Rob Ede
a89d3a58bc
refactor: fix nightly warning 2024-09-12 15:06:22 -04:00
Rob Ede
3c640ec120
chore: fix rand optionality 2024-09-12 14:56:33 -04:00
Rob Ede
26ccf8b200
chore: fix feature gate 2024-09-12 14:55:04 -04:00
Rob Ede
dd1421f1a0
chore(actix-session): prepare release 0.10.1 2024-09-12 14:50:48 -04:00
dependabot[bot]
4eb779be77
build(deps): bump taiki-e/install-action from 2.42.14 to 2.42.37 (#460)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.42.14 to 2.42.37.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.42.14...v2.42.37)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-02 03:02:50 +00:00
John Vandenberg
48646d1bd3
build(deps): update derive_more to v1.0 (#458)
* build(deps): update derive_more to v1.0

* chore: remove overspecified deps

* chore: use from the derive module

* chore: restore unrelated version reqs

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-08-18 14:21:56 +00:00
Rob Ede
275675e1c2
docs(settings): fix doc test 2024-08-07 01:37:53 +01:00
Rob Ede
50d2fee4e2
chore(actix-settings): prepare release 0.8.0 2024-08-07 01:32:49 +01:00
Rob Ede
0c0d13be12
docs: update session dependants changelogs 2024-08-07 01:05:57 +01:00
Rob Ede
d10b71fe06
docs(session): doc adding features using cargo add 2024-08-07 01:03:40 +01:00
Rob Ede
f2339971cd
chore(actix-session): prepare release 0.10.0 2024-08-07 00:57:54 +01:00
dependabot[bot]
517e72f248
build(deps): update reqwest requirement from 0.11 to 0.12 (#454)
* build(deps): update reqwest requirement from 0.11 to 0.12

Updates the requirements on [reqwest](https://github.com/seanmonstar/reqwest) to permit the latest version.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.0...v0.12.5)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: use reqwest::StatusCode

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-08-06 14:19:02 +00:00
João Fernandes
504e89403b
feature(session): add deadpool-redis compatibility (#381)
* Add compatibility of `deadpool-redis` for the storage `redis_rs`.

* Keep up-to-date the `actix-redis` version.

* Format the project issued by command `cargo +nightly fmt`.

* Add `deadpool-redis` into the documentation and tests.

* Update CHANGES.md.

* Update the documentation of `Deadpool Redis` section on `redis_rs`.

* Replace `no_run` with `ignore` attribute on "Deadpool Redis" example to skip the doc tests failure.

* Rollback the renaming `redis::cmd` to `cmd` for better reading and avoid shadowing, fix the wrong return type on builder function comment.

* Format the project issued by command `cargo +nightly fmt`.

* Format.

* Fix feature naming from the last merge.

* Fix feature missing from the last merge.

* Format the project issued by command `cargo +nightly fmt`.

* Re-import `cookie-session` feature. (Maybe was removed accidentally from the last merge?)

* tmp

* chore: bump deadpool-redis to 0.16

* chore: fixup rest of redis code for pool

* fix: add missing cfg guard

* docs: fix pool docs

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-08-06 13:47:49 +00:00
João Fernandes
31b1dc5aa8
feature(settings): add TLS (#380)
* Complete the missing TLS feature.

* Make the `cfg` attributes more clear.

* Format the project issued by command `cargo +nightly fmt`.

* Small changes on cargo file.

* Update CHANGES.md.

* Add documentation for `Tls::get_ssl_acceptor_builder()` and remove unused imports.

* Add the `cfg` macro with required feature on `TLS` tests.

* Update actix-settings/src/settings/tls.rs

Co-authored-by: Rob Ede <robjtede@icloud.com>

* Copy the workflow steps related to OpenSSL for windows from [actix-web workflow](a7375b6876/.github/workflows/ci.yml (L38-L45)).

* ci: install openssl 1.1.1

* Replaced `apply_settings` with `try_apply_settings` for a better error handling.

* Updated the example.

* Add `OpenSSL` error.

* Restrict `OpenSSL` error only for `tls` feature.

* Rename feature `tls` to `openssl`.

* Add doc feature `broken_intra_doc_links` to `get_ssl_acceptor_builder` function.

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-08-03 08:59:13 +00:00
dependabot[bot]
d7daf441d1
build(deps): bump taiki-e/install-action from 2.41.7 to 2.42.14 (#453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-03 09:51:46 +01:00
dependabot[bot]
2de4b1886c
build(deps): update redis requirement from 0.25 to 0.26 (#451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-07-30 12:05:02 +01:00
Rob Ede
caa5dbc5b3
chore: fix re-exports 2024-07-29 22:00:31 +01:00
Rob Ede
c259e715f8
chore: fix just doc 2024-07-29 21:59:53 +01:00
edgerunnergit
d8a86751f0
Make generate_session_key() public (#449)
* make generate_session_key() public and change impl to use DistString

* add changelong and use nightly fmt

* Add better support for receiving larger payloads (#430)

* Add better support for receiving larger payloads

This change enables the maximum frame size to be configured when receiving websocket frames. It also
adds a new stream time that aggregates continuation frames together into their proper collected
representation. It provides no mechanism yet for sending continuations.

* actix-ws: Add continuation & size config to changelog

* actix-ws: Add Debug, Eq to AggregatedMessage

* actix-ws: Add a configurable maximum size to aggregated continuations

* refactor: move aggregate types to own module

* test: fix chat example

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>

* docs(ws): update readme

* chore(actix-ws): prepare release 0.3.0

* chore(ws): remove unused dev dep

* chore: expose generate_session_key

* chore: fix import

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: asonix <asonix@asonix.dog>
2024-07-29 20:53:18 +00:00
Rob Ede
cac93d2bc7
chore(ws): remove unused dev dep 2024-07-20 07:32:11 +01:00
Rob Ede
95f4e0f692
chore(actix-ws): prepare release 0.3.0 2024-07-20 07:24:20 +01:00
Rob Ede
24f3985eab
docs(ws): update readme 2024-07-20 07:23:21 +01:00
asonix
b0d2947a4a
Add better support for receiving larger payloads (#430)
* Add better support for receiving larger payloads

This change enables the maximum frame size to be configured when receiving websocket frames. It also
adds a new stream time that aggregates continuation frames together into their proper collected
representation. It provides no mechanism yet for sending continuations.

* actix-ws: Add continuation & size config to changelog

* actix-ws: Add Debug, Eq to AggregatedMessage

* actix-ws: Add a configurable maximum size to aggregated continuations

* refactor: move aggregate types to own module

* test: fix chat example

* docs: update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-07-20 06:09:30 +00:00
Rob Ede
0802eff40d
chore: fix fmt recipe 2024-07-20 07:11:31 +01:00
dependabot[bot]
2a6a36af23
build(deps): update prost requirement from 0.12 to 0.13 (#450)
* build(deps): update prost requirement from 0.12 to 0.13

Updates the requirements on [prost](https://github.com/tokio-rs/prost) to permit the latest version.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Changelog](https://github.com/tokio-rs/prost/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/prost/compare/v0.12.0...v0.13.1)

---
updated-dependencies:
- dependency-name: prost
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelog

* chore(actix-protobuf): prepare release 0.11.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-07-20 05:16:26 +00:00
dependabot[bot]
3ebdc6192c
build(deps): bump codecov/codecov-action from 4.4.1 to 4.5.0 (#446)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.4.1 to 4.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.4.1...v4.5.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 17:32:54 +00:00
dependabot[bot]
87cf947a45
build(deps): bump taiki-e/cache-cargo-install-action from 2.0.0 to 2.0.1 (#447)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 17:32:50 +00:00
dependabot[bot]
f063bec5ba
build(deps): bump taiki-e/install-action from 2.38.0 to 2.41.7 (#448)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.38.0 to 2.41.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.38.0...v2.41.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 17:32:48 +00:00
Rob Ede
45e9e00285
ci: fix coverage job 2024-06-20 03:10:20 +01:00
Rob Ede
6934db623b
ci: fix coverage job 2024-06-20 03:06:57 +01:00
Rob Ede
1a658a98e1
ci: fix coverage 2024-06-20 03:04:20 +01:00
Rob Ede
2a092a19a8
ci: coverage via just 2024-06-20 02:57:44 +01:00
Rob Ede
032aeb6fdb
chore: fix nightly warning 2024-06-20 02:28:58 +01:00
Rob Ede
52e58610e4
chore: share repo and website 2024-06-20 02:18:37 +01:00
Rob Ede
023158cfa8
chore: move lints to manifest 2024-06-20 02:14:35 +01:00
Rob Ede
14c605fae2
docs: indicative mood 2024-06-20 02:10:19 +01:00
Rob Ede
d94c023bf9
build: group recipes 2024-06-20 01:58:25 +01:00
Rob Ede
7e21fd753e
docs: clean up ws examples 2024-06-20 01:55:14 +01:00
Rob Ede
e7ee2a06ab
docs: split chat example 2024-06-20 01:37:37 +01:00
Rob Ede
8aa2c959c4
chore(actix-web-httpauth): prepare release 0.8.2 2024-06-11 04:02:32 +01:00
Rob Ede
2f1d1daee8
ci: fix doctest job 2024-06-11 03:56:49 +01:00
Rob Ede
d15572b501
ci: remove doc upload workflow 2024-06-11 03:56:08 +01:00
Rob Ede
b9e47d61c3
docs(httpauth): add HttpAuthentication::with_fn examples 2024-06-11 03:55:41 +01:00
Rob Ede
515a727ca3
docs(httpauth): rework example 2024-06-11 03:52:45 +01:00
Rob Ede
20234ec555
ci: fail on doc error 2024-06-11 03:52:15 +01:00
Rob Ede
e4bb5ed355
build: group recipes 2024-06-11 03:52:02 +01:00
dependabot[bot]
5368569d00
build(deps): bump actions-rust-lang/setup-rust-toolchain (#442)
Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-09 23:00:01 +00:00
dependabot[bot]
21b9408a23
build(deps): bump taiki-e/install-action from 2.34.1 to 2.38.0 (#443)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.34.1 to 2.38.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.34.1...v2.38.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-09 22:59:50 +00:00
Rob Ede
5879740322
ci: monthly GHA updates 2024-06-09 23:40:37 +01:00
Rob Ede
4adc9f8884
docs(session): use standard docs 2024-06-09 23:38:33 +01:00
dependabot[bot]
abf75eeb06
build(deps): update redis requirement from 0.24 to 0.25 (#412)
* build(deps): update redis requirement from 0.24 to 0.25

Updates the requirements on [redis](https://github.com/redis-rs/redis-rs) to permit the latest version.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.24.0...redis-0.25.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* refactor(session): rename TLS features

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 22:29:13 +00:00
Rob Ede
433c926503
chore: address clippy lints 2024-06-09 23:08:20 +01:00
Rob Ede
8ebb12b75a
refactor: use tracing in session examples 2024-06-09 20:34:09 +01:00
zbigniewzolnierowicz
931c4eea4d
feat(session): add rustls (actix#342) (#402)
* feat(session): add rustls feature (actix#342)

* docs(session): fix weird grammar

* docs: update crate docs

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 19:19:29 +00:00
asonix
8195484415
actix-ws: take the encoded buffer when yielding rather than split it (#435)
* actix-ws: take the encoded buffer when yielding rather than split it

* actix-ws: add memory reduction to changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-06-09 05:06:24 +00:00
Rob Ede
3ae4ef2706
docs: remove unnecessary code block annotations 2024-06-09 05:18:43 +01:00
dependabot[bot]
65c698cd7f
build(deps): bump taiki-e/install-action from 2.33.34 to 2.34.1 (#441)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.34 to 2.34.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.34...v2.34.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 17:38:33 +00:00
dependabot[bot]
f2ef72d056
build(deps): bump taiki-e/install-action from 2.33.26 to 2.33.34 (#440)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.26 to 2.33.34.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.26...v2.33.34)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 17:09:28 +00:00
dependabot[bot]
e4ee236341
build(deps): bump taiki-e/install-action from 2.33.22 to 2.33.26 (#436)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.22 to 2.33.26.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.22...v2.33.26)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 17:14:02 +00:00
dependabot[bot]
41ae57d414
build(deps): bump JamesIves/github-pages-deploy-action (#437)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.0...v4.6.1)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 17:13:49 +00:00
dependabot[bot]
1b82024499
build(deps): bump codecov/codecov-action from 4.3.1 to 4.4.1 (#438)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.1 to 4.4.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.3.1...v4.4.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 17:13:30 +00:00
asonix
6b04450703
Enable sending Continuations from actix-ws (#431)
* Enable sending continuations from an actix-ws Session

* actix-ws: Allow sending continuations from Session

* Convert ignored doctests to no_run doctests

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-05-13 16:49:34 +00:00
dependabot[bot]
c0c7588a57
build(deps): bump taiki-e/install-action from 2.33.17 to 2.33.22 (#432)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 17:26:31 +01:00
dependabot[bot]
b918084a53
build(deps): bump codecov/codecov-action from 4.3.0 to 4.3.1 (#428)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 19:39:46 +00:00
dependabot[bot]
b762b41360
build(deps): bump taiki-e/install-action from 2.33.9 to 2.33.17 (#429)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.33.9 to 2.33.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.33.9...v2.33.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 19:39:34 +00:00
dependabot[bot]
da53492c8c
build(deps): bump codecov/codecov-action from 4.2.0 to 4.3.0 (#421)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 03:24:33 +01:00
dependabot[bot]
2c81bc093b
build(deps): bump taiki-e/cache-cargo-install-action from 1.3.0 to 2.0.0 (#426)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 1.3.0 to 2.0.0.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v1.3.0...v2.0.0)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 03:23:33 +01:00
dependabot[bot]
a2ef65715b
build(deps): bump taiki-e/install-action from 2.32.9 to 2.33.9 (#425)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.32.9 to 2.33.9.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.32.9...v2.33.9)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 03:23:22 +01:00
dependabot[bot]
9beb348d45
build(deps): bump JamesIves/github-pages-deploy-action (#423)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 03:23:14 +01:00
dependabot[bot]
66544952b6
build(deps): bump codecov/codecov-action from 4.1.1 to 4.2.0 (#419)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.1 to 4.2.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.1.1...v4.2.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 10:13:54 +00:00
dependabot[bot]
9ddb95b74a
build(deps): bump taiki-e/install-action from 2.32.1 to 2.32.9 (#420)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.32.1 to 2.32.9.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.32.1...v2.32.9)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 10:13:40 +00:00
dependabot[bot]
f450e3fb85
build(deps): bump taiki-e/install-action from 2.29.7 to 2.32.1 (#417)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.29.7 to 2.32.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.29.7...v2.32.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 16:13:48 +00:00
dependabot[bot]
31951dcc9b
build(deps): bump codecov/codecov-action from 4.1.0 to 4.1.1 (#418)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-01 16:13:30 +00:00
dependabot[bot]
dfc6fe1986
build(deps): bump taiki-e/install-action from 2.29.0 to 2.29.7 (#414)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 17:52:13 +00:00
dependabot[bot]
122fba0580
build(deps): bump taiki-e/install-action from 2.28.11 to 2.29.0 (#413)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 15:04:23 +00:00
Rob Ede
f250348e57
chore: remove actix-redis crate (#408) 2024-03-12 23:30:06 +00:00
dependabot[bot]
e6f99e915d
build(deps): bump taiki-e/install-action from 2.28.1 to 2.28.11 (#411)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.28.1 to 2.28.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.28.1...v2.28.11)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:28:29 +00:00
dependabot[bot]
9d68074bf1
build(deps): bump taiki-e/install-action from 2.27.10 to 2.28.1 (#410)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.27.10 to 2.28.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.27.10...v2.28.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 16:06:35 +00:00
dependabot[bot]
bbb4ed047c
build(deps): bump codecov/codecov-action from 4.0.2 to 4.1.0 (#409)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.0.2 to 4.1.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.0.2...v4.1.0)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 16:06:19 +00:00
Rob Ede
39291c86b7
test: fix doc test 2024-03-02 21:46:36 +00:00
Rob Ede
db2193b8c5
chore: bump futures-* deps 2024-03-02 21:29:40 +00:00
dependabot[bot]
f0c33a970f
build(deps): bump taiki-e/install-action from 2.27.2 to 2.27.10 (#405)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.27.2 to 2.27.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.27.2...v2.27.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 18:24:02 +00:00
Rob Ede
74c8545363
chore(actix-identity): prepare release 0.7.1 2024-03-02 18:32:58 +00:00
dependabot[bot]
9112cf9f23
build(deps): bump codecov/codecov-action from 4.0.1 to 4.0.2 (#406)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 18:21:19 +00:00
Rob Ede
563d6e0b20
docs(session): add session_ttl aliases 2024-02-19 16:34:28 +00:00
dependabot[bot]
a71c7f6a90
build(deps): bump taiki-e/install-action from 2.26.8 to 2.27.2 (#404)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.26.8 to 2.27.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.26.8...v2.27.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 16:24:20 +00:00
Rob Ede
a5f5e31a82
chore: remove redundant imports 2024-02-19 16:18:17 +00:00
Dany Gagnon
5414e2655b
Add the ability to change the keys (#348)
* feat: add the ability to change the session key store in redis

* feat: change everywhere the constants are used

* refactor: add formatting with cargo fmt

---------

Co-authored-by: Dany Gagnon <danygagnon@Danys-MacBook-Pro.local>
2024-02-15 10:14:17 +00:00
John Vandenberg
daffc24245
Misc tidy up (#400) 2024-02-14 01:19:29 +00:00
dependabot[bot]
2e0cbb8bbb
build(deps): bump codecov/codecov-action from 3 to 4 (#397)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-02-05 16:25:37 +00:00
dependabot[bot]
7fe13e142e
build(deps): bump taiki-e/install-action from 2.26.7 to 2.26.13 (#396)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 16:10:26 +00:00
Marvin
8ddbf26cc1
doc(actix-cors): update MSRV (#395) 2024-01-31 16:18:13 +00:00
dependabot[bot]
b9769edca1
build(deps): bump taiki-e/install-action from 2.25.10 to 2.26.7 (#393)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.25.10 to 2.26.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.25.10...v2.26.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 17:08:12 +00:00
axelfaure
e3027549c5
Add a reference to Apistos to README.md (#390)
* Add a reference to Apistos to README.md

Apistos is a new crate for OpenAPI v3 documentation

* Actix Web

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2024-01-26 16:18:45 +00:00
dependabot[bot]
1934457e48
build(deps): bump taiki-e/install-action from 2.25.2 to 2.25.10 (#389)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.25.2 to 2.25.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.25.2...v2.25.10)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 16:06:21 +00:00
dependabot[bot]
254d4084a9
build(deps): update env_logger requirement from 0.10 to 0.11 (#388)
Updates the requirements on [env_logger](https://github.com/rust-cli/env_logger) to permit the latest version.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 15:56:30 +00:00
dependabot[bot]
a9e615bac4
build(deps): bump taiki-e/install-action from 2.24.1 to 2.25.2 (#385)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.24.1 to 2.25.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.24.1...v2.25.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 16:02:32 +00:00
dependabot[bot]
1e70159e08
build(deps): bump actions-rust-lang/setup-rust-toolchain (#384)
Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.6.0 to 1.8.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.6.0...v1.8.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 16:02:19 +00:00
Rob Ede
89bf63e1ef
chore(actix-identity): prepare release 0.7.0 2024-01-11 04:44:02 +00:00
Rob Ede
8b4e8ea34e
chore(actix-session): prepare release 0.9.0 2024-01-11 04:27:56 +00:00
dependabot[bot]
5ceb3c72cd
build(deps): bump taiki-e/install-action from 2.23.7 to 2.24.1 (#382)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.23.7 to 2.24.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.23.7...v2.24.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-08 15:36:45 +00:00
Rob Ede
c62b271d9a
chore(actix-cors): prepare release 0.7.0 2024-01-06 21:13:26 +00:00
Rob Ede
320cbebc7e
chore: fmt markdowns 2024-01-06 21:08:09 +00:00
Rob Ede
0c859a96c8
docs(cors): use cargo-rdme 2024-01-06 21:02:36 +00:00
Rob Ede
d55fc6d7f5
fix!(cors): default block_on_origin_mismatch to false (#379) 2024-01-06 20:40:44 +00:00
Rob Ede
e2bf504055
feat(session): use real async traits (#365) 2024-01-04 04:10:46 +00:00
Rob Ede
77b8dcdf59
chore: clippy 2024-01-04 04:05:56 +00:00
Rob Ede
b694c9317a
mark Cors builder as must_use 2024-01-04 03:42:10 +00:00
dependabot[bot]
57eaad2ffe
build(deps): bump taiki-e/install-action from 2.23.1 to 2.23.7 (#377)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.23.1 to 2.23.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.23.1...v2.23.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 15:42:56 +00:00
Rob Ede
0cb0e28208
ci: combine install steps 2023-12-26 04:05:35 +00:00
Rob Ede
8049a75d9f
ci: use cargo-ci-clean-cache 2023-12-26 03:58:01 +00:00
dependabot[bot]
0dd810e213
build(deps): bump taiki-e/install-action from 2.22.6 to 2.23.1 (#376)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.22.6 to 2.23.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.22.6...v2.23.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 15:45:53 +00:00
dependabot[bot]
5bf831c27b
build(deps): bump taiki-e/install-action from 2.22.0 to 2.22.6 (#375)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.22.0 to 2.22.6.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.22.0...v2.22.6)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 16:25:03 +00:00
dependabot[bot]
a7e3503ad1
build(deps): bump taiki-e/install-action from 2.21.26 to 2.22.0 (#374)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.26 to 2.22.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.26...v2.22.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 16:53:08 +00:00
dependabot[bot]
819f45106f
build(deps): update redis requirement from 0.23 to 0.24 (#373)
Updates the requirements on [redis](https://github.com/redis-rs/redis-rs) to permit the latest version.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.23.0...redis-0.24.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 16:52:47 +00:00
Rob Ede
4f76943423
chore(actix-cors): prepare release 0.6.5 2023-12-06 17:09:20 -05:00
yhx-12243
2f30fd71a9
fix(cors): The item in "Vary" header should be "Access-Control-Request-Private-Network". (#369)
* fix(cors): vary should be "Access-Control-Request-Private-Network"

* docs(cors): update the changelog
2023-12-06 13:52:22 +00:00
dependabot[bot]
5198c68c06
Bump JamesIves/github-pages-deploy-action from 4.4.3 to 4.5.0 (#372)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.3 to 4.5.0.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.3...v4.5.0)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 16:06:33 +00:00
dependabot[bot]
2d4cf5f422
Bump taiki-e/install-action from 2.21.20 to 2.21.26 (#370)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.20 to 2.21.26.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.20...v2.21.26)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 16:05:21 +00:00
dependabot[bot]
53dce5c34f
Bump actions-rust-lang/setup-rust-toolchain from 1.5.0 to 1.6.0 (#371)
Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions-rust-lang/setup-rust-toolchain/compare/v1.5.0...v1.6.0)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 16:03:40 +00:00
dependabot[bot]
8de686a711
Bump taiki-e/install-action from 2.21.17 to 2.21.20 (#368)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.17 to 2.21.20.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.17...v2.21.20)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-27 15:39:43 +00:00
dependabot[bot]
50fd71d496
Bump taiki-e/install-action from 2.21.11 to 2.21.17 (#367)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.11 to 2.21.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.11...v2.21.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 15:59:06 +00:00
Phillip Wenig
3c5478966f
add actix-telepathy to community crate list (#364)
Co-authored-by: Phillip W <info@pwenig.de>
2023-11-15 20:51:47 +00:00
dependabot[bot]
1e18d62852
Bump taiki-e/install-action from 2.21.7 to 2.21.11 (#363)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.7 to 2.21.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.7...v2.21.11)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 16:05:05 +00:00
Rob Ede
7aeeb9a445
ci: use giraffate/clippy-action for clippy 2023-11-10 14:06:23 +00:00
Rob Ede
5b2085f414
ci: fix clippy lint 2023-11-10 13:57:49 +00:00
Rob Ede
6afca96ddf
ci: disallow dbg macro 2023-11-10 13:55:20 +00:00
dependabot[bot]
a48c2926f9
Bump taiki-e/install-action from 2.21.3 to 2.21.7 (#362)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.21.3 to 2.21.7.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.21.3...v2.21.7)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-06 18:31:30 +00:00
Rob Ede
6d0ab96dfd
adopt actix-ws crate (#361) 2023-11-03 22:49:18 +00:00
Rob Ede
a593a8dc90
fix readme version 2023-11-03 20:39:32 +00:00
Rob Ede
4d79d263ef
update readme versions 2023-11-03 20:37:18 +00:00
Rob Ede
31540f8e4b
chore(actix-settings): prepare release 0.7.1 2023-11-03 20:30:00 +00:00
Rob Ede
11046d7663
chore(actix-settings): prepare release 0.7.0 2023-11-03 20:24:04 +00:00
Rob Ede
73b2aac6d6
make Settings::from_default_template infallible 2023-11-03 20:23:55 +00:00
Heki
76d9313171
Allow ActixSettings to be applied to HttpServer in actix-settings (#321)
* allow other settings objects to be applied

* update changelog

* update changelog

---------

Co-authored-by: LinuxHeki <linuxheki@gmail.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-11-03 20:16:31 +00:00
Rob Ede
373a89a978
fix warnings on doc upload workflow 2023-11-03 20:02:50 +00:00
Rob Ede
61f16c609a
fix doc warnings 2023-11-03 20:00:56 +00:00
Rob Ede
ecd2016c09
use standard attributes for all crates 2023-11-03 19:46:12 +00:00
Rob Ede
471f07e27f
improve actix-cors docs 2023-11-03 19:44:12 +00:00
dependabot[bot]
077c6edced
Bump taiki-e/install-action from 2.20.15 to 2.21.3 (#360)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.20.15 to 2.21.3.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.20.15...v2.21.3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 15:54:04 +00:00
Kunal Singh
fad631c448
fix: typo in actix-session cargo.toml file (#356) 2023-10-29 22:37:57 +00:00
dependabot[bot]
20f72cab3e
Bump taiki-e/install-action from 2.20.3 to 2.20.15 (#355)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.20.3 to 2.20.15.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.20.3...v2.20.15)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-23 16:07:54 +00:00
dependabot[bot]
4bad825456
Bump taiki-e/cache-cargo-install-action from 1.2.2 to 1.3.0 (#353)
Bumps [taiki-e/cache-cargo-install-action](https://github.com/taiki-e/cache-cargo-install-action) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/taiki-e/cache-cargo-install-action/releases)
- [Changelog](https://github.com/taiki-e/cache-cargo-install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/cache-cargo-install-action/compare/v1.2.2...v1.3.0)

---
updated-dependencies:
- dependency-name: taiki-e/cache-cargo-install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 19:00:57 +00:00
dependabot[bot]
cb3eba93cc
Bump taiki-e/install-action from 2.20.2 to 2.20.3 (#352)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.20.2 to 2.20.3.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.20.2...v2.20.3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 15:43:04 +00:00
Rob Ede
9d993c6c73
ci: reduce dependabot rate 2023-10-13 17:42:47 +02:00
dependabot[bot]
4761826616
Bump taiki-e/install-action from 2.20.1 to 2.20.2 (#351)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.20.1 to 2.20.2.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.20.1...v2.20.2)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-12 08:55:19 +00:00
dependabot[bot]
ec340670a8
Bump taiki-e/install-action from 2.19.4 to 2.20.1 (#350)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.19.4 to 2.20.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.19.4...v2.20.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 16:10:46 +00:00
dependabot[bot]
3a7834c3ba
Bump taiki-e/install-action from 2.19.3 to 2.19.4 (#349)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-05 17:01:41 +01:00
dependabot[bot]
7db43782ce
Bump taiki-e/install-action from 2.19.1 to 2.19.3 (#347)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.19.1 to 2.19.3.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.19.1...v2.19.3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 15:56:59 +00:00
dependabot[bot]
1ee1afb2a6
Bump taiki-e/install-action from 2.18.17 to 2.19.1 (#345)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.17 to 2.19.1.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.17...v2.19.1)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-28 15:49:25 +00:00
dependabot[bot]
9e4754bbfa
Bump taiki-e/install-action from 2.18.16 to 2.18.17 (#344)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.16 to 2.18.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.16...v2.18.17)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-27 15:27:25 +00:00
dependabot[bot]
cd3e5f9772
Bump taiki-e/install-action from 2.18.15 to 2.18.16 (#341)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.15 to 2.18.16.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.15...v2.18.16)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:24:50 +00:00
dependabot[bot]
45ee50f9cb
Bump taiki-e/install-action from 2.18.14 to 2.18.15 (#340)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.14 to 2.18.15.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.14...v2.18.15)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 16:46:27 +00:00
dependabot[bot]
1d6ef8938f
Bump taiki-e/install-action from 2.18.13 to 2.18.14 (#339)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.13 to 2.18.14.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.13...v2.18.14)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-21 16:12:18 +00:00
dependabot[bot]
316c0d238d
Bump taiki-e/install-action from 2.18.11 to 2.18.13 (#338)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:20:41 +01:00
dependabot[bot]
5baa3c3d95
Bump taiki-e/cache-cargo-install-action from 1.2.1 to 1.2.2 (#337)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:20:24 +01:00
Rob Ede
2dea1f2748
docs: update dep shields 2023-09-16 07:52:18 +01:00
Rob Ede
09ff35bd2d
chore: fix tests for settings 2023-09-16 03:44:47 +01:00
Rob Ede
6caf37cedd
chore(actix-limitation): prepare release 0.5.1 2023-09-16 03:36:29 +01:00
Rob Ede
bafd8179ff
chore: fix actix web feature specs 2023-09-16 03:35:56 +01:00
Rob Ede
3fad53211a
chore(actix-limitation): prepare release 0.5.0 2023-09-16 03:29:58 +01:00
Rob Ede
9a7113028e
chore(actix-identity): prepare release 0.6.0 2023-09-16 03:29:31 +01:00
Rob Ede
9e31f5b306
chore(actix-session): prepare release 0.8.0 2023-09-16 03:28:31 +01:00
Rob Ede
94f99e4843
chore(actix-web-httpauth): prepare release 0.8.1 2023-09-16 03:14:40 +01:00
Rob Ede
600dda5ef3
chore: correct futures-util dep specs 2023-09-16 03:14:04 +01:00
Rob Ede
2a074ddf18
chore(actix-redis): prepare release 0.13.0 2023-09-16 03:07:34 +01:00
Rob Ede
9fc34a9c48
chore(actix-protobuf): prepare release 0.10.0 2023-09-16 03:07:01 +01:00
dependabot[bot]
f942d8a191
Update redis-async requirement from 0.14 to 0.16 (#336)
* Update redis-async requirement from 0.14 to 0.16

Updates the requirements on [redis-async](https://github.com/benashford/redis-async-rs) to permit the latest version.
- [Commits](https://github.com/benashford/redis-async-rs/commits)

---
updated-dependencies:
- dependency-name: redis-async
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelog

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-09-16 00:33:09 +00:00
dependabot[bot]
b737452294
Update redis requirement from 0.22 to 0.23 (#334)
* Update redis requirement from 0.22 to 0.23

Updates the requirements on [redis](https://github.com/redis-rs/redis-rs) to permit the latest version.
- [Release notes](https://github.com/redis-rs/redis-rs/releases)
- [Commits](https://github.com/redis-rs/redis-rs/compare/redis-0.22.0...redis-0.23.3)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelog

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-09-16 00:30:20 +00:00
Max Karou
55ace79d64
Implement From<Basic> for BasicAuth (#327)
* implement `From<Basic>` for `BasicAuth`

* update changelog

---------

Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-09-16 00:27:50 +00:00
dependabot[bot]
c029287801
Update prost requirement from 0.11 to 0.12 (#333)
* Update prost requirement from 0.11 to 0.12

Updates the requirements on [prost](https://github.com/tokio-rs/prost) to permit the latest version.
- [Release notes](https://github.com/tokio-rs/prost/releases)
- [Commits](https://github.com/tokio-rs/prost/compare/prost-build-0.11.1...v0.12.1)

---
updated-dependencies:
- dependency-name: prost
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: update changelog

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-09-16 00:25:08 +00:00
Rob Ede
0d27e3a65a
feat(settings): impl Error for Error 2023-09-16 01:19:16 +01:00
dependabot[bot]
257871ca7a
Update toml requirement from 0.5 to 0.8 (#335)
* Update toml requirement from 0.5 to 0.8

Updates the requirements on [toml](https://github.com/toml-rs/toml) to permit the latest version.
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.5.0...toml-v0.8.0)

---
updated-dependencies:
- dependency-name: toml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs(settings): update changelog

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-09-15 23:59:19 +00:00
dependabot[bot]
d921417726
Bump JamesIves/github-pages-deploy-action from 3.7.1 to 4.4.3 (#330)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 3.7.1 to 4.4.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/3.7.1...v4.4.3)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 23:54:21 +00:00
dependabot[bot]
70b46280ed
Bump actions/checkout from 3 to 4 (#332)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 23:54:02 +00:00
dependabot[bot]
55d70231cc
Bump taiki-e/install-action from 2.18.9 to 2.18.11 (#329)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.18.9 to 2.18.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.18.9...v2.18.11)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-15 23:43:34 +00:00
Rob Ede
aaedb9c625
MSRV 1.68 (#328) 2023-09-16 00:30:38 +01:00
Rob Ede
75386f4a1d
ensure docs.rs builds all crates with all features 2023-04-11 12:22:28 +01:00
Rob Ede
8a31f3020e
revert local-network change
see https://github.com/actix/actix-extras/pull/320#issuecomment-1501189129
2023-04-09 19:56:43 +01:00
Rob Ede
8c93f5314b
update readme crate versions 2023-04-09 19:47:57 +01:00
Rob Ede
f37c93a2a8
migrate to doc_auto_cfg 2023-04-09 19:41:57 +01:00
Rob Ede
111d95eaea
rename private-network-access feature (#320)
* update CI with concurrency options

* cors: rename private-network => local-network

* modernize CI

* clippy

* run api diff job on all features
2023-04-09 19:35:30 +01:00
Rob Ede
8729f60f79
fix CI MSRV 2023-03-23 12:15:25 +00:00
Rob Ede
77ee27b4ae
inline workspace package properties
msrv needs to be 1.64
2023-03-23 11:40:54 +00:00
Rob Ede
b948ac9f7a
fix MSRV in CI 2023-03-23 10:53:54 +00:00
Rob Ede
ad1f15eb18
centralize msrv and edition specs 2023-03-22 21:17:30 +00:00
Rob Ede
8a9c604c03
update msrv to 1.60
promted by prost 0.11.8
2023-03-22 21:13:56 +00:00
Rob Ede
218f18e69d
fix default features attributes 2023-03-22 20:51:14 +00:00
citreae535
2bc16eee18
Update base64 dependency to 0.21 (#316) 2023-01-30 16:53:30 +00:00
Jacobtread
713b157fd4
Corrected actix-form-data community crate details (#314) 2023-01-13 10:43:52 +00:00
Rob Ede
bf49b39740
use secure tokio version range
see RUSTSEC-2023-0001

part of actix/actix-web#2962
2023-01-10 09:03:27 +00:00
Rob Ede
441d604c00
derive identity error impls 2023-01-07 02:22:13 +00:00
Joseph McCormick
1ed893a08c
Feature: Add IdentityError to actix-identity crate. (#296)
* Add IdentityError to actix-identity crate.

In order to let crates in the actix web ecosystem interact correctly
with `actix_web::Error`, this commit introduces its own error type,
replacing the previous usage of `anyhow::Error`.

* Mend some clippy warnings on IdentityError.

* Split identity error into more granular versions.

- `MissingIdentityError` occurs whenever we attempt to gather
  information about an identity from a session, and fail.
- `LoginError` occurs whenever we attempt to login via an identity, and
  fail.

* Feedback for identity error implementation.

- `IdentityError` -> `GetIdentityError`
- Move error messages into Display impl where appropriate
- Split `id` and `get_identity` errors into two types
- Implement `source` on custom errors

* Expand identity error types with struct markers.

In order to get a little more future compatibility and reduce
abstraction leaking, this commit introduces some contextual structs to
our identity errors package.

* Improve doc message for SessionExpiryError.

Co-authored-by: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com>

* Improve identity error docs and messaging.

Co-authored-by: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com>

* Expand LostIdentityError with placeholder.

Adds a placeholder unit struct to the LostIdentityError variant of
GetIdentityError, which should let us expand on that variant with extra
context later if we like.

* Add From coercion for LostIdentityError.

Improve the ergonomics of using the LostIdentityError unit struct.

* Update Cargo.toml

* Update CHANGES.md

* expose identity error module

* fix error impl

Co-authored-by: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2023-01-07 02:05:12 +00:00
Rob Ede
708aa945dc
workaround msrv issues fix 2023-01-07 01:57:03 +00:00
Rob Ede
9be4f1ff73
workaround dev dep msrv issues 2023-01-07 01:38:07 +00:00
Rob Ede
f8a1165d10
fix manifest 2023-01-07 01:17:45 +00:00
Rob Ede
d9175a0399
update base64 dep to 0.20 2023-01-07 01:16:14 +00:00
Rob Ede
fe4d3d366d
Update redis dependency to 0.22 2023-01-07 01:15:26 +00:00
Rob Ede
1036f54fd0
update redis-async to 0.14 2023-01-07 01:09:34 +00:00
Rob Ede
e9428ba261
update env_logger dev dep 2023-01-07 01:08:01 +00:00
Rob Ede
779860b664
clippy 2023-01-07 01:04:16 +00:00
Rob Ede
6848312467
prettier markdown changelogs 2023-01-07 01:02:02 +00:00
Peihao Yang
8c509151f1
add sentinel middleware to community crates (#312) 2023-01-05 14:36:31 +00:00
Yuki Okushi
1774b8a36e
Fix GHA deprecation warnings (#301) 2022-12-01 10:45:31 +00:00
aalhitennf
9508be94d5
Update README.md (#304) 2022-11-12 19:52:29 +00:00
aalhitennf
8e76c6c628
add bincode extractor lib to community crates (#303) 2022-11-12 13:26:16 +00:00
Even O. Rogstadkjærnet
8fd166435f
Add secure field to removal cookie (#300)
Closes https://github.com/actix/actix-extras/issues/299
2022-11-08 09:29:23 +00:00
Rob Ede
1ac325ab79
fix cors changelog 2022-10-30 16:28:21 +00:00
Rob Ede
b95ce3a210
prepare actix-cors release 0.6.4 2022-10-30 16:25:52 +00:00
Rob Ede
ac444ca798
add support for private network access cors header (#297)
closes #294
2022-10-28 23:44:21 +01:00
Yuki Okushi
fb8a814acb
session: Fix a typo in a link to actix-redis (#293) 2022-10-15 12:36:59 +01:00
Rob Ede
da0a806e8d
clippy 2022-09-25 21:08:36 +01:00
Duy Do
d28ab6eaa1
CORS origin does not end with / (#291) 2022-09-22 11:46:24 +00:00
Rob Ede
a2c5cbd637
fix cors changelog 2022-09-22 00:25:48 +01:00
Rob Ede
e6ef190510
prepare actix-cors release 0.6.3 2022-09-22 00:24:38 +01:00
CapableWeb
3b5682c860
Add block_on_origin_mismatch option to middleware (#287)
Co-authored-by: CapableWeb <capableweb@domain.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-09-21 23:22:20 +00:00
Moritz Hedtke
82a100d96c
Add note for accessing session state in stream. (#285) 2022-09-21 12:51:45 +00:00
Marko Malenic
d98ebf2bdf
Update Cors function documentation to match behaviour (#289)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-09-21 09:15:54 +00:00
Rob Ede
1561bda822
apply imports_granularity fmt rule 2022-09-11 21:55:40 +01:00
Rob Ede
339b81e843
prepare actix-session release 0.7.2 2022-09-11 21:13:20 +01:00
Rob Ede
eb3660a772
set same-site attribute when clearing session cookie (#284)
fixes #282
2022-09-11 21:11:33 +01:00
Rob Ede
9a3b410409
prepare actix-limitation release 0.4.0 2022-09-11 00:06:57 +01:00
Raphael C
32313c0af6
Limitation: custom key from closure (#281)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-09-10 23:02:54 +00:00
Raphael C
a623c50e9c
Limitation: display and handle client error (#280)
* feat(limitation): display and handle client error

* feat(limitation): handle other count errors

* feat: add middleware errors catch changes to changelog

Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-08-28 20:49:14 +01:00
Rob Ede
7d932cd540
bump msrv to 1.59 2022-08-28 20:30:32 +01:00
Rob Ede
ffe122b76e
prepare actix-protobuf release 0.9.0 2022-08-24 18:07:47 +01:00
Rob Ede
1e682e7a59
update to prost 0.11 (#279)
* updated to prost 0.11 and added application/x-protobuf

* updated derive-more, prost, futures-util versions

* updated Changelog and a small fix in Cargo.toml

* cargo fmt

* bumped version to 0.8.1

* removed version bump

* add back intentional patch versions

Co-authored-by: Ahmed Masud <ahmed.masud@saf.ai>
2022-08-24 18:07:13 +01:00
Rob Ede
e61dbae860
rename AtError => Error (#275)
* refactor(settings)!: rename AtError => Error

and remove AtResult from public API

* update changelog

* recover from file metadata errors
2022-08-10 09:13:34 +01:00
Rob Ede
a325f5dd02
add some doc examples to -session 2022-08-09 01:03:28 +01:00
Rob Ede
bad6159516
prepare actix-cors release 0.6.2 2022-08-07 20:57:09 +01:00
Rob Ede
7c3c9357e0
fix expose all headers (#273)
* fix expose all headers

* update changelog
2022-08-07 20:56:33 +01:00
Rob Ede
bcb8dbe1fc
add config file to example 2022-07-31 20:26:40 +01:00
Rob Ede
983746f106
prepare actix-settings release 0.6.0 2022-07-31 20:13:27 +01:00
Rob Ede
b054733854
document settings crate (#271) 2022-07-31 20:12:19 +01:00
Rob Ede
ab3f591307
fmt 2022-07-31 15:34:52 +01:00
Rob Ede
c08cd8a23a
add standard crate lints 2022-07-31 15:33:22 +01:00
Rob Ede
da32c1bb49
rename settings modules 2022-07-31 15:30:50 +01:00
Rob Ede
90766e5d68
use panics in tests for better diagnostics 2022-07-31 15:18:23 +01:00
Rob Ede
f678842e46
modululize -settings 2022-07-31 15:10:22 +01:00
Rob Ede
e13b62fc6b
adopt actix-settings crate (#270)
* adopt actix-settings crate

* add licenses and readme addition

* revamp readme

* delete temp prettier file
2022-07-31 14:44:45 +01:00
Rob Ede
6e79465362
make RateLimiter non-exhaustive 2022-07-31 03:03:43 +01:00
Rob Ede
cd9dc163e5
prepare actix-session release 0.7.1 2022-07-24 15:29:50 +01:00
Mohamed Emad
810a88a156
fix: bad interaction between session state changes and renewal (#265) 2022-07-24 14:27:25 +00:00
Rob Ede
cfd16c5478
clippy 2022-07-21 22:20:48 +01:00
Rob Ede
07c5176bd0
align descriptions 2022-07-21 03:07:06 +01:00
Rob Ede
446c92c3d0
update proto deb badge 2022-07-21 02:54:53 +01:00
Rob Ede
65a6252fec
update crate dep badges 2022-07-21 02:54:11 +01:00
Rob Ede
73732b0a62
prepare actix-web-httpauth release 0.8.0 2022-07-21 02:51:33 +01:00
Rob Ede
ff06958b32
improve httpauth ergonomics (#264)
* improve httpauth ergonomics

* update changelog

* code and docs cleanup

* docs

* docs clean

* remove AuthExtractor trait

* update changelog
2022-07-21 02:50:22 +01:00
Rob Ede
4d2f4d58b4
clippy 2022-07-21 00:07:49 +01:00
Mike Cronce
140453c649
Return &str from BasicAuth::user_id() and BasicAuth::password() (#249)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-07-19 01:33:32 +00:00
Rob Ede
fbae63d07f
prepare actix-web-httpauth release 0.7.0 2022-07-19 01:52:04 +01:00
Roland
417c06b00e
fix: broken stream when authentication failed (#260)
* fix: broken http stream when authentication failed

Signed-off-by: Roland Ma <rolandma@outlook.com>

* remove unchanged

Signed-off-by: Roland Ma <rolandma@outlook.com>

* Update CHANGES.md

* Update CHANGES.md

* Update CHANGES.md

Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-07-19 01:40:01 +01:00
Rob Ede
553c2bfb92
prepare actix-identity release 0.5.2 2022-07-19 01:33:53 +01:00
Luca Palmieri
1089faaf93
[actix-identity] Fix visit deadline (#263) 2022-07-19 01:31:31 +01:00
Rob Ede
1cc37c371e
prepare actix-identity release 0.5.1 2022-07-11 18:07:50 +01:00
Rob Ede
d853c115b6
trim unnecessary identity deps (#259)
* trim unnecessary identity deps

* update changelog
2022-07-11 18:07:26 +01:00
Rob Ede
603215095a
prepare actix-identity release 0.5.0 2022-07-11 13:39:14 +01:00
Luca Palmieri
d3fb564380
Add changelog for actix-identity (#258) 2022-07-11 12:46:49 +01:00
Rob Ede
ee71d4cfa7
update readme 2022-07-11 02:15:47 +01:00
Rob Ede
f39a64f526
prepare actix-limitation release 0.3.0 2022-07-11 02:05:40 +01:00
Rob Ede
d5dc087e93
remove Limiter builder lifetime 2022-07-11 02:05:22 +01:00
Rob Ede
169b262c66
fix reference links 2022-07-09 20:29:08 +01:00
Rob Ede
d4384932ff
relative links on dir references 2022-07-09 20:27:01 +01:00
Rob Ede
4e1a95fc75
link community crates to crates.io
and add actix-multipart-extract
2022-07-09 20:26:00 +01:00
Rob Ede
910f964100
update community crates 2022-07-09 20:18:47 +01:00
Rob Ede
3e002a677b
bump session version of redis 2022-07-09 20:14:27 +01:00
Rob Ede
ca9879425b
prepare actix-redis release 0.12.0 2022-07-09 20:11:56 +01:00
Rob Ede
ecd7756644
update redis deps 2022-07-09 20:11:14 +01:00
Rob Ede
9bc014b96f
forbid unsafe in all crates 2022-07-09 20:05:47 +01:00
Rob Ede
e0ffd4e592
bump uuid dev dep 2022-07-09 19:58:42 +01:00
Rob Ede
97ee544057
update limitation's session version 2022-07-09 19:55:53 +01:00
Rob Ede
3b1c161547
remove protobuf example 2022-07-09 19:55:38 +01:00
Rob Ede
1830f66dca
revert session dep in limitation 2022-07-09 19:29:01 +01:00
Rob Ede
c52ea7a5d2
prepare actix-session release 0.7.0 2022-07-09 19:03:28 +01:00
Luca Palmieri
b1cea64795
Rebuild actix-identity on top of actix-session (#246)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-07-09 19:00:15 +01:00
Rob Ede
b8f4a658a9
rename post-merge ci job 2022-07-04 16:44:48 +01:00
Rob Ede
69e4264e0c
install cargo hack faster 2022-07-04 16:43:33 +01:00
Luca Palmieri
c2f068db66
Add a new configuration parameter to refresh the TTL of the session even if unchanged (#233)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-07-03 21:18:14 +01:00
Rob Ede
d09299390a
prepare actix-protobuf release 0.8.0 2022-06-26 00:36:27 +01:00
Tobias de Bruijn
a42ca24327
Updated Cargo.toml to support Prost 0.10 (#257) 2022-06-26 00:28:16 +01:00
Yuki Okushi
3c48e00e7a
Bump up MSRV to 1.57 (#256) 2022-06-22 20:42:46 +09:00
SatoruMasakazu_real
7267a19b1d
Changed description for actix-ws inside README (#255) 2022-06-16 20:43:41 +01:00
Yuki Okushi
f6508f290c
Do not run tests on MSRV (#250) 2022-05-25 14:07:31 +01:00
David Schmitt
d11a272384
Add missing backtic in SessionMiddleware docs (#245) 2022-05-18 10:55:57 +01:00
Luca Palmieri
8fd1772d5e
Consistent import formatting (#237) 2022-03-29 11:46:13 +01:00
Rob Ede
aebf9ccf58
add -lab and -hash 2022-03-28 02:42:01 +01:00
Rob Ede
4ca3e04929
prepare actix-session release 0.6.2 2022-03-25 23:28:11 +00:00
Luca Palmieri
04f4934001
[actix-session/redis-rs] Catch server-driven disconnections and prevent a user-visible issue via a retry (#235)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2022-03-25 19:08:09 +00:00
tglman
4d77e26e1e
add support for session access from guards (#234) 2022-03-25 18:25:38 +00:00
Luca Palmieri
8db1088345
[actix-session] Opaque 500s (#236) 2022-03-25 18:10:38 +00:00
Rob Ede
ac821e65b1
update readme 2022-03-22 15:42:13 +00:00
Rob Ede
5fdc4d8990
prepare actix-limitation release 0.2.0 2022-03-22 15:36:51 +00:00
Rob Ede
bf41b4cd9c
prepare actix-session release 0.6.1 2022-03-21 00:48:24 +00:00
Luca Palmieri
449abd6081
[actix-session] Documentation - Typo(s) / Improvements (#228) 2022-03-20 21:57:26 +00:00
Rob Ede
977e3141c9
limitation clean up (#232)
* add license links to -limitation

* move tests to inline mod

* use cow str for limiter parameters

* add docs to all limitation items

* rename builder methods

* fix ignored tests

* update changelog

* fix ci
2022-03-20 00:40:34 +00:00
Rob Ede
bb553b2308
adopt actix-limitation crate (#229)
* import code from actix-limitation master branch

* fix compilation

* update legal info

* fix compile errors

* ignore failing tests

* remove futures dep

* add changelog

* update readme

* fix doc test example
2022-03-18 17:00:33 +00:00
Rob Ede
e8ebf525ad
update aliri version 2022-03-18 13:14:57 +00:00
Rob Ede
2417549b35
clippy 2022-03-15 18:04:13 +00:00
Rob Ede
92269fc308
prepare actix-redis release 0.11.0 2022-03-15 18:01:31 +00:00
Rob Ede
01932f87d3
update ecosystem versions 2022-03-15 16:39:33 +00:00
Rob Ede
010a905dca
prepare actix-session release 0.6.0 2022-03-15 16:29:37 +00:00
Rob Ede
2d63973654
final session doc tweaks 2022-03-15 16:27:04 +00:00
Rob Ede
a086d30db2
prepare actix-cors release 0.6.1 2022-03-07 21:04:36 +00:00
Rob Ede
b748e7e3a7
conditionally add vary header to errors 2022-03-07 16:52:25 +00:00
Rob Ede
6fbe2eab94
allow OPTIONS requests without request-method header (#226) 2022-03-07 15:32:07 +00:00
Rob Ede
0ba1073cb2
ignore actix-session tests on master ci 2022-03-05 23:35:19 +00:00
Luca Palmieri
7e6335a09f
Rework actix session (#212)
Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: Luca P <rust@lpalmieri.com>
Co-authored-by: Sebastian Rollén <38324289+SebRollen@users.noreply.github.com>
2022-03-05 23:22:14 +00:00
Rob Ede
a1d0f051b7
fix typo 2022-03-04 17:39:32 +00:00
Rob Ede
e1f79dae17
fix test 2022-03-01 04:30:10 +00:00
Rob Ede
12f6db3755
tweak docs 2022-03-01 04:23:51 +00:00
nerix
673b77a765
docs(identity): clarify policy creation (#198) 2022-03-01 04:21:18 +00:00
simon-an
ce92f0036f
add test for middleware usage in app and scope (#215) 2022-03-01 04:18:46 +00:00
Rob Ede
fd272f817c
prepare actix-web-httpauth release 0.6.0 2022-03-01 04:14:07 +00:00
Rob Ede
2e86fb9822
prepare actix-identity release 0.4.0 2022-03-01 04:13:56 +00:00
Rob Ede
9e911490cf
prepare actix-protobuf release 0.7.0 2022-03-01 04:12:52 +00:00
Rob Ede
ef8f33e3b3
prepare actix-redis release 0.10.0 2022-03-01 04:12:20 +00:00
Rob Ede
c805dd7609
prepare actix-session release 0.5.0 2022-03-01 03:11:21 +00:00
Rob Ede
3959c55c47
prepare actix-cors release 0.6.0 2022-02-25 23:11:28 +00:00
Rob Ede
73a5ae98b6
update actix-web dependency to 4.0 2022-02-25 23:05:11 +00:00
Rob Ede
d46ee464e0
update examples links 2022-02-18 03:33:03 +00:00
Rob Ede
ff38ba2ce3
prepare actix-redis release 0.10.0-beta.6 2022-02-07 17:50:39 +00:00
Rob Ede
00378548ed
prepare actix-session release 0.5.0-beta.8 2022-02-07 17:50:39 +00:00
Rob Ede
cf0630b54d
prepare actix-cors release 0.6.0-beta.10 2022-02-07 17:50:39 +00:00
Rob Ede
695800c9bd
add vary header to all handled responses (#224) 2022-02-07 16:34:01 +00:00
Rob Ede
f9beeecaf6
prepare actix-identity release 0.4.0-beta.9 2022-02-07 02:52:24 +00:00
Rob Ede
d9f27120f6
prepare actix-web-httpauth release 0.6.0-beta.8 2022-02-07 02:51:58 +00:00
Rob Ede
f870074354
prepare actix-cors release 0.6.0-beta.9 2022-02-07 02:31:21 +00:00
Rob Ede
ab3615b8d5
relax body bounds (#223) 2022-02-07 02:30:26 +00:00
Rob Ede
7bb1f8d710
fix ci 2022-02-03 22:58:04 +00:00
Rob Ede
fac087723a
fix ci 2022-02-03 22:57:02 +00:00
Rob Ede
50fe96f278
disable coverage for now 2022-02-03 22:53:27 +00:00
Rob Ede
25d3d34927
fix doctest 2022-02-03 22:41:41 +00:00
Rob Ede
0ac068a14d
fix redis 2022-02-03 22:40:02 +00:00
Rob Ede
323d01fcca
bump actix-web to 4.0.0-rc.1 2022-02-03 22:33:47 +00:00
Rob Ede
6abec48e29
prepare actix-protobuf release 0.7.0-beta.5 2022-02-03 22:16:57 +00:00
Rob Ede
f676fc7de5
bump prost to 0.9 2022-02-03 22:14:13 +00:00
Rob Ede
dbc82001c0
address dep vuln warnings 2022-02-03 22:12:03 +00:00
josh rotenberg
717b93e507
fix api documentation link (#220) 2022-01-28 17:17:15 +00:00
Vladimir Pouzanov
7982abb71e
Fix the tracing-actix-web description (#219) 2022-01-23 19:46:01 +00:00
Rob Ede
e813e63138
add tracing-actix-web to community crate list 2022-01-23 00:11:50 +00:00
Rob Ede
bb900a41fc
prepare actix-identity release 0.4.0-beta.8 2022-01-21 20:43:39 +00:00
Rob Ede
4e72e3d8ec
update actix-identity to aw4b21 2022-01-21 20:42:58 +00:00
Rob Ede
09734a8d12
add awmp to list of community crates 2022-01-19 17:05:38 +00:00
Rob Ede
1fec026dc7
fix cors tests 2022-01-01 23:07:27 +00:00
Rob Ede
6d6b045b3a
add access-control-request-* headers to vary response header 2022-01-01 23:05:29 +00:00
Rob Ede
8540b61a13
fix ci 2021-12-31 08:38:31 +00:00
Rob Ede
cf7ed8e1b9
run nightly tests on master only 2021-12-31 08:32:04 +00:00
Rob Ede
19bff0028e
prepare actix-web-httpauth release 0.6.0-beta.7 2021-12-29 10:29:29 +00:00
Rob Ede
6d3ee78db5
prepare actix-redis release 0.10.0-beta.5 2021-12-29 10:29:29 +00:00
Rob Ede
ce2c97070e
prepare actix-session release 0.5.0-beta.7 2021-12-29 10:29:29 +00:00
Rob Ede
440ab34bd2
prepare actix-protobuf release 0.7.0-beta.3 2021-12-29 10:29:27 +00:00
Rob Ede
88859af6a5
prepare actix-identity release 0.4.0-beta.7 2021-12-29 10:25:17 +00:00
Rob Ede
422a264787
prepare actix-cors release 0.6.0-beta.8 2021-12-29 10:25:04 +00:00
Rob Ede
0d12201073
update to actix web v4 beta 18 (#218) 2021-12-29 10:22:56 +00:00
Rob Ede
5f17026a55
fix changelog 2021-12-29 09:59:36 +00:00
Rob Ede
92c9601850
bump msrv to 1.54 2021-12-29 09:42:31 +00:00
Rob Ede
6ca36b5d53
fixup changelogs 2021-12-29 09:36:12 +00:00
Luca Palmieri
14a622092d
Update to actix-web beta.17 (#217) 2021-12-29 09:25:52 +00:00
Rob Ede
6676a50944
actix-web beta 15 updates (#216) 2021-12-18 03:37:23 +00:00
Rob Ede
c047cd5653
mark cors v0.6.0-beta.5 as yanked 2021-12-13 03:03:19 +00:00
Rob Ede
fad4426388
revert cors example 2021-12-13 02:50:22 +00:00
Rob Ede
0805f2b1c6
stop cloning request across service call (#213) 2021-12-13 02:49:27 +00:00
Rob Ede
a0c93c62b3
prepare actix-web-httpauth release 0.6.0-beta.5 2021-12-12 19:13:40 +00:00
Rob Ede
128fc49811
prepare actix-protobuf release 0.7.0-beta.3 2021-12-12 19:13:29 +00:00
Rob Ede
e83c488544
prepare actix-identity release 0.4.0-beta.5 2021-12-12 19:13:16 +00:00
Rob Ede
360fd890dc
prepare actix-redis release 0.10.0-beta.4 2021-12-12 19:12:46 +00:00
Rob Ede
863d0b114b
prepare actix-session release 0.5.0-beta.5 2021-12-12 19:11:58 +00:00
Rob Ede
8fa073586e
prepare actix-cors release 0.6.0-beta.5 2021-12-12 19:11:36 +00:00
Luca Palmieri
e77ed16f49
Do not create a session if the session state is empty (#207) 2021-12-11 16:23:22 +00:00
Rob Ede
74ec115161
migrate to actix-web beta 14 (#209) 2021-12-11 16:05:21 +00:00
Rob Ede
700d90b68b
add actix-form-data to community crates 2021-12-11 00:59:05 +00:00
Rob Ede
56051786a6
standardize future types to ones from actix_utils 2021-12-08 07:29:12 +00:00
Rob Ede
ec66754c0d
standardize crate level lints 2021-12-08 06:11:13 +00:00
Sergey Pashinin
e636af039a
Update actix-redis to current state (beta) of actix (#210) 2021-12-06 01:03:18 +00:00
Rob Ede
64acd3229f
start redis in coverage job 2021-11-22 23:58:31 +00:00
Rob Ede
8bf2ae711a
fic doc test ci job 2021-11-22 23:48:45 +00:00
Rob Ede
5272bc1c79
split out coverage and doc test ci jobs 2021-11-22 23:42:47 +00:00
Rob Ede
3cc1487a4a
fix non-linux ci 2021-11-22 23:25:22 +00:00
Rob Ede
551cbfb113
prepare actix-web-httpauth release 0.6.0-beta.4 2021-11-22 23:21:25 +00:00
Rob Ede
0b4a4eeff6
prepare actix-session release 0.5.0-beta.4 2021-11-22 23:21:00 +00:00
Rob Ede
37532fd1eb
prepare actix-identity release 0.4.0-beta.4 2021-11-22 23:20:33 +00:00
Rob Ede
ddb74e6de0
prepare actix-cors release 0.6.0-beta.4 2021-11-22 23:19:10 +00:00
Rob Ede
13f8dcb717
update to aw beta 9 2021-11-22 23:11:58 +00:00
fakeshadow
07deaadd7b
add optional auth extractor implement. (#205) 2021-10-26 21:10:22 +01:00
Rob Ede
477b0f8f06
prepare actix-protobuf release 0.7.0-beta.2 2021-10-21 18:30:33 +01:00
Rob Ede
0999159877
prepare actix-identity release 0.4.0-beta.3 2021-10-21 18:30:03 +01:00
Rob Ede
7737da83e8
prepare actix-redis release 0.10.0-beta.3 2021-10-21 17:59:05 +01:00
Rob Ede
c6edb2a48a
prepare actix-session release 0.5.0-beta.3 2021-10-21 17:37:00 +01:00
Rob Ede
5d82b4e1e2
prepare actix-web-httpauth release 0.6.0-beta.3 2021-10-21 16:10:09 +01:00
Rob Ede
45643d4035
fix cors expose_any_header behavior (#204) 2021-10-21 15:47:56 +01:00
Chiu-Hsiang Hsu
545873b5b2
update actix-web dependencies to v4 beta.10 (#203)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-10-21 14:10:00 +01:00
Rob Ede
627fe96be0
bump msrv to 1.52.1 2021-10-19 01:49:39 +01:00
Luca Palmieri
d0f2075ce9
Implement Clone for CookieSession. (#201) 2021-10-18 13:03:14 +01:00
Luca Palmieri
a08b96529f
Add flash message crate. (#202) 2021-10-16 18:03:36 +01:00
Thales
1261557dc9
protobuf: Update prost to 0.8 (Fixes CI on nightly) (#197)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-10-11 19:56:15 +01:00
Rob Ede
798a5d6d0e
add actix-ip-filter to community libs 2021-10-11 02:55:15 +01:00
Rob Ede
051929ab47
use cargo resolver 2 2021-10-11 02:55:15 +01:00
Ali MJ Al-Nasrawy
50621bae71
cors: make middleware generic over body type (#195) 2021-09-13 11:00:58 +01:00
Rob Ede
e10937103e
fmt with new width 2021-08-30 23:27:44 +01:00
Rob Ede
c6f579790f
bump msrv to 1.51 2021-08-30 23:10:36 +01:00
Rob Ede
44c7b07ce2
prepare second beta round (#189) 2021-06-27 07:28:26 +01:00
Rob Ede
20ef05c36e
fix doctest ci (#188) 2021-06-27 07:02:38 +01:00
Marcus Griep
64eec6e550
Add aliri_actix as a community crate (#186) 2021-06-04 19:13:11 +01:00
Rob Ede
8e35b652d4
update readme 2021-05-07 18:07:49 +01:00
Lunush
8741cd32cc
Minor README fixes (#174) 2021-04-09 14:39:50 +01:00
Rob Ede
727353213f
fix cors example version 2021-04-02 11:48:19 +01:00
Rob Ede
d898e9e217
dont stabilize cors 2021-04-02 11:46:03 +01:00
Rob Ede
d0f0fb474b
fixup other cargo manifests 2021-04-02 11:44:18 +01:00
Rob Ede
bb1cc7443f
prepare identity release 0.4.0-beta.1 2021-04-02 11:44:03 +01:00
Rob Ede
7ab2b62810
prepare redis release 0.10.0-beta.1 2021-04-02 11:38:27 +01:00
Rob Ede
0ba14f786e
prepare session release 0.5.0-beta.1 2021-04-02 11:31:30 +01:00
Rob Ede
554a852dea
prepare httpauth release 0.6.0-beta.1 2021-04-02 11:25:57 +01:00
Rob Ede
5624ac9bb0
prepare cors release 1.0.0-beta.1 2021-04-02 10:22:32 +01:00
Rob Ede
fc6563a019
various session api improvements (#170) 2021-03-23 22:35:27 +00:00
Rob Ede
23912afd49
refactor identity (#168) 2021-03-23 05:05:03 +00:00
Rob Ede
c7df62d0b6
simplify ci like actix-web (#165) 2021-03-22 11:46:02 +00:00
Rob Ede
15d72b1694
use insert for protobuf content type header 2021-03-22 05:30:16 +00:00
Rob Ede
c8f1d9671c
add upgrade -web hints in changelogs 2021-03-22 05:24:35 +00:00
Rob Ede
7d0df351e0
normalize time deps to secure versions 2021-03-22 05:20:10 +00:00
Rob Ede
2254a429d4
use forward_ready for service definitions 2021-03-22 05:18:59 +00:00
fakeshadow
b0854ed144
fix actix-redis by revert most recent changes (#164) 2021-03-22 05:07:45 +00:00
Andrey Kutejko
ca85f6b245
Update dependencies (Tokio 1.0) (#144) 2021-03-21 22:50:26 +00:00
Rohit Chattopadhyay
86ff1302ad
Fix API Documentation URL (#163) 2021-03-21 15:34:27 +00:00
Rob Ede
5a72dd33d5
session, redis, and httpauth pre-v4 releases (#162) 2021-03-21 09:38:29 +00:00
Juan J. Jimenez-Anca
8d635f71fb
allow session-only cookies (#161)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-03-07 04:26:06 +09:00
Daniel T. Rodrigues
ba248a681b
update example links (#159) 2021-02-28 19:29:54 +00:00
Artem Medvedev
81c48930c4
Add actix-web-grants crate to readme (#149) 2021-01-17 02:47:19 +00:00
Rob Ede
d77c908567
update Cors::allow_origin_fn docs 2021-01-03 22:35:52 +00:00
Yuki Okushi
91e91c4327 Prepare cors 0.5.4 release 2020-12-31 22:11:04 +09:00
Julian Tescher
cf970ee091
Fix expose_any_header setter (#143) 2020-12-31 21:13:36 +09:00
Yuki Okushi
699846d965
Disable PR comment from codecov 2020-12-17 21:41:58 +09:00
Quentin Kniep
936a116264
Fix purge from other paths than root (#129)
Co-authored-by: Rob Ede <robjtede@icloud.com>
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
2020-12-04 03:52:48 +09:00
Yuki Okushi
f970d90894
Use re-exported bytes items from instead of using them directly (#139) 2020-12-01 10:21:56 +09:00
Yuki Okushi
699a98493b
Update github-pages-deploy-action to 3.7.1 (#138) 2020-12-01 06:06:48 +09:00
Arniu Tseng
a22e4cfa36
impl std::error::Error for Error (#135) 2020-11-30 20:19:32 +09:00
Rob Ede
99e98f1a29
address clippy warnings (#134) 2020-11-23 10:37:56 +00:00
Rob Ede
ab80a91468
update cors deps badge 2020-11-20 00:27:53 +00:00
Rob Ede
f60c3653d3
add actix-web-static-files to readme libs 2020-11-20 00:25:45 +00:00
Rob Ede
c534ef0f98
fix derive_more version spec
fixes #130
2020-11-19 00:52:49 +00:00
Rob Ede
cbfd5d94ee
fix httpauth extraction error handling in middleware (#128) 2020-11-18 15:08:03 +00:00
Rob Ede
61778d864e
ensure tinyvec is using alloc feature 2020-11-15 22:24:18 +00:00
Rob Ede
a396c1b961
prepare cors release 0.5.1 2020-11-05 18:45:47 +00:00
Guarabot
0b2eff9384
Fix allow_any_header setter (#121) 2020-11-05 18:38:27 +00:00
Rob Ede
76429602c6
prepare cors release 0.5.0 2020-10-19 23:09:42 +01:00
Rob Ede
f20b724830
cors origin validator function receives origin as first parameter (#120) 2020-10-19 19:30:46 +01:00
Rob Ede
6084c47810
CORS builder rework (#119) 2020-10-19 05:51:31 +01:00
Luca Bruno
c8e641d4b6
cors: hide service struct from docs (#118)
This hides `CorsMiddleware` from rustdocs, following the pattern of
the rest of `actix_web::middleware` implementations.
2020-10-17 07:01:30 +09:00
Matteo
b59e307de1
Updated example link to use actix-extras repo (#117) 2020-10-14 21:41:05 +01:00
Rob Ede
2d1a493f17
move actix-casbin lib from examples readme 2020-10-14 17:59:04 +01:00
Yuki Okushi
c57c97c7c8
Disable macOS cache to fix spurious failures (#116) 2020-10-13 23:54:04 +09:00
eupn
06f17ec223
Panic on wildcard in Cors builder's allowed_origin() (#114)
* Assert allowed origin in Cors builder

* Add panic test for wildcard

* Add changelog entry

* rustfmt

* Apply suggestions from code review

Co-authored-by: Rob Ede <robjtede@icloud.com>

Co-authored-by: Rob Ede <robjtede@icloud.com>
2020-10-10 14:25:33 +01:00
Rob Ede
134e43ab5e
split up cors lib and fix doc links (#113)
* split up cors lib and fix doc links

* clippy
2020-10-08 11:50:56 +01:00
Rob Ede
16db1b6808
bump cors version 2020-10-07 11:34:49 +01:00
Rob Ede
d02e508731
prepare cors release 0.4.1 2020-10-07 11:32:27 +01:00
Roman Lakhtadyr
b33012999c
feat(cors): allow using closures in allow_origin_fn (#110) 2020-10-07 11:29:20 +01:00
Francois Stephany
0f7a147323
Unify homepage/repository urls in Cargo manifests (#111) 2020-10-03 01:32:01 +01:00
Rob Ede
e3da3094f0
update changelog 2020-09-28 02:44:16 +01:00
Rob Ede
33dfea8997
update httpauth readme 2020-09-28 02:04:18 +01:00
Rob Ede
d14e188246
prepare CORS release 0.4.0 (#107) 2020-09-27 14:29:33 +01:00
FallenWarrior2k
f185a9c7e6
CORS: Take TryInto instead of TryFrom (#106) 2020-09-26 20:02:45 +01:00
Roman Lakhtadyr
99fe08f332
actix-cors: Create allowed_origin_fn builder method (#93)
Co-authored-by: ArmorDarks <git@lavrins.com>
2020-09-25 00:36:53 +01:00
FallenWarrior2k
bb8120a8c0
Update Session::set_session to take IntoIterator (#105) 2020-09-22 00:04:21 +01:00
Rob Ede
03ccf09e2e
prepare identity 0.3.1 release (#104) 2020-09-21 23:05:40 +01:00
Rob Ede
6ae147d190
add CookieIdentityPolicy::http_only method (#102) 2020-09-17 18:10:27 +01:00
Rob Ede
400d889116
prepare redis release 0.9.1 (#99) 2020-09-13 04:28:08 +01:00
Rob Ede
f3d5dfde40
enforce required minimum redis-async patch version (#98) 2020-09-12 17:30:09 +01:00
Rob Ede
7a26d99c1a
lint and readme cleanup (#97) 2020-09-12 00:52:55 +01:00
Rob Ede
4a546718aa
prepare v3 compatible releases (#95) 2020-09-11 21:22:55 +01:00
Rob Ede
bad5f32a68
update all packages to use actix-web v3 (#94) 2020-09-11 16:26:15 +01:00
Yuki Okushi
7e6bdf2eb2
Check code with rustfmt not to introduce format commits (#88) 2020-07-21 03:51:51 +09:00
Yuki Okushi
e5fe8d42fa
Use matches macro to fix clippy warnings (#86) 2020-07-21 02:20:23 +09:00
Yuki Okushi
693c2f5041
Tweak actions (#85)
- Tweak trigger events
- Remove unnecessary workaround (hopefully)
- Stop using `ghq_import`
2020-07-20 06:18:00 +09:00
Rob Ede
d25ae41525
Create PULL_REQUEST_TEMPLATE.md (#84) 2020-07-20 03:02:34 +09:00
Oskar Persson
a960eb0ef6
Update backoff in actix-redis to 0.2.1 (#83) 2020-07-19 02:30:46 +01:00
Yuki Okushi
43ababef8f
Clean up deps and macro_use (#81) 2020-07-14 11:20:42 +01:00
Yuki Okushi
2ae3c80548
Use OR instead of deprecated / in license field (#80) 2020-07-14 11:16:32 +01:00
Yuki Okushi
2a20ce4568
Replace deprecated from_master with derive_from (#82) 2020-07-14 11:15:30 +01:00
Yuki Okushi
39fe80df40
Merge pull request #77 from JohnTitor/new-httpauth 2020-07-08 08:28:32 +09:00
Yuki Okushi
61da910a81
httpauth: Bump up to 0.4.2 2020-07-08 07:55:40 +09:00
Yuki Okushi
6d3e4c9aa1
Merge pull request #74 from JohnTitor/protobuf-alpha
protobuf: Update to 0.6.0-alpha.1
2020-07-07 04:28:37 +09:00
Yuki Okushi
9b573131f8
Merge pull request #73 from JohnTitor/clippy-fixes 2020-07-07 04:19:54 +09:00
Yuki Okushi
f920479fdb
protobuf: Update to 0.6.0-alpha.1 2020-07-07 03:54:36 +09:00
Yuki Okushi
bd963fb7d1
Fix clippy warnings 2020-07-07 03:54:18 +09:00
Yuki Okushi
6b839f0a30
Merge pull request #72 from lucab/ups/cors-docs 2020-07-07 03:53:32 +09:00
Yuki Okushi
f4d75260a4
Merge pull request #76 from JohnTitor/fix-tarpaulin
Use tarpaulin 0.13 to avoid failure
2020-07-07 03:52:54 +09:00
Yuki Okushi
6e132d9337
Use tarpaulin 0.13 to avoid failure 2020-07-07 03:30:45 +09:00
Luca BRUNO
95041b8e80
actix-cors: cosmetic and minor fixes
This rewords part of the docstrings for better readability,
and introduces additional lints. That includes ensuring that
all public types implement the Debug trait.
2020-07-06 15:55:53 +00:00
Rob Ede
027f045340
exclude default -web features where not needed (#70) 2020-06-18 11:22:14 +01:00
Boyd Johnson
70df190e0b
httpauth: Refactor out Mutex (#69) 2020-06-11 16:10:18 +01:00
Yuki Okushi
1d32248844
Merge pull request #66 from actix/cache-v2
Update `actions/cache` to v2
2020-05-28 04:58:51 +09:00
Yuki Okushi
5157927219
Update actions/cache to v2 2020-05-28 03:03:30 +09:00
LJ
e03544cb0d
Allow for session cookies to be lazily created (#39) 2020-05-25 01:27:49 +01:00
Yuki Okushi
a0166495ea
Merge pull request #64 from JohnTitor/issue-template
Setup issue template
2020-05-23 13:45:37 +09:00
Yuki Okushi
baafcbe625
Setup issue template 2020-05-23 12:22:25 +09:00
Yuki Okushi
2209359c78
Merge pull request #61 from JohnTitor/futures
Minimize `futures` dependency
2020-05-21 17:55:21 +09:00
Yuki Okushi
10fe10c9c1
httpauth: Minimize futures dependency 2020-05-21 17:23:59 +09:00
Yuki Okushi
b699506526
redis: Minimize futures dependency 2020-05-21 17:17:56 +09:00
Yuki Okushi
2e3a094ef7
protobuf: Minimize futures dependency 2020-05-21 17:13:53 +09:00
Yuki Okushi
c152c1ae8c
Merge pull request #59 from JohnTitor/badge
httpauth: Remove badge metadata
2020-05-20 15:24:27 +09:00
Yuki Okushi
4eb987f8c8
httpauth: Remove badge metadata 2020-05-20 11:02:56 +09:00
Yuki Okushi
61d5586052
Merge pull request #58 from adamchalmers/patch-1
Bugfix: AuthenticationError should set status_code
2020-05-20 11:01:31 +09:00
Adam Chalmers
a50f1db473 Bugfix: AuthenticationError should set status_code
I noticed that my `AuthenticationError`'s `status_code` field was 401, but when I ran `.as_response_error()` the `ResponseError`'s code was 500 (the default value). It's because impl ResponseError for AuthenticationError doesn't define the `status_code()` trait method. Merely setting the status code in `error_response` isn't enough, it seems. I added a unit test for this case too.
2020-05-19 07:34:53 -05:00
Yuki Okushi
5c6c04caf3
Merge pull request #52 from maitsarv/master
Documentation update: RedisSessionBackend is renamed to RedisSession
2020-05-17 21:20:47 +09:00
Yuki Okushi
f3778c4856
Merge pull request #55 from JohnTitor/new-redis
redis: Bump up to 0.9.0-alpha.2
2020-05-17 20:51:42 +09:00
mait
f3eaf5640c Documentation update: RedisSessionBackend is renamed to RedisSession 2020-05-17 14:46:41 +03:00
Yuki Okushi
d51012075f
redis: Bump up to 0.9.0-alpha.2 2020-05-17 20:19:13 +09:00
Yuki Okushi
8bf33648ce
Merge pull request #53 from JohnTitor/framed-write
Follow upstream change wrt. `FramedWrite`
2020-05-17 12:27:24 +09:00
Yuki Okushi
923cb7bcd2
Bump up MSRV to 1.40.0 2020-05-17 11:34:00 +09:00
Yuki Okushi
ecc774450f
Follow upstream change wrt. FramedWrite 2020-05-17 11:12:12 +09:00
Yuki Okushi
fee78223a9
Merge pull request #51 from JohnTitor/now-utc
Replace deprecated methods with `now_utc()`
2020-05-09 08:26:30 +09:00
Yuki Okushi
446b920d96
Replace deprecated methods with now_utc() 2020-05-09 08:07:31 +09:00
Yuki Okushi
6b5215d439
Merge pull request #45 from pfrenssen/check-prost-example
Compile the actix-protobuf example during CI test runs
2020-04-13 23:40:12 +09:00
Pieter Frenssen
beb6e1ccde Compile the actix-protobuf example. 2020-04-13 16:14:19 +03:00
Pieter Frenssen
ede715374b
Clarify how to use Session::set_session() (#41)
* Clarify in examples how to use Session::set_session().

* Add a doc example for Session::set_session().
2020-04-08 01:49:58 +09:00
Yuki Okushi
a72d4aa0c8
Merge pull request #40 from actix/fix/noisy-check
fix noisy check warning
2020-04-05 13:20:43 +09:00
Rob Ede
fd2cc47447
fix noisy check warning 2020-04-05 00:27:08 +01:00
Yuki Okushi
4d64ac13dd
Merge pull request #38 from JohnTitor/base64
Update the `base64` dependency to 0.12
2020-04-03 20:47:17 +09:00
Yuki Okushi
92c4230550
Update the base64 dependency to 0.12 2020-04-03 17:01:36 +09:00
Omikuji
f6686c9292
Change ttl to u32 (#35)
* Change TTL to u32

* Update CHANGES.md
2020-04-03 16:19:21 +09:00
Bart Willems
f4bcebdecd
allow user to set the cookie HttpOnly policy for the redis session (#36)
* allow user to set the cookie HttpOnly policy for the redis session

Signed-off-by: Bart Willems <bwillems@protonmail.com>
2020-03-29 21:36:01 +09:00
Yuki Okushi
f878889627
Merge pull request #34 from JohnTitor/redis
actix-redis: Bump up to 0.9.0-alpha.1
2020-03-28 10:10:26 +09:00
Yuki Okushi
21024be9d6
Bump up to 0.9.0-alpha.1 2020-03-27 20:50:31 +09:00
Yuki Okushi
2bfc9be5ad
Run rustfmt 2020-03-27 20:50:31 +09:00
Rob Ede
585709929b
Merge pull request #29 from pfrenssen/patch-1
Fix cargo package link title in README
2020-03-22 13:51:37 +00:00
Pieter Frenssen
066fb3a1b5
Fix cargo package link title in README 2020-03-22 10:35:56 +02:00
Yuki Okushi
4be50ff7dd
Merge pull request #27 from JohnTitor/clean-up
Clean-up things
2020-03-19 02:42:05 +09:00
Yuki Okushi
5e59fb157c
Add codecov.yml 2020-03-18 06:34:32 +09:00
Yuki Okushi
00f520cd2d
Fix clippy warnings 2020-03-18 06:34:32 +09:00
Yuki Okushi
d13798748f
Merge pull request #26 from JohnTitor/remove-udeps
Remove unused `actix` dependency
2020-03-18 05:31:32 +09:00
Yuki Okushi
83d2b9c95b
Remove unused actix dependency 2020-03-18 04:33:21 +09:00
Yuki Okushi
de374afbca
Revert "Disable coverage for PRs" (#25)
* Revert "Disable coverage for PRs"

* Remove `token` arg
2020-03-18 04:19:15 +09:00
kevinpoitra
79dc7fcaff
Update actix-redis' dependencies (#24)
* Update actix-redis's dependencies

* Change `chrono::Duration` to `time::Duration` in the docs

* Remove unneeded comment

* Update CHANGES.md
2020-03-15 16:54:35 +09:00
Yuki Okushi
d913550570
Merge pull request #23 from JohnTitor/new-session
Release actix-session v0.4.0-alpha.1
2020-03-15 07:14:13 +09:00
Yuki Okushi
483f641165
Bump up to 0.4.0-alpha.1 2020-03-15 07:01:21 +09:00
Yuki Okushi
510be85629
Fix style 2020-03-15 06:59:41 +09:00
Yuki Okushi
bc36cc1159
Minimize futures dependency 2020-03-15 06:58:47 +09:00
Yuki Okushi
ab1bcf15d4
Merge pull request #22 from JohnTitor/new-identity
Release actix-identity 0.3.0-alpha.1
2020-03-15 06:50:44 +09:00
Yuki Okushi
dd7ee27388
Bump up to 0.3.0-alpha.1 2020-03-15 06:15:22 +09:00
Yuki Okushi
a3ebc8ca66
Minimize futures dependency 2020-03-15 05:44:29 +09:00
Yuki Okushi
02ada2bd88
Merge pull request #21 from JohnTitor/identity-session
Check-in actix-identity and actix-session
2020-03-13 07:06:44 +09:00
Yuki Okushi
30ee6e0b4c
session: Update actix-web dependency to 3.0.0-alpha.1 2020-03-13 06:04:07 +09:00
Yuki Okushi
570209c1b1
identity: Update actix-web dependency to 3.0.0-alpha.1 2020-03-13 06:01:39 +09:00
Yuki Okushi
62cf9baa51
Merge pull request #20 from JohnTitor/cors
Release `actix-cors` v0.3.0-alpha.1
2020-03-12 06:46:02 +09:00
Yuki Okushi
8176d5e121
Run rustfmt 2020-03-12 05:48:57 +09:00
Yuki Okushi
7f1680ebcf
Bump up to 0.3.0-alpha.1 2020-03-12 05:48:04 +09:00
Yuki Okushi
405e61a603
Update actix-web dependency to 3.0.0-alpha.1 2020-03-12 05:46:58 +09:00
Yuki Okushi
9a8275b455
Minimize futures-* dependencies 2020-03-12 05:43:48 +09:00
Yuki Okushi
51f5c326ee
Merge pull request #9 from JohnTitor/migrate-httpauth
actix-web-httpauth: Prepare for new release
2020-02-19 10:59:03 +09:00
Yuki Okushi
e245b1cffe Prepare for new release 2020-02-19 09:31:51 +09:00
Yuki Okushi
880206ee2e
Merge pull request #8 from JohnTitor/migrate-redis
actix-redis: Prepare for new release
2020-02-18 10:56:32 +09:00
Yuki Okushi
8bc0227ec8 Fix clippy warning 2020-02-18 08:22:33 +09:00
Yuki Okushi
e225e2aec7 Move env_logger to dev-dependencies 2020-02-18 08:11:36 +09:00
Yuki Okushi
66edc228c3 Prepare for new release 2020-02-18 08:01:26 +09:00
161 changed files with 63943 additions and 5748 deletions

7
.cargo/config.toml Normal file
View File

@ -0,0 +1,7 @@
[alias]
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
ci-min = "hack check --workspace --no-default-features"
ci-check-min-examples = "hack check --workspace --no-default-features --examples"
ci-check = "check --workspace --tests --examples --bins"
ci-test = "test --workspace --lib --tests --all-features --examples --bins --no-fail-fast"
ci-doctest = "test --workspace --doc --all-features --no-fail-fast"

43
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,43 @@
---
name: bug report
about: create a bug report
---
Your issue may already be reported! Please search on the [actix-extras issue tracker](https://github.com/actix/actix-extras/issues) before creating one.
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current 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 -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context
<!--- 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 -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
- Rust version (output of `rustc -V`):
- `actix-*` crate versions:

27
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

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

10
.github/dependabot.yml vendored Normal file
View File

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

107
.github/workflows/ci-post-merge.yml vendored Normal file
View File

@ -0,0 +1,107 @@
name: CI (post-merge)
on:
push: { branches: [master] }
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_test_linux_nightly:
strategy:
fail-fast: false
matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
name: ${{ matrix.target.name }} / nightly
runs-on: ${{ matrix.target.os }}
services:
redis:
image: redis:5.0.7
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
- name: Install cargo-hack, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42
with:
tool: cargo-hack,cargo-ci-cache-clean
- name: check minimal
run: cargo ci-min
- name: check minimal + examples
run: cargo ci-check-min-examples
- name: check default
run: cargo ci-check
- name: tests
timeout-minutes: 40
run: cargo ci-test
- name: CI cache clean
run: cargo-ci-cache-clean
build_and_test_other_nightly:
strategy:
fail-fast: false
# prettier-ignore
matrix:
target:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
name: ${{ matrix.target.name }} / nightly
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v4
- name: Install OpenSSL
if: matrix.target.os == 'windows-latest'
shell: bash
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
- name: Install cargo-hack and cargo-ci-cache-clean
uses: taiki-e/install-action@v2.49.42
with:
tool: cargo-hack,cargo-ci-cache-clean
- name: check minimal
run: cargo ci-min
- name: check minimal + examples
run: cargo ci-check-min-examples
- name: check default
run: cargo ci-check
- name: tests
timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation -- --nocapture
- name: CI cache clean
run: cargo-ci-cache-clean

153
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: CI
on:
pull_request:
types: [opened, synchronize, reopened]
merge_group:
types: [checks_requested]
push:
branches: [master]
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build_and_test_linux:
strategy:
fail-fast: false
matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
version:
- { name: msrv, version: 1.75.0 }
- { name: stable, version: stable }
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
runs-on: ${{ matrix.target.os }}
services:
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--entrypoint redis-server
steps:
- uses: actions/checkout@v4
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install cargo-hack and cargo-ci-cache-clean, just
uses: taiki-e/install-action@v2.49.42
with:
tool: cargo-hack,cargo-ci-cache-clean,just
- name: workaround MSRV issues
if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv
- name: check minimal
run: cargo ci-min
- name: check minimal + examples
run: cargo ci-check-min-examples
- name: check default
run: cargo ci-check
- name: tests
timeout-minutes: 40
run: cargo ci-test
- name: CI cache clean
run: cargo-ci-cache-clean
build_and_test_other:
strategy:
fail-fast: false
matrix:
# prettier-ignore
target:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- { name: msrv, version: 1.75.0 }
- { name: stable, version: stable }
name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v4
- name: Install OpenSSL
if: matrix.target.os == 'windows-latest'
shell: bash
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: ${{ matrix.version.version }}
- name: Install cargo-hack, cargo-ci-cache-clean, just
uses: taiki-e/install-action@v2.49.42
with:
tool: cargo-hack,cargo-ci-cache-clean,just
- name: workaround MSRV issues
if: matrix.version.name == 'msrv'
run: just downgrade-for-msrv
- name: check minimal
run: cargo ci-min
- name: check minimal + examples
run: cargo ci-check-min-examples
- name: check default
run: cargo ci-check
- name: tests
timeout-minutes: 40
run: cargo ci-test --exclude=actix-session --exclude=actix-limitation
- name: CI cache clean
run: cargo-ci-cache-clean
doc_tests:
name: Documentation Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@v2.49.42
with:
tool: just
- name: Test docs
run: just test-docs
- name: Build docs
run: just doc

View File

@ -1,18 +0,0 @@
on: pull_request
name: Clippy Check
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --all --tests

47
.github/workflows/coverage.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Coverage
on:
push:
branches: [master]
permissions: { contents: read }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
coverage:
runs-on: ubuntu-latest
services:
redis:
image: redis:5.0.7
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: nightly
components: llvm-tools-preview
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.49.42
with:
tool: just,cargo-llvm-cov,cargo-nextest
- name: Generate code coverage
run: just test-coverage-codecov
- name: Upload to Codecov
uses: codecov/codecov-action@v5.4.0
with:
files: codecov.json
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

48
.github/workflows/lint.yml vendored Normal file
View File

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

View File

@ -1,83 +0,0 @@
name: CI (Linux)
on: [push, pull_request]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
redis:
image: redis:5.0.7
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
- name: Generate coverage file
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
run: |
cargo install cargo-tarpaulin
cargo tarpaulin --out Xml --workspace --all-features
- name: Upload to Codecov
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: cobertura.xml
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,66 +0,0 @@
name: CI (macOS)
on: [push, pull_request]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-apple-darwin
runs-on: macos-latest
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-apple-darwin
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-cors
--package=actix-protobuf
--package=actix-web-httpauth
--all-features --no-fail-fast -- --nocapture
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,43 +0,0 @@
name: CI (Linux, MSRV)
on: [push, pull_request]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- 1.39.0
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
redis:
image: redis:5.0.7
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: tests (1.39.0)
if: matrix.version == '1.39.0'
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-cors
--package=actix-protobuf
--package=actix-redis
--package=actix-web-httpauth
--all-features --no-fail-fast -- --nocapture

View File

@ -1,35 +0,0 @@
name: Upload documentation (actix-redis)
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
if: github.repository == 'actix/actix-extras'
steps:
- uses: actions/checkout@master
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: check build
uses: actions-rs/cargo@v1
with:
command: doc
args: --no-deps --package=actix-redis
- name: Tweak HTML
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
- name: Upload documentation
run: |
git clone https://github.com/davisp/ghp-import.git
./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc

View File

@ -1,70 +0,0 @@
name: CI (Windows)
on: [push, pull_request]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
target:
- x86_64-pc-windows-msvc
- x86_64-pc-windows-gnu
- i686-pc-windows-msvc
name: ${{ matrix.version }} - ${{ matrix.target }}
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target }}
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ matrix.version }}-${{ matrix.target }}-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ matrix.version }}-${{ matrix.target }}-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ matrix.version }}-${{ matrix.target }}-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }}
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-cors
--package=actix-protobuf
--package=actix-web-httpauth
--all-features --no-fail-fast -- --nocapture
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

4
.gitignore vendored
View File

@ -1,6 +1,5 @@
/target
**/*.rs.bk
Cargo.lock
guide/build/
/gh-pages
@ -11,3 +10,6 @@ guide/build/
*.pid
*.sock
*~
.DS_Store
Server.toml

5
.prettierrc.yml Normal file
View File

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

3292
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,39 @@
# TODO: identity and session are waiting for time crate changes
[workspace]
resolver = "2"
members = [
"actix-cors",
# "actix-identity",
"actix-protobuf",
"actix-redis",
# "actix-session",
"actix-web-httpauth",
"actix-cors",
"actix-identity",
"actix-limitation",
"actix-protobuf",
"actix-session",
"actix-settings",
"actix-web-httpauth",
"actix-ws",
]
[workspace.package]
repository = "https://github.com/actix/actix-extras"
homepage = "https://actix.rs"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.75"
[workspace.lints.rust]
rust-2018-idioms = { level = "deny" }
nonstandard-style = { level = "deny" }
future-incompatible = { level = "deny" }
[patch.crates-io]
actix-cors = { path = "./actix-cors" }
actix-identity = { path = "./actix-identity" }
actix-limitation = { path = "./actix-limitation" }
actix-protobuf = { path = "./actix-protobuf" }
actix-session = { path = "./actix-session" }
actix-settings = { path = "./actix-settings" }
actix-web-httpauth = { path = "./actix-web-httpauth" }
# uncomment to quickly test against local actix-web repo
# actix-http = { path = "../actix-web/actix-http" }
# actix-router = { path = "../actix-web/actix-router" }
# actix-web = { path = "../actix-web" }
# awc = { path = "../actix-web/awc" }

View File

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

View File

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

View File

@ -1,29 +1,90 @@
# actix-extras
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)[![build status](https://github.com/actix/actix-extras/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-extras/actions)
> A collection of additional crates supporting [Actix Web].
> A collection of additional crates supporting the [actix] and [actix-web] frameworks.
<!-- prettier-ignore-start -->
[![CI](https://github.com/actix/actix-extras/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-extras/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-extras/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-extras)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/5Ux4QGChWc)
[![Dependency Status](https://deps.rs/repo/github/actix/actix-extras/status.svg)](https://deps.rs/repo/github/actix/actix-extras)
## Crates
<!-- prettier-ignore-end -->
| Crate | | |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| [actix-cors] | [![crates.io](https://img.shields.io/crates/v/actix-cors)](https://crates.io/crates/actix-cors) [![Documentation](https://docs.rs/actix-cors/badge.svg)](https://docs.rs/actix-cors) | Cross-origin resource sharing (CORS) for actix-web applications. |
| [actix-identity] | [![crates.io](https://img.shields.io/crates/v/actix-identity)](https://crates.io/crates/actix-identity) [![Documentation](https://docs.rs/actix-identity/badge.svg)](https://docs.rs/actix-identity) | Identity service for actix-web framework. |
| [actix-protobuf] | [![crates.io](https://img.shields.io/crates/v/actix-protobuf)](https://crates.io/crates/actix-protobuf) [![Documentation](https://docs.rs/actix-protobuf/badge.svg)](https://docs.rs/actix-protobuf) | Protobuf support for actix-web framework. |
| [actix-redis] | [![crates.io](https://img.shields.io/crates/v/actix-redis)](https://crates.io/crates/actix-redis) [![Documentation](https://docs.rs/actix-redis/badge.svg)](https://docs.rs/actix-redis) | Redis integration for actix framework. |
| [actix-session] | [![crates.io](https://img.shields.io/crates/v/actix-session)](https://crates.io/crates/actix-session) [![Documentation](https://docs.rs/actix-session/badge.svg)](https://docs.rs/actix-session) | Session for actix-web framework. |
| [actix-web-httpauth] | [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth)](https://crates.io/crates/actix-web-httpauth) [![Documentation](https://docs.rs/actix-web-httpauth/badge.svg)](https://docs.rs/actix-web-httpauth) | HTTP authentication schemes for actix-web. |
## Crates by @actix
| Crate | | |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| [actix-cors] | [![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors) [![dependency status](https://deps.rs/crate/actix-cors/latest/status.svg)](https://deps.rs/crate/actix-cors) | Cross-Origin Resource Sharing (CORS) controls. |
| [actix-identity] | [![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity) [![dependency status](https://deps.rs/crate/actix-identity/latest/status.svg)](https://deps.rs/crate/actix-identity) | Identity management. |
| [actix-limitation] | [![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation) [![dependency status](https://deps.rs/crate/actix-limitation/latest/status.svg)](https://deps.rs/crate/actix-limitation) | Rate-limiting using a fixed window counter for arbitrary keys, backed by Redis. |
| [actix-protobuf] | [![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf) [![dependency status](https://deps.rs/crate/actix-protobuf/latest/status.svg)](https://deps.rs/crate/actix-protobuf) | Protobuf payload extractor. |
| [actix-session] | [![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session) [![dependency status](https://deps.rs/crate/actix-session/latest/status.svg)](https://deps.rs/crate/actix-session) | Session management. |
| [actix-settings] | [![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings) [![dependency status](https://deps.rs/crate/actix-settings/latest/status.svg)](https://deps.rs/crate/actix-settings) | Easily manage Actix Web's settings from a TOML file and environment variables. |
| [actix-web-httpauth] | [![crates.io](https://img.shields.io/crates/v/actix-web-httpauth?label=latest)](https://crates.io/crates/actix-web-httpauth) [![dependency status](https://deps.rs/crate/actix-web-httpauth/latest/status.svg)](https://deps.rs/crate/actix-web-httpauth) | HTTP authentication schemes. |
| [actix-ws] | [![crates.io](https://img.shields.io/crates/v/actix-ws?label=latest)][actix-ws] [![dependency status](https://deps.rs/crate/actix-ws/latest/status.svg)](https://deps.rs/crate/actix-ws) | WebSockets for Actix Web, without actors. |
---
## Community Crates
These crates are provided by the community.
| Crate | | |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| [actix-web-lab] | [![crates.io](https://img.shields.io/crates/v/actix-web-lab?label=latest)][actix-web-lab] [![dependency status](https://deps.rs/crate/actix-web-lab/latest/status.svg)](https://deps.rs/crate/actix-web-lab) | Experimental extractors, middleware, and other extras for possible inclusion in Actix Web. |
| [actix-form-data] | [![crates.io](https://img.shields.io/crates/v/actix-form-data?label=latest)][actix-form-data] [![dependency status](https://deps.rs/crate/actix-form-data/latest/status.svg)](https://deps.rs/crate/actix-form-data) | Multipart form data from actix multipart streams. |
| [actix-governor] | [![crates.io](https://img.shields.io/crates/v/actix-governor?label=latest)][actix-governor] [![dependency status](https://deps.rs/crate/actix-governor/latest/status.svg)](https://deps.rs/crate/actix-governor) | Rate-limiting backed by governor. |
| [actix-casbin] | [![crates.io](https://img.shields.io/crates/v/actix-casbin?label=latest)][actix-casbin] [![dependency status](https://deps.rs/crate/actix-casbin/latest/status.svg)](https://deps.rs/crate/actix-casbin) | Authorization library that supports access control models like ACL, RBAC & ABAC. |
| [actix-ip-filter] | [![crates.io](https://img.shields.io/crates/v/actix-ip-filter?label=latest)][actix-ip-filter] [![dependency status](https://deps.rs/crate/actix-ip-filter/latest/status.svg)](https://deps.rs/crate/actix-ip-filter) | IP address filter. Supports glob patterns. |
| [actix-web-static-files] | [![crates.io](https://img.shields.io/crates/v/actix-web-static-files?label=latest)][actix-web-static-files] [![dependency status](https://deps.rs/crate/actix-web-static-files/latest/status.svg)](https://deps.rs/crate/actix-web-static-files) | Static files as embedded resources. |
| [actix-web-grants] | [![crates.io](https://img.shields.io/crates/v/actix-web-grants?label=latest)][actix-web-grants] [![dependency status](https://deps.rs/crate/actix-web-grants/latest/status.svg)](https://deps.rs/crate/actix-web-grants) | Extension for validating user authorities. |
| [aliri_actix] | [![crates.io](https://img.shields.io/crates/v/aliri_actix?label=latest)][aliri_actix] [![dependency status](https://deps.rs/crate/aliri_actix/latest/status.svg)](https://deps.rs/crate/aliri_actix) | Endpoint authorization and authentication using scoped OAuth2 JWT tokens. |
| [actix-web-flash-messages] | [![crates.io](https://img.shields.io/crates/v/actix-web-flash-messages?label=latest)][actix-web-flash-messages] [![dependency status](https://deps.rs/crate/actix-web-flash-messages/latest/status.svg)](https://deps.rs/crate/actix-web-flash-messages) | Support for flash messages/one-time notifications in `actix-web`. |
| [awmp] | [![crates.io](https://img.shields.io/crates/v/awmp?label=latest)][awmp] [![dependency status](https://deps.rs/crate/awmp/latest/status.svg)](https://deps.rs/crate/awmp) | An easy to use wrapper around multipart fields for Actix Web. |
| [tracing-actix-web] | [![crates.io](https://img.shields.io/crates/v/tracing-actix-web?label=latest)][tracing-actix-web] [![dependency status](https://deps.rs/crate/tracing-actix-web/latest/status.svg)](https://deps.rs/crate/tracing-actix-web) | A middleware to collect telemetry data from applications built on top of the Actix Web framework. |
| [actix-hash] | [![crates.io](https://img.shields.io/crates/v/actix-hash?label=latest)][actix-hash] [![dependency status](https://deps.rs/crate/actix-hash/latest/status.svg)](https://deps.rs/crate/actix-hash) | Hashing utilities for Actix Web. |
| [actix-bincode] | ![crates.io](https://img.shields.io/crates/v/actix-bincode?label=latest) [![dependency status](https://deps.rs/crate/actix-bincode/latest/status.svg)](https://deps.rs/crate/actix-bincode) | Bincode payload extractor for Actix Web. |
| [sentinel-actix] | ![crates.io](https://img.shields.io/crates/v/sentinel-actix?label=latest) [![dependency status](https://deps.rs/crate/sentinel-actix/latest/status.svg)](https://deps.rs/crate/sentinel-actix) | General and flexible protection for Actix Web. |
| [actix-telepathy] | ![crates.io](https://img.shields.io/crates/v/actix-telepathy?label=latest) [![dependency status](https://deps.rs/crate/actix-telepathy/latest/status.svg)](https://deps.rs/crate/actix-telepathy) | Build distributed applications with `RemoteActors` and `RemoteMessages`. |
| [apistos] | ![crates.io](https://img.shields.io/crates/v/apistos?label=latest) [![dependency status](https://deps.rs/crate/apistos/latest/status.svg)](https://deps.rs/crate/apistos) | Automatic OpenAPI v3 documentation for Actix Web. |
| [actix-web-validation] | ![crates.io](https://img.shields.io/crates/v/actix-web-validation?label=latest) [![dependency status](https://deps.rs/crate/actix-web-validation/latest/status.svg)](https://deps.rs/crate/actix-web-validation) | Request validation for Actix Web. |
| [actix-jwt-cookies] | ![crates.io](https://img.shields.io/crates/v/actix-jwt-cookies?label=latest) [![dependency status](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies/status.svg)](https://deps.rs/repo/github/Necoo33/actix-jwt-cookies?path=%2F) | Store your data in encrypted cookies and get it elegantly. |
| [actix-ws-broadcaster] | ![crates.io](https://img.shields.io/crates/v/actix-ws-broadcaster?label=latest) [![dependency status](https://deps.rs/repo/github/Necoo33/actix-ws-broadcaster/status.svg?path=%2F)](https://deps.rs/repo/github/Necoo33/actix-ws-broadcaster?path=%2F) | A broadcaster library for actix-ws that includes grouping and conditional broadcasting. |
To add a crate to this list, submit a pull request.
<!-- REFERENCES -->
[actix]: https://github.com/actix/actix
[actix-web]: https://github.com/actix/actix-web
[actix web]: https://github.com/actix/actix-web
[actix-extras]: https://github.com/actix/actix-extras
[actix-cors]: actix-cors
[actix-identity]: actix-identity
[actix-protobuf]: actix-protobuf
[actix-redis]: actix-redis
[actix-session]: actix-session
[actix-web-httpauth]: actix-web-httpauth
[actix-cors]: ./actix-cors
[actix-identity]: ./actix-identity
[actix-limitation]: ./actix-limitation
[actix-protobuf]: ./actix-protobuf
[actix-session]: ./actix-session
[actix-settings]: ./actix-settings
[actix-web-httpauth]: ./actix-web-httpauth
[actix-web-lab]: https://crates.io/crates/actix-web-lab
[actix-multipart-extract]: https://crates.io/crates/actix-multipart-extract
[actix-form-data]: https://crates.io/crates/actix-form-data
[actix-casbin]: https://crates.io/crates/actix-casbin
[actix-ip-filter]: https://crates.io/crates/actix-ip-filter
[actix-web-static-files]: https://crates.io/crates/actix-web-static-files
[actix-web-grants]: https://crates.io/crates/actix-web-grants
[actix-web-flash-messages]: https://crates.io/crates/actix-web-flash-messages
[actix-governor]: https://crates.io/crates/actix-governor
[aliri_actix]: https://crates.io/crates/aliri_actix
[awmp]: https://crates.io/crates/awmp
[tracing-actix-web]: https://crates.io/crates/tracing-actix-web
[actix-ws]: https://crates.io/crates/actix-ws
[actix-hash]: https://crates.io/crates/actix-hash
[actix-bincode]: https://crates.io/crates/actix-bincode
[sentinel-actix]: https://crates.io/crates/sentinel-actix
[actix-telepathy]: https://crates.io/crates/actix-telepathy
[actix-web-validation]: https://crates.io/crates/actix-web-validation
[actix-telepathy]: https://crates.io/crates/actix-telepathy
[apistos]: https://crates.io/crates/apistos
[actix-jwt-cookies]: https://crates.io/crates/actix-jwt-cookies
[actix-ws-broadcaster]: https://crates.io/crates/actix-ws-broadcaster

View File

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

View File

@ -1,25 +1,39 @@
[package]
name = "actix-cors"
version = "0.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Cross-origin resource sharing (CORS) for actix-web applications."
readme = "README.md"
keywords = ["cors", "web", "framework"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-extras.git"
documentation = "https://docs.rs/actix-cors/"
license = "MIT/Apache-2.0"
edition = "2018"
version = "0.7.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Cross-Origin Resource Sharing (CORS) controls for Actix Web"
keywords = ["actix", "cors", "web", "security", "crossorigin"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[lib]
name = "actix_cors"
path = "src/lib.rs"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features]
draft-private-network-access = []
[dependencies]
actix-web = "2.0.0"
actix-service = "1.0.1"
derive_more = "0.99.2"
futures = "0.3.1"
actix-utils = "3"
actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display", "error"] }
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
log = "0.4"
once_cell = "1"
smallvec = "1"
[dev-dependencies]
actix-rt = "1.0.0"
actix-web = { version = "4", default-features = false, features = ["macros"] }
env_logger = "0.11"
regex = "1.4"
[lints]
workspace = true

View File

@ -1,17 +1,72 @@
# actix-cors
[![crates.io](https://img.shields.io/crates/v/actix-cors)](https://crates.io/crates/actix-cors)
[![Documentation](https://docs.rs/actix-cors/badge.svg)](https://docs.rs/actix-cors)
[![Dependency Status](https://deps.rs/crate/actix-cors/0.2.0/status.svg)](https://deps.rs/crate/actix-cors/0.2.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-cors)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
<!-- prettier-ignore-start -->
> Cross-origin resource sharing (CORS) for Actix applications.
[![crates.io](https://img.shields.io/crates/v/actix-cors?label=latest)](https://crates.io/crates/actix-cors)
[![Documentation](https://docs.rs/actix-cors/badge.svg?version=0.7.1)](https://docs.rs/actix-cors/0.7.1)
![Version](https://img.shields.io/badge/rustc-1.75+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-cors.svg)
<br />
[![Dependency Status](https://deps.rs/crate/actix-cors/0.7.1/status.svg)](https://deps.rs/crate/actix-cors/0.7.1)
[![Download](https://img.shields.io/crates/d/actix-cors.svg)](https://crates.io/crates/actix-cors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & community resources
<!-- prettier-ignore-end -->
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-cors/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-cors](https://crates.io/crates/actix-cors)
* Minimum supported Rust version: 1.39.0 or later
<!-- 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
- [API Documentation](https://docs.rs/actix-cors)
- [Example Project](https://github.com/actix/examples/tree/master/cors)
- Minimum Supported Rust Version (MSRV): 1.75

View File

@ -0,0 +1,54 @@
use actix_cors::Cors;
use actix_web::{http::header, middleware::Logger, web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(move || {
App::new()
// `permissive` is a wide-open development config
// .wrap(Cors::permissive())
.wrap(
// default settings are overly restrictive to reduce chance of
// misconfiguration leading to security concerns
Cors::default()
// add specific origin to allowed origin list
.allowed_origin("http://project.local:8080")
// allow any port on localhost
.allowed_origin_fn(|origin, _req_head| {
origin.as_bytes().starts_with(b"http://localhost")
// manual alternative:
// unwrapping is acceptable on the origin header since this function is
// only called when it exists
// req_head
// .headers()
// .get(header::ORIGIN)
// .unwrap()
// .as_bytes()
// .starts_with(b"http://localhost")
})
// set allowed methods list
.allowed_methods(vec!["GET", "POST"])
// set allowed request header list
.allowed_headers(&[header::AUTHORIZATION, header::ACCEPT])
// add header to allowed list
.allowed_header(header::CONTENT_TYPE)
// set list of headers that are safe to expose
.expose_headers(&[header::CONTENT_DISPOSITION])
// allow cURL/HTTPie from working without providing Origin headers
.block_on_origin_mismatch(false)
// set preflight cache TTL
.max_age(3600),
)
.wrap(Logger::default())
.default_service(web::to(|| async { "Hello, cross-origin world!" }))
})
.workers(1)
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@ -0,0 +1,55 @@
/// An enum signifying that some of type `T` is allowed, or `All` (anything is allowed).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllOrSome<T> {
/// Everything is allowed. Usually equivalent to the `*` value.
All,
/// Only some of `T` is allowed
Some(T),
}
/// Default as `AllOrSome::All`.
impl<T> Default for AllOrSome<T> {
fn default() -> Self {
AllOrSome::All
}
}
impl<T> AllOrSome<T> {
/// Returns whether this is an `All` variant.
pub fn is_all(&self) -> bool {
matches!(self, AllOrSome::All)
}
/// Returns whether this is a `Some` variant.
#[allow(dead_code)]
pub fn is_some(&self) -> bool {
!self.is_all()
}
/// Provides a shared reference to `T` if variant is `Some`.
pub fn as_ref(&self) -> Option<&T> {
match *self {
AllOrSome::All => None,
AllOrSome::Some(ref t) => Some(t),
}
}
/// Provides a mutable reference to `T` if variant is `Some`.
pub fn as_mut(&mut self) -> Option<&mut T> {
match *self {
AllOrSome::All => None,
AllOrSome::Some(ref mut t) => Some(t),
}
}
}
#[cfg(test)]
#[test]
fn tests() {
assert!(AllOrSome::<()>::All.is_all());
assert!(!AllOrSome::<()>::All.is_some());
assert!(!AllOrSome::Some(()).is_all());
assert!(AllOrSome::Some(()).is_some());
}

702
actix-cors/src/builder.rs Normal file
View File

@ -0,0 +1,702 @@
use std::{collections::HashSet, rc::Rc};
use actix_utils::future::{self, Ready};
use actix_web::{
body::{EitherBody, MessageBody},
dev::{RequestHead, Service, ServiceRequest, ServiceResponse, Transform},
error::HttpError,
http::{
header::{HeaderName, HeaderValue},
Method, Uri,
},
Either, Error, Result,
};
use log::error;
use once_cell::sync::Lazy;
use smallvec::smallvec;
use crate::{AllOrSome, CorsError, CorsMiddleware, Inner, OriginFn};
/// Convenience for getting mut refs to inner. Cleaner than `Rc::get_mut`.
/// Additionally, always causes first error (if any) to be reported during initialization.
fn cors<'a>(
inner: &'a mut Rc<Inner>,
err: &Option<Either<HttpError, CorsError>>,
) -> Option<&'a mut Inner> {
if err.is_some() {
return None;
}
Rc::get_mut(inner)
}
static ALL_METHODS_SET: Lazy<HashSet<Method>> = Lazy::new(|| {
HashSet::from_iter(vec![
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::HEAD,
Method::OPTIONS,
Method::CONNECT,
Method::PATCH,
Method::TRACE,
])
});
/// Builder for CORS middleware.
///
/// To construct a CORS middleware, call [`Cors::default()`] to create a blank, restrictive builder.
/// Then use any of the builder methods to customize CORS behavior.
///
/// 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.**
///
/// # 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 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
/// 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.
///
/// # Example
///
/// ```
/// use actix_cors::Cors;
/// use actix_web::http::header;
///
/// let cors = Cors::default()
/// .allowed_origin("https://www.rust-lang.org")
/// .allowed_methods(vec!["GET", "POST"])
/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
/// .allowed_header(header::CONTENT_TYPE)
/// .max_age(3600);
///
/// // `cors` can now be used in `App::wrap`.
/// ```
///
/// [Fetch Standard CORS protocol]: https://fetch.spec.whatwg.org/#http-cors-protocol
#[derive(Debug)]
#[must_use]
pub struct Cors {
inner: Rc<Inner>,
error: Option<Either<HttpError, CorsError>>,
}
impl Cors {
/// 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.
/// Max age 1 hour. Does not send wildcard.
pub fn permissive() -> Self {
let inner = Inner {
allowed_origins: AllOrSome::All,
allowed_origins_fns: smallvec![],
allowed_methods: ALL_METHODS_SET.clone(),
allowed_methods_baked: None,
allowed_headers: AllOrSome::All,
allowed_headers_baked: None,
expose_headers: AllOrSome::All,
expose_headers_baked: None,
max_age: Some(3600),
preflight: true,
send_wildcard: false,
supports_credentials: true,
#[cfg(feature = "draft-private-network-access")]
allow_private_network_access: false,
vary_header: true,
block_on_origin_mismatch: false,
};
Cors {
inner: Rc::new(inner),
error: None,
}
}
/// Resets allowed origin list to a state where any origin is accepted.
///
/// See [`Cors::allowed_origin`] for more info on allowed origins.
pub fn allow_any_origin(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.allowed_origins = AllOrSome::All;
}
self
}
/// 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`
/// request header. These are `origin-or-null` types in the [Fetch Standard].
///
/// By default, no origins are accepted.
///
/// When this list is set, the client's `Origin` request header will be checked in a
/// case-sensitive manner.
///
/// When all origins are allowed and `send_wildcard` is set, `*` will be sent in the
/// `Access-Control-Allow-Origin` response header. If `send_wildcard` is not set, the client's
/// `Origin` request header will be echoed back in the `Access-Control-Allow-Origin`
/// response header.
///
/// If the origin of the request doesn't match any allowed origins and at least one
/// `allowed_origin_fn` function is set, these functions will be used to determinate
/// allowed origins.
///
/// # Initialization Errors
/// - If supplied origin is not valid uri
/// - If supplied origin is a wildcard (`*`). [`Cors::send_wildcard`] should be used instead.
///
/// [Fetch Standard]: https://fetch.spec.whatwg.org/#origin-header
pub fn allowed_origin(mut self, origin: &str) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
match TryInto::<Uri>::try_into(origin) {
Ok(_) if origin == "*" => {
error!("Wildcard in `allowed_origin` is not allowed. Use `send_wildcard`.");
self.error = Some(Either::Right(CorsError::WildcardOrigin));
}
Ok(_) => {
if cors.allowed_origins.is_all() {
cors.allowed_origins = AllOrSome::Some(HashSet::with_capacity(8));
}
if let Some(origins) = cors.allowed_origins.as_mut() {
// any uri is a valid header value
let hv = origin.try_into().unwrap();
origins.insert(hv);
}
}
Err(err) => {
self.error = Some(Either::Left(err.into()));
}
}
}
self
}
/// Determinates allowed origins by processing requests which didn't match any origins specified
/// in the `allowed_origin`.
///
/// The function will receive two parameters, the Origin header value, and the `RequestHead` of
/// each request, which can be used to determine whether to allow the request or not.
///
/// If the function returns `true`, the client's `Origin` request header will be echoed back
/// into the `Access-Control-Allow-Origin` response header.
pub fn allowed_origin_fn<F>(mut self, f: F) -> Cors
where
F: (Fn(&HeaderValue, &RequestHead) -> bool) + 'static,
{
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.allowed_origins_fns.push(OriginFn {
boxed_fn: Rc::new(f),
});
}
self
}
/// Resets allowed methods list to all methods.
///
/// See [`Cors::allowed_methods`] for more info on allowed methods.
pub fn allow_any_method(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
ALL_METHODS_SET.clone_into(&mut cors.allowed_methods);
}
self
}
/// Sets a list of methods which allowed origins can perform.
///
/// These will be sent in the `Access-Control-Allow-Methods` response header.
///
/// This defaults to an empty set.
pub fn allowed_methods<U, M>(mut self, methods: U) -> Cors
where
U: IntoIterator<Item = M>,
M: TryInto<Method>,
<M as TryInto<Method>>::Error: Into<HttpError>,
{
if let Some(cors) = cors(&mut self.inner, &self.error) {
for m in methods {
match m.try_into() {
Ok(method) => {
cors.allowed_methods.insert(method);
}
Err(err) => {
self.error = Some(Either::Left(err.into()));
break;
}
}
}
}
self
}
/// Resets allowed request header list to a state where any header is accepted.
///
/// See [`Cors::allowed_headers`] for more info on allowed request headers.
pub fn allow_any_header(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.allowed_headers = AllOrSome::All;
}
self
}
/// Add an allowed request header.
///
/// See [`Cors::allowed_headers`] for more info on allowed request headers.
pub fn allowed_header<H>(mut self, header: H) -> Cors
where
H: TryInto<HeaderName>,
<H as TryInto<HeaderName>>::Error: Into<HttpError>,
{
if let Some(cors) = cors(&mut self.inner, &self.error) {
match header.try_into() {
Ok(method) => {
if cors.allowed_headers.is_all() {
cors.allowed_headers = AllOrSome::Some(HashSet::with_capacity(8));
}
if let AllOrSome::Some(ref mut headers) = cors.allowed_headers {
headers.insert(method);
}
}
Err(err) => self.error = Some(Either::Left(err.into())),
}
}
self
}
/// Sets a list of request header field names which can be used when this resource is accessed
/// by allowed origins.
///
/// 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.
///
/// This defaults to an empty set.
pub fn allowed_headers<U, H>(mut self, headers: U) -> Cors
where
U: IntoIterator<Item = H>,
H: TryInto<HeaderName>,
<H as TryInto<HeaderName>>::Error: Into<HttpError>,
{
if let Some(cors) = cors(&mut self.inner, &self.error) {
for h in headers {
match h.try_into() {
Ok(method) => {
if cors.allowed_headers.is_all() {
cors.allowed_headers = AllOrSome::Some(HashSet::with_capacity(8));
}
if let AllOrSome::Some(ref mut headers) = cors.allowed_headers {
headers.insert(method);
}
}
Err(err) => {
self.error = Some(Either::Left(err.into()));
break;
}
}
}
}
self
}
/// Resets exposed response header list to a state where all headers are exposed.
///
/// See [`Cors::expose_headers`] for more info on exposed response headers.
pub fn expose_any_header(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.expose_headers = AllOrSome::All;
}
self
}
/// 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.
///
/// This defaults to an empty set.
pub fn expose_headers<U, H>(mut self, headers: U) -> Cors
where
U: IntoIterator<Item = H>,
H: TryInto<HeaderName>,
<H as TryInto<HeaderName>>::Error: Into<HttpError>,
{
for h in headers {
match h.try_into() {
Ok(header) => {
if let Some(cors) = cors(&mut self.inner, &self.error) {
if cors.expose_headers.is_all() {
cors.expose_headers = AllOrSome::Some(HashSet::with_capacity(8));
}
if let AllOrSome::Some(ref mut headers) = cors.expose_headers {
headers.insert(header);
}
}
}
Err(err) => {
self.error = Some(Either::Left(err.into()));
break;
}
}
}
self
}
/// Sets a maximum time (in seconds) for which this CORS request may be cached.
///
/// 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.
pub fn max_age(mut self, max_age: impl Into<Option<usize>>) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.max_age = max_age.into();
}
self
}
/// Configures use of wildcard (`*`) origin in responses when appropriate.
///
/// If send wildcard is set and the `allowed_origins` parameter is `All`, a wildcard
/// `Access-Control-Allow-Origin` response header is sent, rather than the requests
/// `Origin` header.
///
/// This option **CANNOT** be used in conjunction with a [credential
/// supported](Self::supports_credentials()) configuration. Doing so will result in an error
/// during server startup.
///
/// Defaults to disabled.
pub fn send_wildcard(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.send_wildcard = true;
}
self
}
/// Allows users to make authenticated requests.
///
/// If true, injects the `Access-Control-Allow-Credentials` header in responses. This allows
/// cookies and credentials to be submitted across domains.
///
/// This option **CANNOT** be used in conjunction with option cannot be used in conjunction
/// with [wildcard origins](Self::send_wildcard()) configured. Doing so will result in an error
/// during server startup.
///
/// Defaults to disabled.
pub fn supports_credentials(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.supports_credentials = true;
}
self
}
/// Allow private network access.
///
/// If true, injects the `Access-Control-Allow-Private-Network: true` header in responses if the
/// request contained the `Access-Control-Request-Private-Network: true` header.
///
/// For more information on this behavior, see the draft [Private Network Access] spec.
///
/// Defaults to `false`.
///
/// [Private Network Access]: https://wicg.github.io/private-network-access
#[cfg(feature = "draft-private-network-access")]
pub fn allow_private_network_access(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.allow_private_network_access = true;
}
self
}
/// Disables `Vary` header support.
///
/// When enabled the header `Vary: Origin` will be returned as per the Fetch Standard
/// implementation guidelines.
///
/// Setting this header when the `Access-Control-Allow-Origin` is dynamically generated
/// (eg. when there is more than one allowed origin, and an Origin other than '*' is returned)
/// informs CDNs and other caches that the CORS headers are dynamic, and cannot be cached.
///
/// By default, `Vary` header support is enabled.
pub fn disable_vary_header(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.vary_header = false;
}
self
}
/// Disables preflight request handling.
///
/// When enabled CORS middleware automatically handles `OPTIONS` requests. This is useful for
/// application level middleware.
///
/// By default, preflight support is enabled.
pub fn disable_preflight(mut self) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.preflight = false;
}
self
}
/// Configures whether requests should be pre-emptively blocked on mismatched origin.
///
/// If `true`, a 400 Bad Request is returned immediately when a request fails origin validation.
///
/// If `false`, the request will be processed as normal but relevant CORS headers will not be
/// appended to the response. In this case, the browser is trusted to validate CORS headers and
/// and block requests based on pre-flight requests. Use this setting to allow cURL and other
/// non-browser HTTP clients to function as normal, no matter what `Origin` the request has.
///
/// Defaults to false.
pub fn block_on_origin_mismatch(mut self, block: bool) -> Cors {
if let Some(cors) = cors(&mut self.inner, &self.error) {
cors.block_on_origin_mismatch = block;
}
self
}
}
impl Default for Cors {
/// A restrictive (security paranoid) set of defaults.
///
/// *No* allowed origins, methods, request headers or exposed headers. Credentials
/// not supported. No max age (will use browser's default).
fn default() -> Cors {
let inner = Inner {
allowed_origins: AllOrSome::Some(HashSet::with_capacity(8)),
allowed_origins_fns: smallvec![],
allowed_methods: HashSet::with_capacity(8),
allowed_methods_baked: None,
allowed_headers: AllOrSome::Some(HashSet::with_capacity(8)),
allowed_headers_baked: None,
expose_headers: AllOrSome::Some(HashSet::with_capacity(8)),
expose_headers_baked: None,
max_age: None,
preflight: true,
send_wildcard: false,
supports_credentials: false,
#[cfg(feature = "draft-private-network-access")]
allow_private_network_access: false,
vary_header: true,
block_on_origin_mismatch: false,
};
Cors {
inner: Rc::new(inner),
error: None,
}
}
}
impl<S, B> Transform<S, ServiceRequest> for Cors
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = CorsMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
if let Some(ref err) = self.error {
match err {
Either::Left(err) => error!("{}", err),
Either::Right(err) => error!("{}", err),
}
return future::err(());
}
let mut inner = Rc::clone(&self.inner);
if inner.supports_credentials && inner.send_wildcard && inner.allowed_origins.is_all() {
error!(
"Illegal combination of CORS options: credentials can not be supported when all \
origins are allowed and `send_wildcard` is enabled."
);
return future::err(());
}
// bake allowed headers value if Some and not empty
match inner.allowed_headers.as_ref() {
Some(header_set) if !header_set.is_empty() => {
let allowed_headers_str = intersperse_header_values(header_set);
Rc::make_mut(&mut inner).allowed_headers_baked = Some(allowed_headers_str);
}
_ => {}
}
// bake allowed methods value if not empty
if !inner.allowed_methods.is_empty() {
let allowed_methods_str = intersperse_header_values(&inner.allowed_methods);
Rc::make_mut(&mut inner).allowed_methods_baked = Some(allowed_methods_str);
}
// bake exposed headers value if Some and not empty
match inner.expose_headers.as_ref() {
Some(header_set) if !header_set.is_empty() => {
let expose_headers_str = intersperse_header_values(header_set);
Rc::make_mut(&mut inner).expose_headers_baked = Some(expose_headers_str);
}
_ => {}
}
future::ok(CorsMiddleware { service, inner })
}
}
/// Only call when values are guaranteed to be valid header values and set is not empty.
pub(crate) fn intersperse_header_values<T>(val_set: &HashSet<T>) -> HeaderValue
where
T: AsRef<str>,
{
debug_assert!(
!val_set.is_empty(),
"only call `intersperse_header_values` when set is not empty"
);
val_set
.iter()
.fold(String::with_capacity(64), |mut acc, val| {
acc.push_str(", ");
acc.push_str(val.as_ref());
acc
})
// set is not empty so string will always have leading ", " to trim
[2..]
.try_into()
// all method names are valid header values
.unwrap()
}
impl PartialEq for Cors {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
// Because of the cors-function, checking if the content is equal implies that the errors are equal
//
// Proof by contradiction:
// Lets assume that the inner values are equal, but the error values are not.
// This means there had been an error, which has been fixed.
// This cannot happen as the first call to set the invalid value means that further usages of the cors-function will reject other input.
// => inner has to be in a different state
}
}
#[cfg(test)]
mod test {
use std::convert::Infallible;
use actix_web::{
body,
dev::fn_service,
http::StatusCode,
test::{self, TestRequest},
HttpResponse,
};
use super::*;
#[test]
fn illegal_allow_credentials() {
// using the permissive defaults (all origins allowed) and adding send_wildcard
// and supports_credentials should error on construction
assert!(Cors::permissive()
.supports_credentials()
.send_wildcard()
.new_transform(test::ok_service())
.into_inner()
.is_err());
}
#[actix_web::test]
async fn restrictive_defaults() {
let cors = Cors::default()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res.headers().contains_key("Access-Control-Allow-Origin"));
}
#[actix_web::test]
async fn allowed_header_try_from() {
let _cors = Cors::default().allowed_header("Content-Type");
}
#[actix_web::test]
async fn allowed_header_try_into() {
struct ContentType;
impl TryInto<HeaderName> for ContentType {
type Error = Infallible;
fn try_into(self) -> Result<HeaderName, Self::Error> {
Ok(HeaderName::from_static("content-type"))
}
}
let _cors = Cors::default().allowed_header(ContentType);
}
#[actix_web::test]
async fn middleware_generic_over_body_type() {
let srv = fn_service(|req: ServiceRequest| async move {
Ok(req.into_response(HttpResponse::with_body(StatusCode::OK, body::None::new())))
});
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());
}
}

49
actix-cors/src/error.rs Normal file
View File

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

409
actix-cors/src/inner.rs Normal file
View File

@ -0,0 +1,409 @@
use std::{collections::HashSet, fmt, rc::Rc};
use actix_web::{
dev::RequestHead,
error::Result,
http::{
header::{self, HeaderMap, HeaderName, HeaderValue},
Method,
},
};
use once_cell::sync::Lazy;
use smallvec::SmallVec;
use crate::{AllOrSome, CorsError};
#[derive(Clone)]
pub(crate) struct OriginFn {
#[allow(clippy::type_complexity)]
pub(crate) boxed_fn: Rc<dyn Fn(&HeaderValue, &RequestHead) -> bool>,
}
impl Default for OriginFn {
/// Dummy default for use in tiny_vec. Do not use.
fn default() -> Self {
let boxed_fn: Rc<dyn Fn(&_, &_) -> _> = Rc::new(|_origin, _req_head| false);
Self { boxed_fn }
}
}
impl PartialEq for OriginFn {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.boxed_fn, &other.boxed_fn)
}
}
impl fmt::Debug for OriginFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("origin_fn")
}
}
/// Try to parse header value as HTTP method.
pub(crate) fn header_value_try_into_method(hdr: &HeaderValue) -> Option<Method> {
hdr.to_str()
.ok()
.and_then(|meth| Method::try_from(meth).ok())
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Inner {
pub(crate) allowed_origins: AllOrSome<HashSet<HeaderValue>>,
pub(crate) allowed_origins_fns: SmallVec<[OriginFn; 4]>,
pub(crate) allowed_methods: HashSet<Method>,
pub(crate) allowed_methods_baked: Option<HeaderValue>,
pub(crate) allowed_headers: AllOrSome<HashSet<HeaderName>>,
pub(crate) allowed_headers_baked: Option<HeaderValue>,
/// `All` will echo back `Access-Control-Request-Header` list.
pub(crate) expose_headers: AllOrSome<HashSet<HeaderName>>,
pub(crate) expose_headers_baked: Option<HeaderValue>,
pub(crate) max_age: Option<usize>,
pub(crate) preflight: bool,
pub(crate) send_wildcard: bool,
pub(crate) supports_credentials: bool,
#[cfg(feature = "draft-private-network-access")]
pub(crate) allow_private_network_access: bool,
pub(crate) vary_header: bool,
pub(crate) block_on_origin_mismatch: bool,
}
static EMPTY_ORIGIN_SET: Lazy<HashSet<HeaderValue>> = Lazy::new(HashSet::new);
impl Inner {
/// The bool returned in Ok(_) position indicates whether the `Access-Control-Allow-Origin`
/// header should be added to the response or not.
pub(crate) fn validate_origin(&self, req: &RequestHead) -> Result<bool, CorsError> {
// return early if all origins are allowed or get ref to allowed origins set
#[allow(clippy::mutable_key_type)]
let allowed_origins = match &self.allowed_origins {
AllOrSome::All if self.allowed_origins_fns.is_empty() => return Ok(true),
AllOrSome::Some(allowed_origins) => allowed_origins,
// only function origin validators are defined
_ => &EMPTY_ORIGIN_SET,
};
// get origin header and try to parse as string
match req.headers().get(header::ORIGIN) {
// origin header exists and is a string
Some(origin) => {
if allowed_origins.contains(origin) || self.validate_origin_fns(origin, req) {
Ok(true)
} else if self.block_on_origin_mismatch {
Err(CorsError::OriginNotAllowed)
} else {
Ok(false)
}
}
// origin header is missing
// note: with our implementation, the origin header is required for OPTIONS request or
// else this would be unreachable
None => Err(CorsError::MissingOrigin),
}
}
/// Accepts origin if _ANY_ functions return true. Only called when Origin exists.
fn validate_origin_fns(&self, origin: &HeaderValue, req: &RequestHead) -> bool {
self.allowed_origins_fns
.iter()
.any(|origin_fn| (origin_fn.boxed_fn)(origin, req))
}
/// Only called if origin exists and always after it's validated.
pub(crate) fn access_control_allow_origin(&self, req: &RequestHead) -> Option<HeaderValue> {
let origin = req.headers().get(header::ORIGIN);
match self.allowed_origins {
AllOrSome::All => {
if self.send_wildcard {
Some(HeaderValue::from_static("*"))
} else {
// see note below about why `.cloned()` is correct
origin.cloned()
}
}
AllOrSome::Some(_) => {
// since origin (if it exists) is known to be allowed if this method is called
// then cloning the option is all that is required to be used as an echoed back
// header value (or omitted if None)
origin.cloned()
}
}
}
/// Use in preflight checks and therefore operates on header list in
/// `Access-Control-Request-Headers` not the actual header set.
pub(crate) fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> {
// extract access control header and try to parse as method
let request_method = req
.headers()
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
.map(header_value_try_into_method);
match request_method {
// method valid and allowed
Some(Some(method)) if self.allowed_methods.contains(&method) => Ok(()),
// method valid but not allowed
Some(Some(_)) => Err(CorsError::MethodNotAllowed),
// method invalid
Some(_) => Err(CorsError::BadRequestMethod),
// method missing so this is not a preflight request
None => Err(CorsError::MissingRequestMethod),
}
}
pub(crate) fn validate_allowed_headers(&self, req: &RequestHead) -> Result<(), CorsError> {
// return early if all headers are allowed or get ref to allowed origins set
#[allow(clippy::mutable_key_type)]
let allowed_headers = match &self.allowed_headers {
AllOrSome::All => return Ok(()),
AllOrSome::Some(allowed_headers) => allowed_headers,
};
// extract access control header as string
// header format should be comma separated header names
let request_headers = req
.headers()
.get(header::ACCESS_CONTROL_REQUEST_HEADERS)
.map(|hdr| hdr.to_str());
match request_headers {
// header list is valid string
Some(Ok(headers)) => {
// the set is ephemeral we take care not to mutate the
// inserted keys so this lint exception is acceptable
#[allow(clippy::mutable_key_type)]
let mut request_headers = HashSet::with_capacity(8);
// try to convert each header name in the comma-separated list
for hdr in headers.split(',') {
match hdr.trim().try_into() {
Ok(hdr) => request_headers.insert(hdr),
Err(_) => return Err(CorsError::BadRequestHeaders),
};
}
// header list must contain 1 or more header name
if request_headers.is_empty() {
return Err(CorsError::BadRequestHeaders);
}
// request header list must be a subset of allowed headers
if !request_headers.is_subset(allowed_headers) {
return Err(CorsError::HeadersNotAllowed);
}
Ok(())
}
// header list is not a string
Some(Err(_)) => Err(CorsError::BadRequestHeaders),
// header list missing
None => Ok(()),
}
}
}
/// Add CORS related request headers to response's Vary header.
///
/// See <https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches>.
pub(crate) fn add_vary_header(headers: &mut HeaderMap) {
let value = match headers.get(header::VARY) {
Some(hdr) => {
let mut val: Vec<u8> = Vec::with_capacity(hdr.len() + 71);
val.extend(hdr.as_bytes());
val.extend(b", Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
#[cfg(feature = "draft-private-network-access")]
val.extend(b", Access-Control-Request-Private-Network");
val.try_into().unwrap()
}
#[cfg(feature = "draft-private-network-access")]
None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, \
Access-Control-Request-Private-Network",
),
#[cfg(not(feature = "draft-private-network-access"))]
None => HeaderValue::from_static(
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
),
};
headers.insert(header::VARY, value);
}
#[cfg(test)]
mod test {
use std::rc::Rc;
use actix_web::{
dev::Transform,
http::{
header::{self, HeaderValue},
Method, StatusCode,
},
test::{self, TestRequest},
};
use crate::Cors;
fn val_as_str(val: &HeaderValue) -> &str {
val.to_str().unwrap()
}
#[actix_web::test]
async fn test_validate_not_allowed_origin() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.block_on_origin_mismatch(true)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header((header::ORIGIN, "https://www.unknown.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "DNT"))
.to_srv_request();
assert!(cors.inner.validate_origin(req.head()).is_err());
assert!(cors.inner.validate_allowed_method(req.head()).is_err());
assert!(cors.inner.validate_allowed_headers(req.head()).is_err());
}
#[actix_web::test]
async fn test_preflight() {
let mut cors = Cors::default()
.allow_any_origin()
.send_wildcard()
.max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "X-Not-Allowed"))
.to_srv_request();
assert!(cors.inner.validate_allowed_method(req.head()).is_err());
assert!(cors.inner.validate_allowed_headers(req.head()).is_err());
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "put"))
.to_srv_request();
assert!(cors.inner.validate_allowed_method(req.head()).is_err());
assert!(cors.inner.validate_allowed_headers(req.head()).is_ok());
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((
header::ACCESS_CONTROL_REQUEST_HEADERS,
"AUTHORIZATION,ACCEPT",
))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"*"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
assert_eq!(
Some(&b"3600"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_MAX_AGE)
.map(HeaderValue::as_bytes)
);
let hdr = resp
.headers()
.get(header::ACCESS_CONTROL_ALLOW_HEADERS)
.map(val_as_str)
.unwrap();
assert!(hdr.contains("authorization"));
assert!(hdr.contains("accept"));
assert!(hdr.contains("content-type"));
let methods = resp
.headers()
.get(header::ACCESS_CONTROL_ALLOW_METHODS)
.unwrap()
.to_str()
.unwrap();
assert!(methods.contains("POST"));
assert!(methods.contains("GET"));
assert!(methods.contains("OPTIONS"));
Rc::get_mut(&mut cors.inner).unwrap().preflight = false;
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((
header::ACCESS_CONTROL_REQUEST_HEADERS,
"AUTHORIZATION,ACCEPT",
))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn allow_fn_origin_equals_head_origin() {
let cors = Cors::default()
.allowed_origin_fn(|origin, head| {
let head_origin = head
.headers()
.get(header::ORIGIN)
.expect("unwrapping origin header should never fail in allowed_origin_fn");
assert!(origin == head_origin);
true
})
.allow_any_method()
.allow_any_header()
.new_transform(test::status_service(StatusCode::NO_CONTENT))
.await
.unwrap();
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::default()
.method(Method::GET)
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,300 @@
use std::{collections::HashSet, rc::Rc};
use actix_utils::future::ok;
use actix_web::{
body::{EitherBody, MessageBody},
dev::{forward_ready, Service, ServiceRequest, ServiceResponse},
http::{
header::{self, HeaderValue},
Method,
},
Error, HttpResponse, Result,
};
use futures_util::future::{FutureExt as _, LocalBoxFuture};
use log::debug;
use crate::{
builder::intersperse_header_values,
inner::{add_vary_header, header_value_try_into_method},
AllOrSome, CorsError, Inner,
};
/// Service wrapper for Cross-Origin Resource Sharing support.
///
/// This struct contains the settings for CORS requests to be validated and for responses to
/// be generated.
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct CorsMiddleware<S> {
pub(crate) service: S,
pub(crate) inner: Rc<Inner>,
}
impl<S> CorsMiddleware<S> {
/// Returns true if request is `OPTIONS` and contains an `Access-Control-Request-Method` header.
fn is_request_preflight(req: &ServiceRequest) -> bool {
// check request method is OPTIONS
if req.method() != Method::OPTIONS {
return false;
}
// check follow-up request method is present and valid
if req
.headers()
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
.and_then(header_value_try_into_method)
.is_none()
{
return false;
}
true
}
/// Validates preflight request headers against configuration and constructs preflight response.
///
/// Checks:
/// - `Origin` header is acceptable;
/// - `Access-Control-Request-Method` header is acceptable;
/// - `Access-Control-Request-Headers` header is acceptable.
fn handle_preflight(&self, req: ServiceRequest) -> ServiceResponse {
let inner = Rc::clone(&self.inner);
match inner.validate_origin(req.head()) {
Ok(true) => {}
Ok(false) => return req.error_response(CorsError::OriginNotAllowed),
Err(err) => return req.error_response(err),
};
if let Err(err) = inner
.validate_allowed_method(req.head())
.and_then(|_| inner.validate_allowed_headers(req.head()))
{
return req.error_response(err);
}
let mut res = HttpResponse::Ok();
if let Some(origin) = inner.access_control_allow_origin(req.head()) {
res.insert_header((header::ACCESS_CONTROL_ALLOW_ORIGIN, origin));
}
if let Some(ref allowed_methods) = inner.allowed_methods_baked {
res.insert_header((
header::ACCESS_CONTROL_ALLOW_METHODS,
allowed_methods.clone(),
));
}
if let Some(ref headers) = inner.allowed_headers_baked {
res.insert_header((header::ACCESS_CONTROL_ALLOW_HEADERS, headers.clone()));
} else if let Some(headers) = req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) {
// all headers allowed, return
res.insert_header((header::ACCESS_CONTROL_ALLOW_HEADERS, headers.clone()));
}
#[cfg(feature = "draft-private-network-access")]
if inner.allow_private_network_access
&& req
.headers()
.contains_key("access-control-request-private-network")
{
res.insert_header((
header::HeaderName::from_static("access-control-allow-private-network"),
HeaderValue::from_static("true"),
));
}
if inner.supports_credentials {
res.insert_header((
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
HeaderValue::from_static("true"),
));
}
if let Some(max_age) = inner.max_age {
res.insert_header((header::ACCESS_CONTROL_MAX_AGE, max_age.to_string()));
}
let mut res = res.finish();
if inner.vary_header {
add_vary_header(res.headers_mut());
}
req.into_response(res)
}
fn augment_response<B>(
inner: &Inner,
origin_allowed: bool,
mut res: ServiceResponse<B>,
) -> ServiceResponse<B> {
if origin_allowed {
if let Some(origin) = inner.access_control_allow_origin(res.request().head()) {
res.headers_mut()
.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin);
};
}
if let Some(ref expose) = inner.expose_headers_baked {
log::trace!("exposing selected headers: {:?}", expose);
res.headers_mut()
.insert(header::ACCESS_CONTROL_EXPOSE_HEADERS, expose.clone());
} else if matches!(inner.expose_headers, AllOrSome::All) {
// intersperse_header_values requires that argument is non-empty
if !res.headers().is_empty() {
// extract header names from request
let expose_all_request_headers = res
.headers()
.keys()
.map(|name| name.as_str())
.collect::<HashSet<_>>();
// create comma separated string of header names
let expose_headers_value = intersperse_header_values(&expose_all_request_headers);
log::trace!(
"exposing all headers from request: {:?}",
expose_headers_value
);
// add header names to expose response header
res.headers_mut()
.insert(header::ACCESS_CONTROL_EXPOSE_HEADERS, expose_headers_value);
}
}
if inner.supports_credentials {
res.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_CREDENTIALS,
HeaderValue::from_static("true"),
);
}
#[cfg(feature = "draft-private-network-access")]
if inner.allow_private_network_access
&& res
.request()
.headers()
.contains_key("access-control-request-private-network")
{
res.headers_mut().insert(
header::HeaderName::from_static("access-control-allow-private-network"),
HeaderValue::from_static("true"),
);
}
if inner.vary_header {
add_vary_header(res.headers_mut());
}
res
}
}
impl<S, B> Service<ServiceRequest> for CorsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let origin = req.headers().get(header::ORIGIN);
// handle preflight requests
if self.inner.preflight && Self::is_request_preflight(&req) {
let res = self.handle_preflight(req);
return ok(res.map_into_right_body()).boxed_local();
}
// only check actual requests with a origin header
let origin_allowed = match (origin, self.inner.validate_origin(req.head())) {
(None, _) => false,
(_, Ok(origin_allowed)) => origin_allowed,
(_, Err(err)) => {
debug!("origin validation failed; inner service is not called");
let mut res = req.error_response(err);
if self.inner.vary_header {
add_vary_header(res.headers_mut());
}
return ok(res.map_into_right_body()).boxed_local();
}
};
let inner = Rc::clone(&self.inner);
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await;
Ok(Self::augment_response(&inner, origin_allowed, res?).map_into_left_body())
})
}
}
#[cfg(test)]
mod tests {
use actix_web::{
dev::Transform,
middleware::Compat,
test::{self, TestRequest},
App,
};
use super::*;
use crate::Cors;
#[test]
fn compat_compat() {
let _ = App::new().wrap(Compat::new(Cors::default()));
}
#[actix_web::test]
async fn test_options_no_origin() {
// Tests case where allowed_origins is All but there are validate functions to run incase.
// In this case, origins are only allowed when the DNT header is sent.
let cors = Cors::default()
.allow_any_origin()
.allowed_origin_fn(|origin, req_head| {
assert_eq!(&origin, req_head.headers.get(header::ORIGIN).unwrap());
req_head.headers().contains_key(header::DNT)
})
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header((header::ORIGIN, "http://example.com"))
.to_srv_request();
let res = cors.call(req).await.unwrap();
assert_eq!(
None,
res.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
let req = TestRequest::get()
.insert_header((header::ORIGIN, "http://example.com"))
.insert_header((header::DNT, "1"))
.to_srv_request();
let res = cors.call(req).await.unwrap();
assert_eq!(
Some(&b"http://example.com"[..]),
res.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
}

682
actix-cors/tests/tests.rs Normal file
View File

@ -0,0 +1,682 @@
use actix_cors::Cors;
use actix_utils::future::ok;
use actix_web::{
dev::{fn_service, ServiceRequest, Transform},
http::{
header::{self, HeaderValue},
Method, StatusCode,
},
test::{self, TestRequest},
HttpResponse,
};
use regex::bytes::Regex;
fn val_as_str(val: &HeaderValue) -> &str {
val.to_str().unwrap()
}
#[actix_web::test]
#[should_panic]
async fn test_wildcard_origin() {
Cors::default()
.allowed_origin("*")
.new_transform(test::ok_service())
.await
.unwrap();
}
#[actix_web::test]
async fn test_not_allowed_origin_fn() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.allowed_origin_fn(|origin, req| {
assert_eq!(&origin, req.headers.get(header::ORIGIN).unwrap());
req.headers
.get(header::ORIGIN)
.map(HeaderValue::as_bytes)
.filter(|b| b.ends_with(b".unknown.com"))
.is_some()
})
.new_transform(test::ok_service())
.await
.unwrap();
{
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://www.example.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
{
let req = TestRequest::get()
.insert_header(("Origin", "https://www.known.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
None,
resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
);
}
}
#[actix_web::test]
async fn test_allowed_origin_fn() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.allowed_origin_fn(|origin, req| {
assert_eq!(&origin, req.headers.get(header::ORIGIN).unwrap());
req.headers
.get(header::ORIGIN)
.map(HeaderValue::as_bytes)
.filter(|b| b.ends_with(b".unknown.com"))
.is_some()
})
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
"https://www.example.com",
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(val_as_str)
.unwrap()
);
let req = TestRequest::get()
.insert_header(("Origin", "https://www.unknown.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://www.unknown.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
#[actix_web::test]
async fn test_allowed_origin_fn_with_environment() {
let regex = Regex::new("https:.+\\.unknown\\.com").unwrap();
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.allowed_origin_fn(move |origin, req| {
assert_eq!(&origin, req.headers.get(header::ORIGIN).unwrap());
req.headers
.get(header::ORIGIN)
.map(HeaderValue::as_bytes)
.filter(|b| regex.is_match(b))
.is_some()
})
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
"https://www.example.com",
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(val_as_str)
.unwrap()
);
let req = TestRequest::get()
.insert_header(("Origin", "https://www.unknown.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://www.unknown.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
#[actix_web::test]
async fn test_multiple_origins_preflight() {
let cors = Cors::default()
.allowed_origin("https://example.com")
.allowed_origin("https://example.org")
.allowed_methods(vec![Method::GET])
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "GET"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://example.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
let req = TestRequest::default()
.insert_header(("Origin", "https://example.org"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "GET"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://example.org"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
#[actix_web::test]
async fn test_multiple_origins() {
let cors = Cors::default()
.allowed_origin("https://example.com")
.allowed_origin("https://example.org")
.allowed_methods(vec![Method::GET])
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://example.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
let req = TestRequest::get()
.insert_header(("Origin", "https://example.org"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://example.org"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
#[actix_web::test]
async fn test_response() {
let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT];
let cors = Cors::default()
.allow_any_origin()
.send_wildcard()
.disable_preflight()
.max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(exposed_headers.clone())
.expose_headers(exposed_headers.clone())
.allowed_header(header::CONTENT_TYPE)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"*"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"[..]),
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes),
Some(&b"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"[..]),
);
#[allow(clippy::needless_collect)]
{
let headers = resp
.headers()
.get(header::ACCESS_CONTROL_EXPOSE_HEADERS)
.map(val_as_str)
.unwrap()
.split(',')
.map(|s| s.trim())
.collect::<Vec<&str>>();
// TODO: use HashSet subset check
for h in exposed_headers {
assert!(headers.contains(&h.as_str()));
}
}
let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT];
let cors = Cors::default()
.allow_any_origin()
.send_wildcard()
.disable_preflight()
.max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(exposed_headers.clone())
.expose_headers(exposed_headers.clone())
.allowed_header(header::CONTENT_TYPE)
.new_transform(fn_service(|req: ServiceRequest| {
ok(req.into_response({
HttpResponse::Ok()
.insert_header((header::VARY, "Accept"))
.finish()
}))
}))
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
resp.headers()
.get(header::VARY)
.map(HeaderValue::as_bytes)
.unwrap(),
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers().get(header::VARY).map(HeaderValue::as_bytes).unwrap(),
b"Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
let cors = Cors::default()
.disable_vary_header()
.allowed_methods(vec!["POST"])
.allowed_origin("https://www.example.com")
.allowed_origin("https://www.google.com")
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.method(Method::OPTIONS)
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
let origins_str = resp
.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(val_as_str);
assert_eq!(Some("https://www.example.com"), origins_str);
}
#[actix_web::test]
async fn test_validate_origin() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn test_blocks_mismatched_origin_by_default() {
let cors = Cors::default()
.allowed_origin("https://www.example.com")
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::get()
.insert_header(("Origin", "https://www.example.test"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
}
#[actix_web::test]
async fn test_mismatched_origin_block_turned_off() {
let cors = Cors::default()
.allow_any_method()
.allowed_origin("https://www.example.com")
.block_on_origin_mismatch(false)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header(("Origin", "https://wrong.com"))
.insert_header(("Access-Control-Request-Method", "POST"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN), None);
let req = TestRequest::get()
.insert_header(("Origin", "https://wrong.com"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN), None);
}
#[actix_web::test]
async fn test_no_origin_response() {
let cors = Cors::permissive()
.disable_preflight()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default().method(Method::GET).to_srv_request();
let resp = test::call_service(&cors, req).await;
assert!(resp
.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.is_none());
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(
Some(&b"https://www.example.com"[..]),
resp.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.map(HeaderValue::as_bytes)
);
}
#[actix_web::test]
async fn validate_origin_allows_all_origins() {
let cors = Cors::permissive()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header(("Origin", "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn vary_header_on_all_handled_responses() {
let cors = Cors::permissive()
.new_transform(test::ok_service())
.await
.unwrap();
// preflight request
let req = TestRequest::default()
.method(Method::OPTIONS)
.insert_header((header::ORIGIN, "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "GET"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
// follow-up regular request
let req = TestRequest::default()
.method(Method::PUT)
.insert_header((header::ORIGIN, "https://www.example.com"))
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
let cors = Cors::default()
.allow_any_method()
.new_transform(test::ok_service())
.await
.unwrap();
// regular request OK with no CORS response headers
let req = TestRequest::default()
.method(Method::PUT)
.insert_header((header::ORIGIN, "https://www.example.com"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
assert!(!res
.headers()
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS));
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
res.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
res.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
// regular request no origin
let req = TestRequest::default().method(Method::PUT).to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
#[cfg(not(feature = "draft-private-network-access"))]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
);
#[cfg(feature = "draft-private-network-access")]
assert_eq!(
resp.headers()
.get(header::VARY)
.expect("response should have Vary header")
.to_str()
.unwrap(),
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
);
}
#[actix_web::test]
async fn test_allow_any_origin_any_method_any_header() {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default()
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type"))
.insert_header((header::ORIGIN, "https://www.example.com"))
.method(Method::OPTIONS)
.to_srv_request();
let resp = test::call_service(&cors, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_web::test]
async fn expose_all_request_header_values() {
let cors = Cors::permissive()
.new_transform(fn_service(|req: ServiceRequest| async move {
let res = req.into_response(
HttpResponse::Ok()
.insert_header((header::CONTENT_DISPOSITION, "test disposition"))
.finish(),
);
Ok(res)
}))
.await
.unwrap();
let req = TestRequest::default()
.insert_header((header::ORIGIN, "https://www.example.com"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
let cd_hdr = res
.headers()
.get(header::ACCESS_CONTROL_EXPOSE_HEADERS)
.unwrap()
.to_str()
.unwrap();
assert!(cd_hdr.contains("content-disposition"));
assert!(cd_hdr.contains("access-control-allow-origin"));
}
#[cfg(feature = "draft-private-network-access")]
#[actix_web::test]
async fn private_network_access() {
let cors = Cors::permissive()
.allowed_origin("https://public.site")
.allow_private_network_access()
.new_transform(fn_service(|req: ServiceRequest| async move {
let res = req.into_response(
HttpResponse::Ok()
.insert_header((header::CONTENT_DISPOSITION, "test disposition"))
.finish(),
);
Ok(res)
}))
.await
.unwrap();
let req = TestRequest::default()
.insert_header((header::ORIGIN, "https://public.site"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert!(res.headers().contains_key("access-control-allow-origin"));
let req = TestRequest::default()
.insert_header((header::ORIGIN, "https://public.site"))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
.insert_header((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"))
.insert_header(("Access-Control-Request-Private-Network", "true"))
.to_srv_request();
let res = test::call_service(&cors, req).await;
assert!(res.headers().contains_key("access-control-allow-origin"));
assert!(res
.headers()
.contains_key("access-control-allow-private-network"));
}

View File

@ -1,17 +1,158 @@
# Changes
## [Unreleased] - 2020-xx-xx
## Unreleased
* Update the `time` dependency to 0.2.5
## 0.8.0
## [0.2.1] - 2020-01-10
- Update `actix-session` dependency to `0.10`.
* Fix panic with already borrowed: BorrowMutError #1263
## 0.7.1
## [0.2.0] - 2019-12-20
- 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.
* Use actix-web 2.0
## 0.7.0
## [0.1.0] - 2019-06-xx
- Update `actix-session` dependency to `0.9`.
- Minimum supported Rust version (MSRV) is now 1.75.
* Move identity middleware to separate crate
## 0.6.0
- Add `error` module.
- Replace use of `anyhow::Error` in return types with specific error types.
- Update `actix-session` dependency to `0.8`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.5.2
- Fix visit deadline. [#263]
[#263]: https://github.com/actix/actix-extras/pull/263
## 0.5.1
- Remove unnecessary dependencies. [#259]
[#259]: https://github.com/actix/actix-extras/pull/259
## 0.5.0
`actix-identity` v0.5 is a complete rewrite. The goal is to streamline user experience and reduce maintenance overhead.
`actix-identity` is now designed as an additional layer on top of `actix-session` v0.7, focused on identity management. The identity information is stored in the session state, which is managed by `actix-session` and can be stored using any of the supported `SessionStore` implementations. This reduces the surface area in `actix-identity` (e.g., it is no longer concerned with cookies!) and provides a smooth upgrade path for users: if you need to work with sessions, you no longer need to choose between `actix-session` and `actix-identity`; they work together now!
`actix-identity` v0.5 has feature-parity with `actix-identity` v0.4; if you bump into any blocker when upgrading, please open an issue.
Changes:
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
- `IdentityService`, `IdentityPolicy` and `CookieIdentityPolicy` have been replaced by `IdentityMiddleware`. [#246]
- Rename `RequestIdentity` trait to `IdentityExt`. [#246]
- Trying to extract an `Identity` for an unauthenticated user will return a `401 Unauthorized` response to the client. Extract an `Option<Identity>` or a `Result<Identity, actix_web::Error>` if you need to handle cases where requests may or may not be authenticated. [#246]
Example:
```rust
use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
use actix_identity::Identity;
#[get("/")]
async fn index(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
HttpResponse::Ok().finish()
} else {
// Redirect to login page if unauthenticated
HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, "/login"))
.finish()
}
}
```
[#246]: https://github.com/actix/actix-extras/pull/246
## 0.4.0
- Update `actix-web` dependency to `4`.
## 0.4.0-beta.9
- Relax body type bounds on middleware impl. [#223]
- Update `actix-web` dependency to `4.0.0-rc.1`.
[#223]: https://github.com/actix/actix-extras/pull/223
## 0.4.0-beta.8
- No significant changes since `0.4.0-beta.7`.
## 0.4.0-beta.7
- 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.4.0-beta.6
- Update `actix-web` dependency to `4.0.0.beta-15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216
## 0.4.0-beta.5
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
[#209]: https://github.com/actix/actix-extras/pull/209
## 0.4.0-beta.4
- No significant changes since `0.4.0-beta.3`.
## 0.4.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.4.0-beta.2
- No notable changes.
## 0.4.0-beta.1
- Rename `CookieIdentityPolicy::{max_age => max_age_secs}`. [#168]
- Rename `CookieIdentityPolicy::{max_age_time => max_age}`. [#168]
- Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0.
[#168]: https://github.com/actix/actix-extras/pull/168
## 0.3.1
- Add method to set `HttpOnly` flag on cookie identity. [#102]
[#102]: https://github.com/actix/actix-extras/pull/102
## 0.3.0
- Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0.
## 0.3.0-alpha.1
- Update the `time` dependency to 0.2.7
- Update the `actix-web` dependency to 3.0.0-alpha.1
- Minimize `futures` dependency
## 0.2.1
- Fix panic with already borrowed: BorrowMutError #1263
## 0.2.0 - 2019-12-20
- Use actix-web 2.0
## 0.1.0 - 2019-06-xx
- Move identity middleware to separate crate

View File

@ -1,29 +1,41 @@
[package]
name = "actix-identity"
version = "0.2.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Identity service for actix-web framework."
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-identity/"
license = "MIT/Apache-2.0"
edition = "2018"
version = "0.8.0"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Luca Palmieri <rust@lpalmieri.com>",
]
description = "Identity management for Actix Web"
keywords = ["actix", "auth", "identity", "web", "security"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[lib]
name = "actix_identity"
path = "src/lib.rs"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[dependencies]
actix-web = { version = "2.0.0", default-features = false, features = ["secure-cookies"] }
actix-service = "1.0.2"
futures = "0.3.1"
serde = "1.0"
serde_json = "1.0"
time = { version = "0.2.5", default-features = false, features = ["std"] }
actix-service = "2"
actix-session = "0.10"
actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
derive_more = { version = "2", features = ["display", "error", "from"] }
futures-core = "0.3.17"
serde = { version = "1", features = ["derive"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
[dev-dependencies]
actix-rt = "1.0.0"
actix-http = "1.0.1"
bytes = "0.5.3"
actix-http = "3"
actix-web = { version = "4", default-features = false, features = ["macros", "cookies", "secure-cookies"] }
actix-session = { version = "0.10", features = ["redis-session", "cookie-session"] }
env_logger = "0.11"
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json"] }
uuid = { version = "1", features = ["v4"] }
[lints]
workspace = true

View File

@ -1,19 +1,106 @@
# actix-identity
[![crates.io](https://img.shields.io/crates/v/actix-identity)](https://crates.io/crates/actix-identity)
[![Documentation](https://docs.rs/actix-identity/badge.svg)](https://docs.rs/actix-identity)
[![Dependency Status](https://deps.rs/crate/actix-identity/0.2.1/status.svg)](https://deps.rs/crate/actix-identity/0.2.1)
[![Build Status](https://travis-ci.org/actix/actix-identity.svg?branch=master)](https://travis-ci.org/actix/actix-identity)
[![codecov](https://codecov.io/gh/actix/actix-identity/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-identity)
> Identity management for Actix Web.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-identity?label=latest)](https://crates.io/crates/actix-identity)
[![Documentation](https://docs.rs/actix-identity/badge.svg?version=0.8.0)](https://docs.rs/actix-identity/0.8.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-identity)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Dependency Status](https://deps.rs/crate/actix-identity/0.8.0/status.svg)](https://deps.rs/crate/actix-identity/0.8.0)
> Identity service for actix-web framework.
<!-- prettier-ignore-end -->
## Documentation & community resources
<!-- cargo-rdme start -->
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-identity/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-session](https://crates.io/crates/actix-identity)
* Minimum supported Rust version: 1.34 or later
Identity management for Actix Web.
`actix-identity` can be used to track identity of a user across multiple requests. It is built on top of HTTP sessions, via [`actix-session`](https://docs.rs/actix-session).
## Getting started
To start using identity management in your Actix Web application you must register [`IdentityMiddleware`] and `SessionMiddleware` as middleware on your `App`:
```rust
use actix_web::{cookie::Key, App, HttpServer, HttpResponse};
use actix_identity::IdentityMiddleware;
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
#[actix_web::main]
async fn main() {
// When using `Key::generate()` it is important to initialize outside of the
// `HttpServer::new` closure. When deployed the secret key should be read from a
// configuration file or environment variables.
let secret_key = Key::generate();
let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap();
HttpServer::new(move || {
App::new()
// Install the identity framework first.
.wrap(IdentityMiddleware::default())
// The identity system is built on top of sessions. You must install the session
// middleware to leverage `actix-identity`. The session middleware must be mounted
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
// order of registration when it receives an incoming request.
.wrap(SessionMiddleware::new(
redis_store.clone(),
secret_key.clone(),
))
// Your request handlers [...]
})
}
```
User identities can be created, accessed and destroyed using the [`Identity`] extractor in your request handlers:
```rust
use actix_web::{get, post, HttpResponse, Responder, HttpRequest, HttpMessage};
use actix_identity::Identity;
use actix_session::storage::RedisSessionStore;
#[get("/")]
async fn index(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
format!("Welcome! {}", user.id().unwrap())
} else {
"Welcome Anonymous!".to_owned()
}
}
#[post("/login")]
async fn login(request: HttpRequest) -> impl Responder {
// Some kind of authentication should happen here
// e.g. password-based, biometric, etc.
// [...]
// attach a verified user identity to the active session
Identity::login(&request.extensions(), "User1".into()).unwrap();
HttpResponse::Ok()
}
#[post("/logout")]
async fn logout(user: Option<Identity>) -> impl Responder {
if let Some(user) = user {
user.logout();
}
HttpResponse::Ok()
}
```
## Advanced configuration
By default, `actix-identity` does not automatically log out users. You can change this behaviour by customising the configuration for [`IdentityMiddleware`] via [`IdentityMiddleware::builder`].
In particular, you can automatically log out users who:
- have been inactive for a while (see [`IdentityMiddlewareBuilder::visit_deadline`]);
- logged in too long ago (see [`IdentityMiddlewareBuilder::login_deadline`]).
[`IdentityMiddlewareBuilder::visit_deadline`]: config::IdentityMiddlewareBuilder::visit_deadline
[`IdentityMiddlewareBuilder::login_deadline`]: config::IdentityMiddlewareBuilder::login_deadline
<!-- cargo-rdme end -->

View File

@ -0,0 +1,93 @@
//! A rudimentary example of how to set up and use `actix-identity`.
//!
//! ```bash
//! # using HTTPie (https://httpie.io/cli)
//!
//! # outputs "Welcome Anonymous!" message
//! http -v --session=identity GET localhost:8080/
//!
//! # log in using fake details, ensuring that --session is used to persist cookies
//! http -v --session=identity POST localhost:8080/login user_id=foo
//!
//! # outputs "Welcome User1" message
//! http -v --session=identity GET localhost:8080/
//! ```
use std::{io, time::Duration};
use actix_identity::{Identity, IdentityMiddleware};
use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware};
use actix_web::{
cookie::Key, get, middleware::Logger, post, App, HttpMessage, HttpRequest, HttpResponse,
HttpServer, Responder,
};
#[actix_web::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let secret_key = Key::generate();
let expiration = Duration::from_secs(24 * 60 * 60);
HttpServer::new(move || {
let session_mw =
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
// disable secure cookie for local testing
.cookie_secure(false)
// Set a ttl for the cookie if the identity should live longer than the user session
.session_lifecycle(
PersistentSession::default().session_ttl(expiration.try_into().unwrap()),
)
.build();
let identity_mw = IdentityMiddleware::builder()
.visit_deadline(Some(expiration))
.build();
App::new()
// Install the identity framework first.
.wrap(identity_mw)
// The identity system is built on top of sessions. You must install the session
// middleware to leverage `actix-identity`. The session middleware must be mounted
// AFTER the identity middleware: `actix-web` invokes middleware in the OPPOSITE
// order of registration when it receives an incoming request.
.wrap(session_mw)
.wrap(Logger::default())
.service(index)
.service(login)
.service(logout)
})
.bind(("127.0.0.1", 8080))
.unwrap()
.workers(2)
.run()
.await
}
#[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.
// [...]
// Attached a verified user identity to the active
// session.
Identity::login(&request.extensions(), "User1".into()).unwrap();
HttpResponse::Ok()
}
#[post("/logout")]
async fn logout(user: Identity) -> impl Responder {
user.logout();
HttpResponse::NoContent()
}

View File

@ -0,0 +1,125 @@
//! Configuration options to tune the behaviour of [`IdentityMiddleware`].
use std::time::Duration;
use crate::IdentityMiddleware;
#[derive(Debug, Clone)]
pub(crate) struct Configuration {
pub(crate) on_logout: LogoutBehaviour,
pub(crate) login_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 {
fn default() -> Self {
Self {
on_logout: LogoutBehaviour::PurgeSession,
login_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",
}
}
}
/// `LogoutBehaviour` controls what actions are going to be performed when [`Identity::logout`] is
/// invoked.
///
/// [`Identity::logout`]: crate::Identity::logout
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LogoutBehaviour {
/// When [`Identity::logout`](crate::Identity::logout) is called, purge the current session.
///
/// This behaviour might be desirable when you have stored additional information in the
/// session state that are tied to the user's identity and should not be retained after logout.
PurgeSession,
/// When [`Identity::logout`](crate::Identity::logout) is called, remove the identity
/// information from the current session state. The session itself is not destroyed.
///
/// This behaviour might be desirable when you have stored information in the session state that
/// is not tied to the user's identity and should be retained after logout.
DeleteIdentityKeys,
}
/// A fluent builder to construct an [`IdentityMiddleware`] instance with custom configuration
/// parameters.
///
/// Use [`IdentityMiddleware::builder`] to get started!
#[derive(Debug, Clone)]
pub struct IdentityMiddlewareBuilder {
configuration: Configuration,
}
impl IdentityMiddlewareBuilder {
pub(crate) fn new() -> Self {
Self {
configuration: Configuration::default(),
}
}
/// 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.
///
/// By default, the current session is purged ([`LogoutBehaviour::PurgeSession`]).
pub fn logout_behaviour(mut self, logout_behaviour: LogoutBehaviour) -> Self {
self.configuration.on_logout = logout_behaviour;
self
}
/// Automatically logs out users after a certain amount of time has passed since they logged in,
/// regardless of their activity pattern.
///
/// If set to:
/// - `None`: login deadline is disabled.
/// - `Some(duration)`: login deadline is enabled and users will be logged out after `duration`
/// has passed since their login.
///
/// By default, login deadline is disabled.
pub fn login_deadline(mut self, deadline: Option<Duration>) -> Self {
self.configuration.login_deadline = deadline;
self
}
/// Automatically logs out users after a certain amount of time has passed since their last
/// visit.
///
/// If set to:
/// - `None`: visit deadline is disabled.
/// - `Some(duration)`: visit deadline is enabled and users will be logged out after `duration`
/// has passed since their last visit.
///
/// By default, visit deadline is disabled.
pub fn visit_deadline(mut self, deadline: Option<Duration>) -> Self {
self.configuration.visit_deadline = deadline;
self
}
/// Finalises the builder and returns an [`IdentityMiddleware`] instance.
pub fn build(self) -> IdentityMiddleware {
IdentityMiddleware::new(self.configuration)
}
}

View File

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

View File

@ -0,0 +1,272 @@
use actix_session::Session;
use actix_utils::future::{ready, Ready};
use actix_web::{
cookie::time::OffsetDateTime,
dev::{Extensions, Payload},
http::StatusCode,
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
};
use crate::{
config::LogoutBehaviour,
error::{
GetIdentityError, LoginError, LostIdentityError, MissingIdentityError, SessionExpiryError,
},
};
/// A verified user identity. It can be used as a request extractor.
///
/// The lifecycle of a user identity is tied to the lifecycle of the underlying session. If the
/// session is destroyed (e.g. the session expired), the user identity will be forgotten, de-facto
/// forcing a user log out.
///
/// # Examples
/// ```
/// use actix_web::{
/// get, post, Responder, HttpRequest, HttpMessage, HttpResponse
/// };
/// use actix_identity::Identity;
///
/// #[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 {
/// Identity::login(&request.extensions(), "User1".into());
/// HttpResponse::Ok()
/// }
///
/// #[post("/logout")]
/// async fn logout(user: Identity) -> impl Responder {
/// user.logout();
/// HttpResponse::Ok()
/// }
/// ```
///
/// # Extractor Behaviour
/// What happens if you try to extract an `Identity` out of a request that does not have a valid
/// identity attached? The API will return a `401 UNAUTHORIZED` to the caller.
///
/// If you want to customise this behaviour, consider extracting `Option<Identity>` or
/// `Result<Identity, actix_web::Error>` instead of a bare `Identity`: you will then be fully in
/// control of the error path.
///
/// ## Examples
/// ```
/// use actix_web::{http::header::LOCATION, get, HttpResponse, Responder};
/// use actix_identity::Identity;
///
/// #[get("/")]
/// async fn index(user: Option<Identity>) -> impl Responder {
/// if let Some(user) = user {
/// HttpResponse::Ok().finish()
/// } else {
/// // Redirect to login page if unauthenticated
/// HttpResponse::TemporaryRedirect()
/// .insert_header((LOCATION, "/login"))
/// .finish()
/// }
/// }
/// ```
pub struct Identity(IdentityInner);
#[derive(Clone)]
pub(crate) struct IdentityInner {
pub(crate) session: Session,
pub(crate) logout_behaviour: LogoutBehaviour,
pub(crate) is_login_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 {
fn extract(ext: &Extensions) -> Self {
ext.get::<Self>()
.expect(
"No `IdentityInner` instance was found in the extensions attached to the \
incoming request. This usually means that `IdentityMiddleware` has not been \
registered as an application middleware via `App::wrap`. `Identity` cannot be used \
unless the identity machine is properly mounted: register `IdentityMiddleware` as \
a middleware for your application to fix this panic. If the problem persists, \
please file an issue on GitHub.",
)
.to_owned()
}
/// Retrieve the user id attached to the current session.
fn get_identity(&self) -> Result<String, GetIdentityError> {
self.session
.get::<String>(self.id_key)?
.ok_or_else(|| MissingIdentityError.into())
}
}
impl Identity {
/// Return the user id associated to the current session.
///
/// # Examples
/// ```
/// use actix_web::{get, Responder};
/// use actix_identity::Identity;
///
/// #[get("/")]
/// async fn index(user: Option<Identity>) -> impl Responder {
/// if let Some(user) = user {
/// format!("Welcome! {}", user.id().unwrap())
/// } else {
/// "Welcome Anonymous!".to_owned()
/// }
/// }
/// ```
pub fn id(&self) -> Result<String, GetIdentityError> {
self.0
.session
.get(self.0.id_key)?
.ok_or_else(|| LostIdentityError.into())
}
/// Attach a valid user identity to the current session.
///
/// This method should be called after you have successfully authenticated the user. After
/// `login` has been called, the user will be able to access all routes that require a valid
/// [`Identity`].
///
/// # Examples
/// ```
/// use actix_web::{post, Responder, HttpRequest, HttpMessage, HttpResponse};
/// use actix_identity::Identity;
///
/// #[post("/login")]
/// async fn login(request: HttpRequest) -> impl Responder {
/// Identity::login(&request.extensions(), "User1".into());
/// HttpResponse::Ok()
/// }
/// ```
pub fn login(ext: &Extensions, id: String) -> Result<Self, LoginError> {
let inner = IdentityInner::extract(ext);
inner.session.insert(inner.id_key, id)?;
let now = OffsetDateTime::now_utc().unix_timestamp();
if inner.is_login_deadline_enabled {
inner.session.insert(inner.login_unix_timestamp_key, now)?;
}
if inner.is_visit_deadline_enabled {
inner
.session
.insert(inner.last_visit_unix_timestamp_key, now)?;
}
inner.session.renew();
Ok(Self(inner))
}
/// Remove the user identity from the current session.
///
/// After `logout` has been called, the user will no longer be able to access routes that
/// require a valid [`Identity`].
///
/// The behaviour on logout is determined by [`IdentityMiddlewareBuilder::logout_behaviour`].
///
/// # Examples
/// ```
/// use actix_web::{post, Responder, HttpResponse};
/// use actix_identity::Identity;
///
/// #[post("/logout")]
/// async fn logout(user: Identity) -> impl Responder {
/// user.logout();
/// HttpResponse::Ok()
/// }
/// ```
///
/// [`IdentityMiddlewareBuilder::logout_behaviour`]: crate::config::IdentityMiddlewareBuilder::logout_behaviour
pub fn logout(self) {
match self.0.logout_behaviour {
LogoutBehaviour::PurgeSession => {
self.0.session.purge();
}
LogoutBehaviour::DeleteIdentityKeys => {
self.0.session.remove(self.0.id_key);
if self.0.is_login_deadline_enabled {
self.0.session.remove(self.0.login_unix_timestamp_key);
}
if self.0.is_visit_deadline_enabled {
self.0.session.remove(self.0.last_visit_unix_timestamp_key);
}
}
}
}
pub(crate) fn extract(ext: &Extensions) -> Result<Self, GetIdentityError> {
let inner = IdentityInner::extract(ext);
inner.get_identity()?;
Ok(Self(inner))
}
pub(crate) fn logged_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> {
Ok(self
.0
.session
.get(self.0.login_unix_timestamp_key)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(SessionExpiryError)?)
}
pub(crate) fn last_visited_at(&self) -> Result<Option<OffsetDateTime>, GetIdentityError> {
Ok(self
.0
.session
.get(self.0.last_visit_unix_timestamp_key)?
.map(OffsetDateTime::from_unix_timestamp)
.transpose()
.map_err(SessionExpiryError)?)
}
pub(crate) fn set_last_visited_at(&self) -> Result<(), LoginError> {
let now = OffsetDateTime::now_utc().unix_timestamp();
self.0
.session
.insert(self.0.last_visit_unix_timestamp_key, now)?;
Ok(())
}
}
/// Extractor implementation for [`Identity`].
///
/// # Examples
/// ```
/// use actix_web::{get, Responder};
/// use actix_identity::Identity;
///
/// #[get("/")]
/// async fn index(user: Option<Identity>) -> impl Responder {
/// if let Some(user) = user {
/// format!("Welcome! {}", user.id().unwrap())
/// } else {
/// "Welcome Anonymous!".to_owned()
/// }
/// }
/// ```
impl FromRequest for Identity {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(Identity::extract(&req.extensions()).map_err(|err| {
let res = actix_web::error::InternalError::from_response(
err,
HttpResponse::new(StatusCode::UNAUTHORIZED),
);
actix_web::Error::from(res)
}))
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,259 @@
use std::rc::Rc;
use actix_session::SessionExt;
use actix_utils::future::{ready, Ready};
use actix_web::{
body::MessageBody,
cookie::time::{format_description::well_known::Rfc3339, OffsetDateTime},
dev::{Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage as _, Result,
};
use futures_core::future::LocalBoxFuture;
use crate::{
config::{Configuration, IdentityMiddlewareBuilder},
identity::IdentityInner,
Identity,
};
/// Identity management middleware.
///
/// ```no_run
/// use actix_web::{cookie::Key, App, HttpServer};
/// use actix_session::storage::RedisSessionStore;
/// use actix_identity::{Identity, IdentityMiddleware};
/// use actix_session::{Session, SessionMiddleware};
///
/// #[actix_web::main]
/// async fn main() {
/// 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`.
/// .wrap(SessionMiddleware::new(redis_store.clone(), secret_key.clone()))
/// })
/// # ;
/// }
/// ```
#[derive(Default, Clone)]
pub struct IdentityMiddleware {
configuration: Rc<Configuration>,
}
impl IdentityMiddleware {
pub(crate) fn new(configuration: Configuration) -> Self {
Self {
configuration: Rc::new(configuration),
}
}
/// A fluent API to configure [`IdentityMiddleware`].
pub fn builder() -> IdentityMiddlewareBuilder {
IdentityMiddlewareBuilder::new()
}
}
impl<S, B> Transform<S, ServiceRequest> for IdentityMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = InnerIdentityMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(InnerIdentityMiddleware {
service: Rc::new(service),
configuration: Rc::clone(&self.configuration),
}))
}
}
#[doc(hidden)]
pub struct InnerIdentityMiddleware<S> {
service: Rc<S>,
configuration: Rc<Configuration>,
}
impl<S> Clone for InnerIdentityMiddleware<S> {
fn clone(&self) -> Self {
Self {
service: Rc::clone(&self.service),
configuration: Rc::clone(&self.configuration),
}
}
}
impl<S, B> Service<ServiceRequest> for InnerIdentityMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let srv = Rc::clone(&self.service);
let configuration = Rc::clone(&self.configuration);
Box::pin(async move {
let identity_inner = IdentityInner {
session: req.get_session(),
logout_behaviour: configuration.on_logout.clone(),
is_login_deadline_enabled: configuration.login_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);
enforce_policies(&req, &configuration);
srv.call(req).await
})
}
}
// easier to scan with returns where they are
// especially if the function body were to evolve in the future
#[allow(clippy::needless_return)]
fn enforce_policies(req: &ServiceRequest, configuration: &Configuration) {
let must_extract_identity =
configuration.login_deadline.is_some() || configuration.visit_deadline.is_some();
if !must_extract_identity {
return;
}
let identity = match Identity::extract(&req.extensions()) {
Ok(identity) => identity,
Err(err) => {
tracing::debug!(
error.display = %err,
error.debug = ?err,
"Failed to extract an `Identity` from the incoming request."
);
return;
}
};
if let Some(login_deadline) = configuration.login_deadline {
if matches!(
enforce_login_deadline(&identity, login_deadline),
PolicyDecision::LogOut
) {
identity.logout();
return;
}
}
if let Some(visit_deadline) = configuration.visit_deadline {
if matches!(
enforce_visit_deadline(&identity, visit_deadline),
PolicyDecision::LogOut
) {
identity.logout();
return;
} else if let Err(err) = identity.set_last_visited_at() {
tracing::warn!(
error.display = %err,
error.debug = ?err,
"Failed to set the last visited timestamp on `Identity` for an incoming request."
);
}
}
}
fn enforce_login_deadline(
identity: &Identity,
login_deadline: std::time::Duration,
) -> PolicyDecision {
match identity.logged_at() {
Ok(None) => {
tracing::info!(
"Login deadline is enabled, but there is no login timestamp in the session \
state attached to the incoming request. Logging the user out."
);
PolicyDecision::LogOut
}
Err(err) => {
tracing::info!(
error.display = %err,
error.debug = ?err,
"Login deadline is enabled but we failed to extract the login timestamp from the \
session state attached to the incoming request. Logging the user out."
);
PolicyDecision::LogOut
}
Ok(Some(logged_in_at)) => {
let elapsed = OffsetDateTime::now_utc() - logged_in_at;
if elapsed > login_deadline {
tracing::info!(
user.logged_in_at = %logged_in_at.format(&Rfc3339).unwrap_or_default(),
identity.login_deadline_seconds = login_deadline.as_secs(),
identity.elapsed_since_login_seconds = elapsed.whole_seconds(),
"Login deadline is enabled and too much time has passed since the user logged \
in. Logging the user out."
);
PolicyDecision::LogOut
} else {
PolicyDecision::StayLoggedIn
}
}
}
}
fn enforce_visit_deadline(
identity: &Identity,
visit_deadline: std::time::Duration,
) -> PolicyDecision {
match identity.last_visited_at() {
Ok(None) => {
tracing::info!(
"Last visit deadline is enabled, but there is no last visit timestamp in the \
session state attached to the incoming request. Logging the user out."
);
PolicyDecision::LogOut
}
Err(err) => {
tracing::info!(
error.display = %err,
error.debug = ?err,
"Last visit deadline is enabled but we failed to extract the last visit timestamp \
from the session state attached to the incoming request. Logging the user out."
);
PolicyDecision::LogOut
}
Ok(Some(last_visited_at)) => {
let elapsed = OffsetDateTime::now_utc() - last_visited_at;
if elapsed > visit_deadline {
tracing::info!(
user.last_visited_at = %last_visited_at.format(&Rfc3339).unwrap_or_default(),
identity.visit_deadline_seconds = visit_deadline.as_secs(),
identity.elapsed_since_last_visit_seconds = elapsed.whole_seconds(),
"Last visit deadline is enabled and too much time has passed since the last \
time the user visited. Logging the user out."
);
PolicyDecision::LogOut
} else {
PolicyDecision::StayLoggedIn
}
}
}
}
enum PolicyDecision {
StayLoggedIn,
LogOut,
}

View File

@ -0,0 +1,17 @@
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use uuid::Uuid;
pub fn store() -> CookieSessionStore {
CookieSessionStore::default()
}
pub fn user_id() -> String {
Uuid::new_v4().to_string()
}
pub fn session_middleware() -> SessionMiddleware<CookieSessionStore> {
SessionMiddleware::builder(store(), Key::generate())
.cookie_domain(Some("localhost".into()))
.build()
}

View File

@ -0,0 +1,212 @@
use std::time::Duration;
use actix_identity::{config::LogoutBehaviour, IdentityMiddleware};
use reqwest::StatusCode;
use crate::{fixtures::user_id, test_app::TestApp};
#[actix_web::test]
async fn opaque_401_is_returned_for_unauthenticated_users() {
let app = TestApp::spawn();
let response = app.get_identity_required().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert!(response.bytes().await.unwrap().is_empty());
}
#[actix_web::test]
async fn login_works() {
let app = TestApp::spawn();
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
// Access identity-restricted route successfully
let response = app.get_identity_required().await;
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]
async fn logging_in_again_replaces_the_current_identity() {
let app = TestApp::spawn();
let first_user_id = user_id();
let second_user_id = user_id();
// Log-in
let body = app.post_login(first_user_id.clone()).await;
assert_eq!(body.user_id, Some(first_user_id.clone()));
// Log-in again
let body = app.post_login(second_user_id.clone()).await;
assert_eq!(body.user_id, Some(second_user_id.clone()));
let body = app.get_current().await;
assert_eq!(body.user_id, Some(second_user_id.clone()));
}
#[actix_web::test]
async fn session_key_is_renewed_on_login() {
let app = TestApp::spawn();
let user_id = user_id();
// Create an anonymous session
let body = app.post_increment().await;
assert_eq!(body.user_id, None);
assert_eq!(body.counter, 1);
assert_eq!(body.session_status, "changed");
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
assert_eq!(body.counter, 1);
assert_eq!(body.session_status, "renewed");
}
#[actix_web::test]
async fn logout_works() {
let app = TestApp::spawn();
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
// Log-out
let response = app.post_logout().await;
assert!(response.status().is_success());
// Try to access identity-restricted route
let response = app.get_identity_required().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[actix_web::test]
async fn logout_can_avoid_destroying_the_whole_session() {
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder().logout_behaviour(LogoutBehaviour::DeleteIdentityKeys),
);
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
assert_eq!(body.counter, 0);
// Increment counter
let body = app.post_increment().await;
assert_eq!(body.user_id, Some(user_id.clone()));
assert_eq!(body.counter, 1);
// Log-out
let response = app.post_logout().await;
assert!(response.status().is_success());
// Check the state of the counter attached to the session state
let body = app.get_current().await;
assert_eq!(body.user_id, None);
// It would be 0 if the session state had been entirely lost!
assert_eq!(body.counter, 1);
}
#[actix_web::test]
async fn user_is_logged_out_when_login_deadline_is_elapsed() {
let login_deadline = Duration::from_millis(10);
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
);
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
// Wait for deadline to pass
actix_web::rt::time::sleep(login_deadline * 2).await;
let body = app.get_current().await;
// We have been logged out!
assert_eq!(body.user_id, None);
}
#[actix_web::test]
async fn login_deadline_does_not_log_users_out_before_their_time() {
// 1 hour
let login_deadline = Duration::from_secs(60 * 60);
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder().login_deadline(Some(login_deadline)),
);
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
let body = app.get_current().await;
assert_eq!(body.user_id, Some(user_id));
}
#[actix_web::test]
async fn visit_deadline_does_not_log_users_out_before_their_time() {
// 1 hour
let visit_deadline = Duration::from_secs(60 * 60);
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder().visit_deadline(Some(visit_deadline)),
);
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
let body = app.get_current().await;
assert_eq!(body.user_id, Some(user_id));
}
#[actix_web::test]
async fn user_is_logged_out_when_visit_deadline_is_elapsed() {
let visit_deadline = Duration::from_millis(10);
let app = TestApp::spawn_with_config(
IdentityMiddleware::builder().visit_deadline(Some(visit_deadline)),
);
let user_id = user_id();
// Log-in
let body = app.post_login(user_id.clone()).await;
assert_eq!(body.user_id, Some(user_id.clone()));
// Wait for deadline to pass
actix_web::rt::time::sleep(visit_deadline * 2).await;
let body = app.get_current().await;
// We have been logged out!
assert_eq!(body.user_id, None);
}

View File

@ -0,0 +1,3 @@
pub mod fixtures;
mod integration;
pub mod test_app;

View File

@ -0,0 +1,187 @@
use std::net::TcpListener;
use actix_identity::{config::IdentityMiddlewareBuilder, Identity, IdentityMiddleware};
use actix_session::{Session, SessionStatus};
use actix_web::{web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};
use crate::fixtures::session_middleware;
pub struct TestApp {
port: u16,
api_client: reqwest::Client,
}
impl TestApp {
/// Spawn a test application using a custom configuration for `IdentityMiddleware`.
pub fn spawn_with_config(builder: IdentityMiddlewareBuilder) -> Self {
// Random OS port
let listener = TcpListener::bind("localhost:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = HttpServer::new(move || {
App::new()
.wrap(builder.clone().build())
.wrap(session_middleware())
.route("/increment", web::post().to(increment))
.route("/current", web::get().to(show))
.route("/login", web::post().to(login))
.route("/logout", web::post().to(logout))
.route("/identity_required", web::get().to(identity_required))
})
.workers(1)
.listen(listener)
.unwrap()
.run();
actix_web::rt::spawn(server);
let client = reqwest::Client::builder()
.cookie_store(true)
.build()
.unwrap();
TestApp {
port,
api_client: client,
}
}
/// Spawn a test application using the default configuration settings for `IdentityMiddleware`.
pub fn spawn() -> Self {
Self::spawn_with_config(IdentityMiddleware::builder())
}
fn url(&self) -> String {
format!("http://localhost:{}", self.port)
}
pub async fn get_identity_required(&self) -> reqwest::Response {
self.api_client
.get(format!("{}/identity_required", &self.url()))
.send()
.await
.unwrap()
}
pub async fn get_current(&self) -> EndpointResponse {
self.api_client
.get(format!("{}/current", &self.url()))
.send()
.await
.unwrap()
.json()
.await
.unwrap()
}
pub async fn post_increment(&self) -> EndpointResponse {
let response = self
.api_client
.post(format!("{}/increment", &self.url()))
.send()
.await
.unwrap();
response.json().await.unwrap()
}
pub async fn post_login(&self, user_id: String) -> EndpointResponse {
let response = self
.api_client
.post(format!("{}/login", &self.url()))
.json(&LoginRequest { user_id })
.send()
.await
.unwrap();
response.json().await.unwrap()
}
pub async fn post_logout(&self) -> reqwest::Response {
self.api_client
.post(format!("{}/logout", &self.url()))
.send()
.await
.unwrap()
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EndpointResponse {
pub user_id: Option<String>,
pub counter: i32,
pub session_status: String,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct LoginRequest {
user_id: String,
}
async fn show(user: Option<Identity>, session: Session) -> HttpResponse {
let user_id = user.map(|u| u.id().unwrap());
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.unwrap_or(0);
HttpResponse::Ok().json(&EndpointResponse {
user_id,
counter,
session_status: session_status(session),
})
}
async fn increment(session: Session, user: Option<Identity>) -> HttpResponse {
let user_id = user.map(|u| u.id().unwrap());
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.map_or(1, |inner| inner + 1);
session.insert("counter", counter).unwrap();
HttpResponse::Ok().json(&EndpointResponse {
user_id,
counter,
session_status: session_status(session),
})
}
async fn login(
user_id: web::Json<LoginRequest>,
request: HttpRequest,
session: Session,
) -> HttpResponse {
let id = user_id.into_inner().user_id;
let user = Identity::login(&request.extensions(), id).unwrap();
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.unwrap_or(0);
HttpResponse::Ok().json(&EndpointResponse {
user_id: Some(user.id().unwrap()),
counter,
session_status: session_status(session),
})
}
async fn logout(user: Option<Identity>) -> HttpResponse {
if let Some(user) = user {
user.logout();
}
HttpResponse::Ok().finish()
}
async fn identity_required(_identity: Identity) -> HttpResponse {
HttpResponse::Ok().finish()
}
fn session_status(session: Session) -> String {
match session.status() {
SessionStatus::Changed => "changed",
SessionStatus::Purged => "purged",
SessionStatus::Renewed => "renewed",
SessionStatus::Unchanged => "unchanged",
}
.into()
}

View File

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

View File

@ -0,0 +1,43 @@
[package]
name = "actix-limitation"
version = "0.5.1"
authors = [
"0xmad <0xmad@users.noreply.github.com>",
"Rob Ede <robjtede@icloud.com>",
]
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"]
categories = ["asynchronous", "web-programming"]
repository = "https://github.com/actix/actix-extras"
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features]
default = ["session"]
session = ["actix-session"]
[dependencies]
actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies"] }
chrono = "0.4"
derive_more = { version = "2", features = ["display", "error", "from"] }
log = "0.4"
redis = { version = "0.29", default-features = false, features = ["tokio-comp"] }
time = "0.3"
# session
actix-session = { version = "0.10", optional = true }
[dev-dependencies]
actix-web = "4"
static_assertions = "1"
uuid = { version = "1", features = ["v4"] }
[lints]
workspace = true

View File

@ -0,0 +1,58 @@
# actix-limitation
> Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web.
> Originally based on <https://github.com/fnichol/limitation>.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-limitation?label=latest)](https://crates.io/crates/actix-limitation)
[![Documentation](https://docs.rs/actix-limitation/badge.svg?version=0.5.1)](https://docs.rs/actix-limitation/0.5.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation)
[![Dependency Status](https://deps.rs/crate/actix-limitation/0.5.1/status.svg)](https://deps.rs/crate/actix-limitation/0.5.1)
<!-- prettier-ignore-end -->
## Examples
```toml
[dependencies]
actix-web = "4"
actix-limitation = "0.5"
```
```rust
use actix_limitation::{Limiter, RateLimiter};
use actix_session::SessionExt as _;
use actix_web::{dev::ServiceRequest, get, web, App, HttpServer, Responder};
use std::{sync::Arc, time::Duration};
#[get("/{id}/{name}")]
async fn index(info: web::Path<(u32, String)>) -> impl Responder {
format!("Hello {}! id:{}", info.1, info.0)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let limiter = web::Data::new(
Limiter::builder("redis://127.0.0.1")
.key_by(|req: &ServiceRequest| {
req.get_session()
.get(&"session-id")
.unwrap_or_else(|_| req.cookie(&"rate-api-id").map(|c| c.to_string()))
})
.limit(5000)
.period(Duration::from_secs(3600)) // 60 minutes
.build()
.unwrap(),
);
HttpServer::new(move || {
App::new()
.wrap(RateLimiter::default())
.app_data(limiter.clone())
.service(index)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
```

View File

@ -0,0 +1,171 @@
use std::{borrow::Cow, sync::Arc, time::Duration};
#[cfg(feature = "session")]
use actix_session::SessionExt as _;
use actix_web::dev::ServiceRequest;
use redis::Client;
use crate::{errors::Error, GetArcBoxKeyFn, Limiter};
/// Rate limiter builder.
#[derive(Debug)]
pub struct Builder {
pub(crate) redis_url: String,
pub(crate) limit: usize,
pub(crate) period: Duration,
pub(crate) get_key_fn: Option<GetArcBoxKeyFn>,
pub(crate) cookie_name: Cow<'static, str>,
#[cfg(feature = "session")]
pub(crate) session_key: Cow<'static, str>,
}
impl Builder {
/// Set upper limit.
pub fn limit(&mut self, limit: usize) -> &mut Self {
self.limit = limit;
self
}
/// Set limit window/period.
pub fn period(&mut self, period: Duration) -> &mut Self {
self.period = period;
self
}
/// Sets rate limit key derivation function.
///
/// Should not be used in combination with `cookie_name` or `session_key` as they conflict.
pub fn key_by<F>(&mut self, resolver: F) -> &mut Self
where
F: Fn(&ServiceRequest) -> Option<String> + Send + Sync + 'static,
{
self.get_key_fn = Some(Arc::new(resolver));
self
}
/// Sets name of cookie to be sent.
///
/// This method should not be used in combination of `key_by` as they conflict.
#[deprecated = "Prefer `key_by`."]
pub fn cookie_name(&mut self, cookie_name: impl Into<Cow<'static, str>>) -> &mut Self {
if self.get_key_fn.is_some() {
panic!("This method should not be used in combination of get_key as they overwrite each other")
}
self.cookie_name = cookie_name.into();
self
}
/// Sets session key to be used in backend.
///
/// This method should not be used in combination of `key_by` as they conflict.
#[deprecated = "Prefer `key_by`."]
#[cfg(feature = "session")]
pub fn session_key(&mut self, session_key: impl Into<Cow<'static, str>>) -> &mut Self {
if self.get_key_fn.is_some() {
panic!("This method should not be used in combination of get_key as they overwrite each other")
}
self.session_key = session_key.into();
self
}
/// Finalizes and returns a `Limiter`.
///
/// Note that this method will connect to the Redis server to test its connection which is a
/// **synchronous** operation.
pub fn build(&mut self) -> Result<Limiter, Error> {
let get_key = if let Some(resolver) = self.get_key_fn.clone() {
resolver
} else {
let cookie_name = self.cookie_name.clone();
#[cfg(feature = "session")]
let session_key = self.session_key.clone();
let closure: GetArcBoxKeyFn = Arc::new(Box::new(move |req: &ServiceRequest| {
#[cfg(feature = "session")]
let res = req
.get_session()
.get(&session_key)
.unwrap_or_else(|_| req.cookie(&cookie_name).map(|c| c.to_string()));
#[cfg(not(feature = "session"))]
let res = req.cookie(&cookie_name).map(|c| c.to_string());
res
}));
closure
};
Ok(Limiter {
client: Client::open(self.redis_url.as_str())?,
limit: self.limit,
period: self.period,
get_key_fn: get_key,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_builder() {
let redis_url = "redis://127.0.0.1";
let period = Duration::from_secs(10);
let builder = Builder {
redis_url: redis_url.to_owned(),
limit: 100,
period,
get_key_fn: Some(Arc::new(|_| None)),
cookie_name: Cow::Owned("session".to_string()),
#[cfg(feature = "session")]
session_key: Cow::Owned("rate-api".to_string()),
};
assert_eq!(builder.redis_url, redis_url);
assert_eq!(builder.limit, 100);
assert_eq!(builder.period, period);
#[cfg(feature = "session")]
assert_eq!(builder.session_key, "rate-api");
assert_eq!(builder.cookie_name, "session");
}
#[test]
fn test_create_limiter() {
let redis_url = "redis://127.0.0.1";
let period = Duration::from_secs(20);
let mut builder = Builder {
redis_url: redis_url.to_owned(),
limit: 100,
period: Duration::from_secs(10),
get_key_fn: Some(Arc::new(|_| None)),
cookie_name: Cow::Borrowed("sid"),
#[cfg(feature = "session")]
session_key: Cow::Borrowed("key"),
};
let limiter = builder.limit(200).period(period).build().unwrap();
assert_eq!(limiter.limit, 200);
assert_eq!(limiter.period, period);
}
#[test]
#[should_panic = "Redis URL did not parse"]
fn test_create_limiter_error() {
let redis_url = "127.0.0.1";
let period = Duration::from_secs(20);
let mut builder = Builder {
redis_url: redis_url.to_owned(),
limit: 100,
period: Duration::from_secs(10),
get_key_fn: Some(Arc::new(|_| None)),
cookie_name: Cow::Borrowed("sid"),
#[cfg(feature = "session")]
session_key: Cow::Borrowed("key"),
};
builder.limit(200).period(period).build().unwrap();
}
}

View File

@ -0,0 +1,42 @@
use derive_more::derive::{Display, Error, From};
use crate::status::Status;
/// Failure modes of the rate limiter.
#[derive(Debug, Display, Error, From)]
pub enum Error {
/// Redis client failed to connect or run a query.
#[display("Redis client failed to connect or run a query")]
Client(redis::RedisError),
/// Limit is exceeded for a key.
#[display("Limit is exceeded for a key")]
#[from(ignore)]
LimitExceeded(#[error(not(source))] Status),
/// Time conversion failed.
#[display("Time conversion failed")]
Time(time::error::ComponentRange),
/// Generic error.
#[display("Generic error")]
#[from(ignore)]
Other(#[error(not(source))] String),
}
#[cfg(test)]
mod tests {
use super::*;
static_assertions::assert_impl_all! {
Error:
From<redis::RedisError>,
From<time::error::ComponentRange>,
}
static_assertions::assert_not_impl_any! {
Error:
From<String>,
From<Status>,
}
}

179
actix-limitation/src/lib.rs Normal file
View File

@ -0,0 +1,179 @@
//! Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web.
//!
//! ```toml
//! [dependencies]
//! actix-web = "4"
#![doc = concat!("actix-limitation = \"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"),"\"")]
//! ```
//!
//! ```no_run
//! use std::{sync::Arc, time::Duration};
//! use actix_web::{dev::ServiceRequest, get, web, App, HttpServer, Responder};
//! use actix_session::SessionExt as _;
//! use actix_limitation::{Limiter, RateLimiter};
//!
//! #[get("/{id}/{name}")]
//! async fn index(info: web::Path<(u32, String)>) -> impl Responder {
//! format!("Hello {}! id:{}", info.1, info.0)
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! let limiter = web::Data::new(
//! Limiter::builder("redis://127.0.0.1")
//! .key_by(|req: &ServiceRequest| {
//! req.get_session()
//! .get(&"session-id")
//! .unwrap_or_else(|_| req.cookie(&"rate-api-id").map(|c| c.to_string()))
//! })
//! .limit(5000)
//! .period(Duration::from_secs(3600)) // 60 minutes
//! .build()
//! .unwrap(),
//! );
//!
//! HttpServer::new(move || {
//! App::new()
//! .wrap(RateLimiter::default())
//! .app_data(limiter.clone())
//! .service(index)
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! }
//! ```
#![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{borrow::Cow, fmt, sync::Arc, time::Duration};
use actix_web::dev::ServiceRequest;
use redis::Client;
mod builder;
mod errors;
mod middleware;
mod status;
pub use self::{builder::Builder, errors::Error, middleware::RateLimiter, status::Status};
/// Default request limit.
pub const DEFAULT_REQUEST_LIMIT: usize = 5000;
/// Default period (in seconds).
pub const DEFAULT_PERIOD_SECS: u64 = 3600;
/// Default cookie name.
pub const DEFAULT_COOKIE_NAME: &str = "sid";
/// Default session key.
#[cfg(feature = "session")]
pub const DEFAULT_SESSION_KEY: &str = "rate-api-id";
/// Helper trait to impl Debug on GetKeyFn type
trait GetKeyFnT: Fn(&ServiceRequest) -> Option<String> {}
impl<T> GetKeyFnT for T where T: Fn(&ServiceRequest) -> Option<String> {}
/// Get key function type with auto traits
type GetKeyFn = dyn GetKeyFnT + Send + Sync;
/// Get key resolver function type
impl fmt::Debug for GetKeyFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "GetKeyFn")
}
}
/// Wrapped Get key function Trait
type GetArcBoxKeyFn = Arc<GetKeyFn>;
/// Rate limiter.
#[derive(Debug, Clone)]
pub struct Limiter {
client: Client,
limit: usize,
period: Duration,
get_key_fn: GetArcBoxKeyFn,
}
impl Limiter {
/// Construct rate limiter builder with defaults.
///
/// See [`redis-rs` docs](https://docs.rs/redis/0.21/redis/#connection-parameters) on connection
/// parameters for how to set the Redis URL.
#[must_use]
pub fn builder(redis_url: impl Into<String>) -> Builder {
Builder {
redis_url: redis_url.into(),
limit: DEFAULT_REQUEST_LIMIT,
period: Duration::from_secs(DEFAULT_PERIOD_SECS),
get_key_fn: None,
cookie_name: Cow::Borrowed(DEFAULT_COOKIE_NAME),
#[cfg(feature = "session")]
session_key: Cow::Borrowed(DEFAULT_SESSION_KEY),
}
}
/// Consumes one rate limit unit, returning the status.
pub async fn count(&self, key: impl Into<String>) -> Result<Status, Error> {
let (count, reset) = self.track(key).await?;
let status = Status::new(count, self.limit, reset);
if count > self.limit {
Err(Error::LimitExceeded(status))
} else {
Ok(status)
}
}
/// Tracks the given key in a period and returns the count and TTL for the key in seconds.
async fn track(&self, key: impl Into<String>) -> Result<(usize, usize), Error> {
let key = key.into();
let expires = self.period.as_secs();
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
// NodeJS and Redis. For more details, see https://blog.atulr.com/rate-limiter
let mut pipe = redis::pipe();
pipe.atomic()
.cmd("SET") // Set key and value
.arg(&key)
.arg(0)
.arg("EX") // Set the specified expire time, in seconds.
.arg(expires)
.arg("NX") // Only set the key if it does not already exist.
.ignore() // --- ignore returned value of SET command ---
.cmd("INCR") // Increment key
.arg(&key)
.cmd("TTL") // Return time-to-live of key
.arg(&key);
let (count, ttl) = pipe.query_async(&mut connection).await?;
let reset = Status::epoch_utc_plus(Duration::from_secs(ttl))?;
Ok((count, reset))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_limiter() {
let mut builder = Limiter::builder("redis://127.0.0.1:6379/1");
let limiter = builder.build();
assert!(limiter.is_ok());
let limiter = limiter.unwrap();
assert_eq!(limiter.limit, 5000);
assert_eq!(limiter.period, Duration::from_secs(3600));
}
}

View File

@ -0,0 +1,115 @@
use std::{future::Future, pin::Pin, rc::Rc};
use actix_utils::future::{ok, Ready};
use actix_web::{
body::EitherBody,
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
http::StatusCode,
web, Error, HttpResponse,
};
use crate::{Error as LimitationError, Limiter};
/// Rate limit middleware.
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct RateLimiter;
impl<S, B> Transform<S, ServiceRequest> for RateLimiter
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = RateLimiterMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(RateLimiterMiddleware {
service: Rc::new(service),
})
}
}
/// Rate limit middleware service.
#[derive(Debug)]
pub struct RateLimiterMiddleware<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for RateLimiterMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
// A misconfiguration of the Actix App will result in a **runtime** failure, so the expect
// method description is important context for the developer.
let limiter = req
.app_data::<web::Data<Limiter>>()
.expect("web::Data<Limiter> should be set in app data for RateLimiter middleware")
.clone();
let key = (limiter.get_key_fn)(&req);
let service = Rc::clone(&self.service);
let key = match key {
Some(key) => key,
None => {
return Box::pin(async move {
service
.call(req)
.await
.map(ServiceResponse::map_into_left_body)
});
}
};
Box::pin(async move {
let status = limiter.count(key.to_string()).await;
if let Err(err) = status {
match err {
LimitationError::LimitExceeded(_) => {
log::warn!("Rate limit exceed error for {}", key);
Ok(req.into_response(
HttpResponse::new(StatusCode::TOO_MANY_REQUESTS).map_into_right_body(),
))
}
LimitationError::Client(e) => {
log::error!("Client request failed, redis error: {}", e);
Ok(req.into_response(
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
.map_into_right_body(),
))
}
_ => {
log::error!("Count failed: {}", err);
Ok(req.into_response(
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
.map_into_right_body(),
))
}
}
} else {
service
.call(req)
.await
.map(ServiceResponse::map_into_left_body)
}
})
}
}

View File

@ -0,0 +1,118 @@
use std::{ops::Add, time::Duration};
use chrono::SubsecRound as _;
use crate::Error as LimitationError;
/// A report for a given key containing the limit status.
#[derive(Debug, Clone)]
pub struct Status {
pub(crate) limit: usize,
pub(crate) remaining: usize,
pub(crate) reset_epoch_utc: usize,
}
impl Status {
/// Constructs status limit status from parts.
#[must_use]
pub(crate) fn new(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
let remaining = limit.saturating_sub(count);
Status {
limit,
remaining,
reset_epoch_utc,
}
}
/// Returns the maximum number of requests allowed in the current period.
#[must_use]
pub fn limit(&self) -> usize {
self.limit
}
/// Returns how many requests are left in the current period.
#[must_use]
pub fn remaining(&self) -> usize {
self.remaining
}
/// Returns a UNIX timestamp in UTC approximately when the next period will begin.
#[must_use]
pub fn reset_epoch_utc(&self) -> usize {
self.reset_epoch_utc
}
pub(crate) fn epoch_utc_plus(duration: Duration) -> Result<usize, LimitationError> {
match chrono::Duration::from_std(duration) {
Ok(value) => Ok(chrono::Utc::now()
.add(value)
.round_subsecs(0)
.timestamp()
.try_into()
.unwrap_or(0)),
Err(_) => Err(LimitationError::Other(
"Source duration value is out of range for the target type".to_string(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_status() {
let status = Status {
limit: 100,
remaining: 0,
reset_epoch_utc: 1000,
};
assert_eq!(status.limit(), 100);
assert_eq!(status.remaining(), 0);
assert_eq!(status.reset_epoch_utc(), 1000);
}
#[test]
fn test_build_status() {
let count = 200;
let limit = 100;
let status = Status::new(count, limit, 2000);
assert_eq!(status.limit(), limit);
assert_eq!(status.remaining(), 0);
assert_eq!(status.reset_epoch_utc(), 2000);
}
#[test]
fn test_build_status_limit() {
let limit = 100;
let status = Status::new(0, limit, 2000);
assert_eq!(status.limit(), limit);
assert_eq!(status.remaining(), limit);
assert_eq!(status.reset_epoch_utc(), 2000);
}
#[test]
fn test_epoch_utc_plus_zero() {
let duration = Duration::from_secs(0);
let seconds = Status::epoch_utc_plus(duration).unwrap();
assert!(seconds as u64 >= duration.as_secs());
}
#[test]
fn test_epoch_utc_plus() {
let duration = Duration::from_secs(10);
let seconds = Status::epoch_utc_plus(duration).unwrap();
assert!(seconds as u64 >= duration.as_secs() + 10);
}
#[test]
#[should_panic = "Source duration value is out of range for the target type"]
fn test_epoch_utc_plus_overflow() {
let duration = Duration::from_secs(10000000000000000000);
Status::epoch_utc_plus(duration).unwrap();
}
}

View File

@ -0,0 +1,92 @@
use std::time::Duration;
use actix_limitation::{Error, Limiter, RateLimiter};
use actix_web::{dev::ServiceRequest, http::StatusCode, test, web, App, HttpRequest, HttpResponse};
use uuid::Uuid;
#[test]
#[should_panic = "Redis URL did not parse"]
async fn test_create_limiter_error() {
Limiter::builder("127.0.0.1").build().unwrap();
}
#[actix_web::test]
async fn test_limiter_count() -> Result<(), Error> {
let limiter = Limiter::builder("redis://127.0.0.1:6379/2")
.limit(20)
.build()
.unwrap();
let id = Uuid::new_v4();
for i in 0..20 {
let status = limiter.count(id.to_string()).await?;
println!("status: {status:?}");
assert_eq!(20 - status.remaining(), i + 1);
}
Ok(())
}
#[actix_web::test]
async fn test_limiter_count_error() -> Result<(), Error> {
let limiter = Limiter::builder("redis://127.0.0.1:6379/3")
.limit(25)
.build()
.unwrap();
let id = Uuid::new_v4();
for i in 0..25 {
let status = limiter.count(id.to_string()).await?;
assert_eq!(25 - status.remaining(), i + 1);
}
match limiter.count(id.to_string()).await.unwrap_err() {
Error::LimitExceeded(status) => assert_eq!(status.remaining(), 0),
_ => panic!("error should be LimitExceeded variant"),
};
let id = Uuid::new_v4();
for i in 0..25 {
let status = limiter.count(id.to_string()).await?;
assert_eq!(25 - status.remaining(), i + 1);
}
Ok(())
}
#[actix_web::test]
async fn test_limiter_key_by() -> Result<(), Error> {
let cooldown_period = Duration::from_secs(1);
let limiter = Limiter::builder("redis://127.0.0.1:6379/3")
.limit(2)
.period(cooldown_period)
.key_by(|_: &ServiceRequest| Some("fix_key".to_string()))
.build()
.unwrap();
let app = test::init_service(
App::new()
.wrap(RateLimiter::default())
.app_data(web::Data::new(limiter))
.route(
"/",
web::get().to(|_: HttpRequest| async { HttpResponse::Ok().body("ok") }),
),
)
.await;
for _ in 1..2 {
for index in 1..4 {
let req = test::TestRequest::default().to_request();
let resp = test::call_service(&app, req).await;
if index <= 2 {
assert!(resp.status().is_success());
} else {
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
}
}
std::thread::sleep(cooldown_period);
}
Ok(())
}

View File

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

View File

@ -1,30 +1,31 @@
[package]
name = "actix-protobuf"
version = "0.5.1"
edition = "2018"
authors = ["kingxsp <jin.hb.zh@outlook.com>", "Yuki Okushi <huyuumi.dev@gmail.com>"]
description = "Protobuf support for actix-web framework."
readme = "README.md"
keywords = ["actix"]
homepage = "https://github.com/actix/actix-extras"
repository = "https://github.com/actix/actix-extras.git"
license = "MIT/Apache-2.0"
exclude = [".cargo/config", "/examples/**"]
version = "0.11.0"
authors = [
"kingxsp <jin.hb.zh@outlook.com>",
"Yuki Okushi <huyuumi.dev@gmail.com>",
]
description = "Protobuf payload extractor for Actix Web"
keywords = ["actix", "web", "protobuf", "protocol", "rpc"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[lib]
name = "actix_protobuf"
path = "src/lib.rs"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[dependencies]
bytes = "0.5"
futures = "0.3.1"
derive_more = "0.99"
actix = "0.9"
actix-rt = "1"
actix-web = "2"
prost = "0.6.0"
actix-web = { version = "4", default-features = false }
derive_more = { version = "2", features = ["display"] }
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
prost = { version = "0.13", default-features = false }
[dev-dependencies]
prost-derive = "0.6.0"
actix-web = { version = "4", default-features = false, features = ["macros"] }
prost = { version = "0.13", default-features = false, features = ["prost-derive"] }
[lints]
workspace = true

View File

@ -1,14 +1,21 @@
# actix-protobuf
[![crates.io](https://img.shields.io/crates/v/actix-protobuf)](https://crates.io/crates/actix-protobuf)
[![Documentation](https://docs.rs/actix-protobuf/badge.svg)](https://docs.rs/actix-protobuf)
[![Dependency Status](https://deps.rs/crate/actix-protobuf/0.5.1/status.svg)](https://deps.rs/crate/actix-protobuf/0.5.1)
> Protobuf payload extractor for Actix Web.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-protobuf?label=latest)](https://crates.io/crates/actix-protobuf)
[![Documentation](https://docs.rs/actix-protobuf/badge.svg?version=0.11.0)](https://docs.rs/actix-protobuf/0.11.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-protobuf)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Dependency Status](https://deps.rs/crate/actix-protobuf/0.11.0/status.svg)](https://deps.rs/crate/actix-protobuf/0.11.0)
> Protobuf support for actix-web framework.
<!-- prettier-ignore-end -->
* Minimum supported Rust version: 1.39.0 or later
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-protobuf)
- [Example Project](https://github.com/actix/examples/tree/master/protobuf)
- Minimum Supported Rust Version (MSRV): 1.57
## Example
@ -20,6 +27,7 @@ use actix_web::*;
pub struct MyObj {
#[prost(int32, tag = "1")]
pub number: i32,
#[prost(string, tag = "2")]
pub name: String,
}
@ -30,13 +38,13 @@ async fn index(msg: ProtoBuf<MyObj>) -> Result<HttpResponse> {
}
```
See [here](https://github.com/actix/actix-protobuf/tree/master/examples/prost-example) for the complete example.
See [here](https://github.com/actix/examples/tree/master/protobuf) for the complete example.
## License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))
at your option.

View File

@ -1,17 +0,0 @@
[package]
name = "prost-example"
version = "0.5.1"
edition = "2018"
authors = ["kingxsp <jin.hb.zh@outlook.com>", "Yuki Okushi <huyuumi.dev@gmail.com>"]
[dependencies]
bytes = "0.5"
env_logger = "*"
prost = "0.6.0"
prost-derive = "0.6.0"
actix = "0.9"
actix-rt = "1"
actix-web = "2"
actix-protobuf = { path="../../" }

View File

@ -1,68 +0,0 @@
#!/usr/bin/env python3
# just start server and run client.py
# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.2/protobuf-python-3.11.2.zip
# unzip protobuf-python-3.11.2.zip
# cd protobuf-3.11.2/python/
# python3 setup.py install
# pip3 install --upgrade pip
# pip3 install aiohttp
# python3 client.py
import test_pb2
import traceback
import sys
import asyncio
import aiohttp
def op():
try:
obj = test_pb2.MyObj()
obj.number = 9
obj.name = 'USB'
#Serialize
sendDataStr = obj.SerializeToString()
#print serialized string value
print('serialized string:', sendDataStr)
#------------------------#
# message transmission #
#------------------------#
receiveDataStr = sendDataStr
receiveData = test_pb2.MyObj()
#Deserialize
receiveData.ParseFromString(receiveDataStr)
print('pares serialize string, return: devId = ', receiveData.number, ', name = ', receiveData.name)
except(Exception, e):
print(Exception, ':', e)
print(traceback.print_exc())
errInfo = sys.exc_info()
print(errInfo[0], ':', errInfo[1])
async def fetch(session):
obj = test_pb2.MyObj()
obj.number = 9
obj.name = 'USB'
async with session.post('http://127.0.0.1:8081/', data=obj.SerializeToString(),
headers={"content-type": "application/protobuf"}) as resp:
print(resp.status)
data = await resp.read()
receiveObj = test_pb2.MyObj()
receiveObj.ParseFromString(data)
print(receiveObj)
async def go(loop):
obj = test_pb2.MyObj()
obj.number = 9
obj.name = 'USB'
async with aiohttp.ClientSession(loop=loop) as session:
await fetch(session)
loop = asyncio.get_event_loop()
loop.run_until_complete(go(loop))
loop.close()

View File

@ -1,34 +0,0 @@
#[macro_use]
extern crate prost_derive;
use actix_protobuf::*;
use actix_web::*;
#[derive(Clone, PartialEq, Message)]
pub struct MyObj {
#[prost(int32, tag = "1")]
pub number: i32,
#[prost(string, tag = "2")]
pub name: String,
}
async fn index(msg: ProtoBuf<MyObj>) -> Result<HttpResponse> {
println!("model: {:?}", msg);
HttpResponse::Ok().protobuf(msg.0) // <- send response
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.service(web::resource("/").route(web::post().to(index)))
})
.bind("127.0.0.1:8081")?
.shutdown_timeout(1)
.run()
.await
}

View File

@ -1,6 +0,0 @@
syntax = "proto3";
message MyObj {
int32 number = 1;
string name = 2;
}

View File

@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: test.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='test.proto',
package='',
syntax='proto3',
serialized_options=None,
serialized_pb=b'\n\ntest.proto\"%\n\x05MyObj\x12\x0e\n\x06number\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3'
)
_MYOBJ = _descriptor.Descriptor(
name='MyObj',
full_name='MyObj',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='number', full_name='MyObj.number', index=0,
number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='name', full_name='MyObj.name', index=1,
number=2, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=14,
serialized_end=51,
)
DESCRIPTOR.message_types_by_name['MyObj'] = _MYOBJ
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
MyObj = _reflection.GeneratedProtocolMessageType('MyObj', (_message.Message,), {
'DESCRIPTOR' : _MYOBJ,
'__module__' : 'test_pb2'
# @@protoc_insertion_point(class_scope:MyObj)
})
_sym_db.RegisterMessage(MyObj)
# @@protoc_insertion_point(module_scope)

View File

@ -1,42 +1,59 @@
use derive_more::Display;
use std::fmt;
use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::task;
use std::task::Poll;
//! Protobuf payload extractor for Actix Web.
use bytes::BytesMut;
use prost::DecodeError as ProtoBufDecodeError;
use prost::EncodeError as ProtoBufEncodeError;
use prost::Message;
#![forbid(unsafe_code)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use actix_web::dev::{HttpResponseBuilder, Payload};
use actix_web::error::{Error, PayloadError, ResponseError};
use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
use actix_web::{FromRequest, HttpMessage, HttpRequest, HttpResponse, Responder};
use futures::future::{ready, FutureExt, LocalBoxFuture, Ready};
use futures::StreamExt;
use std::{
fmt,
future::Future,
ops::{Deref, DerefMut},
pin::Pin,
task::{self, Poll},
};
use actix_web::{
body::BoxBody,
dev::Payload,
error::PayloadError,
http::header::{CONTENT_LENGTH, CONTENT_TYPE},
web::BytesMut,
Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder, Responder,
ResponseError,
};
use derive_more::derive::Display;
use futures_util::{
future::{FutureExt as _, LocalBoxFuture},
stream::StreamExt as _,
};
use prost::{DecodeError as ProtoBufDecodeError, EncodeError as ProtoBufEncodeError, Message};
#[derive(Debug, Display)]
pub enum ProtoBufPayloadError {
/// Payload size is bigger than 256k
#[display(fmt = "Payload size is bigger than 256k")]
#[display("Payload size is bigger than 256k")]
Overflow,
/// Content type error
#[display(fmt = "Content type error")]
#[display("Content type error")]
ContentType,
/// Serialize error
#[display(fmt = "ProtoBuf serialize error: {}", _0)]
#[display("ProtoBuf serialize error: {_0}")]
Serialize(ProtoBufEncodeError),
/// Deserialize error
#[display(fmt = "ProtoBuf deserialize error: {}", _0)]
#[display("ProtoBuf deserialize error: {_0}")]
Deserialize(ProtoBufDecodeError),
/// Payload error
#[display(fmt = "Error that occur during reading payload: {}", _0)]
#[display("Error that occur during reading payload: {_0}")]
Payload(PayloadError),
}
// TODO: impl error for ProtoBufPayloadError
impl ResponseError for ProtoBufPayloadError {
fn error_response(&self) -> HttpResponse {
match *self {
@ -78,7 +95,7 @@ impl<T: Message> fmt::Debug for ProtoBuf<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ProtoBuf: {:?}", self.0)
}
}
@ -87,7 +104,7 @@ impl<T: Message> fmt::Display for ProtoBuf<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
@ -114,7 +131,6 @@ impl<T> FromRequest for ProtoBuf<T>
where
T: Message + Default + 'static,
{
type Config = ProtoBufConfig;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self, Error>>;
@ -135,21 +151,16 @@ where
}
impl<T: Message + Default> Responder for ProtoBuf<T> {
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
type Body = BoxBody;
fn respond_to(self, _: &HttpRequest) -> Self::Future {
fn respond_to(self, _: &HttpRequest) -> HttpResponse {
let mut buf = Vec::new();
ready(
self.0
.encode(&mut buf)
.map_err(|e| Error::from(ProtoBufPayloadError::Serialize(e)))
.and_then(|()| {
Ok(HttpResponse::Ok()
.content_type("application/protobuf")
.body(buf))
}),
)
match self.0.encode(&mut buf) {
Ok(()) => HttpResponse::Ok()
.content_type("application/protobuf")
.body(buf),
Err(err) => HttpResponse::from_error(Error::from(ProtoBufPayloadError::Serialize(err))),
}
}
}
@ -164,7 +175,9 @@ pub struct ProtoBufMessage<T: Message + Default> {
impl<T: Message + Default> ProtoBufMessage<T> {
/// Create `ProtoBufMessage` for request.
pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self {
if req.content_type() != "application/protobuf" {
if req.content_type() != "application/protobuf"
&& req.content_type() != "application/x-protobuf"
{
return ProtoBufMessage {
limit: 262_144,
length: None,
@ -202,10 +215,7 @@ impl<T: Message + Default> ProtoBufMessage<T> {
impl<T: Message + Default + 'static> Future for ProtoBufMessage<T> {
type Output = Result<T, ProtoBufPayloadError>;
fn poll(
mut self: Pin<&mut Self>,
task: &mut task::Context<'_>,
) -> Poll<Self::Output> {
fn poll(mut self: Pin<&mut Self>, task: &mut task::Context<'_>) -> Poll<Self::Output> {
if let Some(ref mut fut) = self.fut {
return Pin::new(fut).poll(task);
}
@ -239,7 +249,7 @@ impl<T: Message + Default + 'static> Future for ProtoBufMessage<T> {
}
}
return Ok(<T>::decode(&mut body)?);
Ok(<T>::decode(&mut body)?)
}
.boxed_local(),
);
@ -253,39 +263,38 @@ pub trait ProtoBufResponseBuilder {
impl ProtoBufResponseBuilder for HttpResponseBuilder {
fn protobuf<T: Message>(&mut self, value: T) -> Result<HttpResponse, Error> {
self.header(CONTENT_TYPE, "application/protobuf");
self.insert_header((CONTENT_TYPE, "application/protobuf"));
let mut body = Vec::new();
value
.encode(&mut body)
.map_err(ProtoBufPayloadError::Serialize)?;
Ok(self.body(body))
}
}
#[cfg(test)]
mod tests {
use actix_web::{http::header, test::TestRequest};
use super::*;
use actix_web::http::header;
use actix_web::test::TestRequest;
impl PartialEq for ProtoBufPayloadError {
fn eq(&self, other: &ProtoBufPayloadError) -> bool {
match *self {
ProtoBufPayloadError::Overflow => match *other {
ProtoBufPayloadError::Overflow => true,
_ => false,
},
ProtoBufPayloadError::ContentType => match *other {
ProtoBufPayloadError::ContentType => true,
_ => false,
},
ProtoBufPayloadError::Overflow => {
matches!(*other, ProtoBufPayloadError::Overflow)
}
ProtoBufPayloadError::ContentType => {
matches!(*other, ProtoBufPayloadError::ContentType)
}
_ => false,
}
}
}
#[derive(Clone, PartialEq, Message)]
#[derive(Clone, PartialEq, Eq, Message)]
pub struct MyObject {
#[prost(int32, tag = "1")]
pub number: i32,
@ -293,36 +302,34 @@ mod tests {
pub name: String,
}
#[actix_rt::test]
#[actix_web::test]
async fn test_protobuf() {
let protobuf = ProtoBuf(MyObject {
number: 9,
name: "test".to_owned(),
});
let req = TestRequest::default().to_http_request();
let resp = protobuf.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/protobuf"
);
let resp = protobuf.respond_to(&req);
let ct = resp.headers().get(header::CONTENT_TYPE).unwrap();
assert_eq!(ct, "application/protobuf");
}
#[actix_rt::test]
#[actix_web::test]
async fn test_protobuf_message() {
let (req, mut pl) = TestRequest::default().to_http_parts();
let protobuf = ProtoBufMessage::<MyObject>::new(&req, &mut pl).await;
assert_eq!(protobuf.err().unwrap(), ProtoBufPayloadError::ContentType);
let (req, mut pl) =
TestRequest::with_header(header::CONTENT_TYPE, "application/text")
.to_http_parts();
let (req, mut pl) = TestRequest::get()
.insert_header((header::CONTENT_TYPE, "application/text"))
.to_http_parts();
let protobuf = ProtoBufMessage::<MyObject>::new(&req, &mut pl).await;
assert_eq!(protobuf.err().unwrap(), ProtoBufPayloadError::ContentType);
let (req, mut pl) =
TestRequest::with_header(header::CONTENT_TYPE, "application/protobuf")
.header(header::CONTENT_LENGTH, "10000")
.to_http_parts();
let (req, mut pl) = TestRequest::get()
.insert_header((header::CONTENT_TYPE, "application/protobuf"))
.insert_header((header::CONTENT_LENGTH, "10000"))
.to_http_parts();
let protobuf = ProtoBufMessage::<MyObject>::new(&req, &mut pl)
.limit(100)
.await;

View File

@ -1,65 +0,0 @@
# Changes
## [0.8.0] 2019-12-20
* Release
## [0.8.0-alpha.1] 2019-12-16
* Migrate to actix 0.9
## 0.7 (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

View File

@ -1,50 +0,0 @@
[package]
name = "actix-redis"
version = "0.8.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Redis integration for actix framework"
license = "MIT/Apache-2.0"
readme = "README.md"
keywords = ["web", "redis", "async", "actix", "tokio"]
homepage = "https://github.com/actix/actix-extras"
repository = "https://github.com/actix/actix-extras.git"
documentation = "https://docs.rs/actix-redis/"
categories = ["network-programming", "asynchronous"]
exclude = [".travis.yml", ".cargo/config"]
edition = "2018"
[lib]
name = "actix_redis"
path = "src/lib.rs"
[features]
default = ["web"]
# actix-web integration
web = ["actix/http", "actix-service", "actix-web", "actix-session/cookie-session", "rand", "serde", "serde_json"]
[dependencies]
actix = "0.9.0"
actix-utils = "1.0.3"
log = "0.4.6"
backoff = "0.1.5"
derive_more = "0.99.2"
futures = "0.3.1"
redis-async = "0.6.1"
actix-rt = "1.0.0"
time = "0.1.42"
tokio = "0.2.6"
tokio-util = "0.2.0"
# actix-session
actix-web = { version = "2.0.0", optional = true }
actix-service = { version = "1.0.0", optional = true }
actix-session = { version = "0.3.0", optional = true }
rand = { version = "0.7.0", optional = true }
serde = { version = "1.0.101", optional = true, features = ["derive"] }
serde_json = { version = "1.0.40", optional = true }
env_logger = "0.6.2"
[dev-dependencies]
env_logger = "0.6"

View File

@ -1,67 +0,0 @@
# actix-redis
[![crates.io](https://img.shields.io/crates/v/actix-redis)](https://crates.io/crates/actix-redis)
[![Documentation](https://docs.rs/actix-redis/badge.svg)](https://docs.rs/actix-redis)
[![Dependency Status](https://deps.rs/crate/actix-redis/0.8.0/status.svg)](https://deps.rs/crate/actix-redis/0.8.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-redis)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
> Redis integration for actix framework.
## Documentation
* [API Documentation](http://actix.rs/actix-extras/actix_redis/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-redis](https://crates.io/crates/actix-redis)
* Minimum supported Rust version: 1.39 or later
## Redis session backend
Use redis as session storage.
You need to pass an address of the redis server and random value to the
constructor of `RedisSessionBackend`. This is private key for cookie session,
When this value is changed, all session data is lost.
Note that whatever you write into your session is visible by the user (but not modifiable).
Constructor panics if key length is less than 32 bytes.
```rust
use actix_web::{App, HttpServer, web, middleware};
use actix_web::middleware::session::SessionStorage;
use actix_redis::RedisSessionBackend;
#[actix_rt::main]
async fn main() -> std::io::Result {
HttpServer::new(|| App::new()
// enable logger
.middleware(middleware::Logger::default())
// cookie session middleware
.middleware(SessionStorage::new(
RedisSessionBackend::new("127.0.0.1:6379", &[0; 32])
))
// register simple route, handle all methods
.service(web::resource("/").to(index))
)
.bind("0.0.0.0:8080")?
.start()
.await
}
```
## License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
at your option.
## Code of Conduct
Contribution to the actix-redis crate is organized under the terms of the
Contributor Covenant, the maintainer of actix-redis, @fafhrd91, promises to
intervene to uphold that code of conduct.

View File

@ -1,37 +0,0 @@
use actix_redis::RedisSession;
use actix_session::Session;
use actix_web::{middleware, web, App, Error, HttpRequest, HttpServer, Responder};
/// simple handler
async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> {
println!("{:?}", req);
// session
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {}", count);
session.set("counter", count + 1)?;
} else {
session.set("counter", 1)?;
}
Ok("Welcome!")
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info,actix_redis=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
// cookie session middleware
.wrap(RedisSession::new("127.0.0.1:6379", &[0; 32]))
// register simple route, handle all methods
.service(web::resource("/").to(index))
})
.bind("0.0.0.0:8080")?
.run()
.await
}

View File

@ -1,45 +0,0 @@
//! Redis integration for Actix framework.
//!
//! ## Documentation
//! * [API Documentation (Development)](http://actix.github.io/actix-redis/actix_redis/)
//! * [API Documentation (Releases)](https://docs.rs/actix-redis/)
//! * [Chat on gitter](https://gitter.im/actix/actix)
//! * Cargo package: [actix-redis](https://crates.io/crates/actix-redis)
//! * Minimum supported Rust version: 1.26 or later
//!
#[macro_use]
extern crate log;
#[macro_use]
extern crate redis_async;
#[macro_use]
extern crate derive_more;
mod redis;
pub use redis::{Command, RedisActor};
#[cfg(feature = "web")]
mod session;
#[cfg(feature = "web")]
pub use actix_web::cookie::SameSite;
#[cfg(feature = "web")]
pub use session::RedisSession;
/// General purpose actix redis error
#[derive(Debug, Display, 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 waters when connection get dropped
#[display(fmt = "Redis: Disconnected")]
Disconnected,
}
#[cfg(feature = "web")]
impl actix_web::ResponseError for Error {}
// re-export
pub use redis_async::error::Error as RespError;
pub use redis_async::resp::RespValue;

View File

@ -1,147 +0,0 @@
use std::collections::VecDeque;
use std::io;
use actix::actors::resolver::{Connect, Resolver};
use actix::prelude::*;
use actix_utils::oneshot;
use backoff::backoff::Backoff;
use backoff::ExponentialBackoff;
use futures::FutureExt;
use redis_async::error::Error as RespError;
use redis_async::resp::{RespCodec, RespValue};
use tokio::io::{split, WriteHalf};
use tokio::net::TcpStream;
use tokio_util::codec::FramedRead;
use crate::Error;
/// Command for send data to Redis
#[derive(Debug)]
pub struct Command(pub RespValue);
impl Message for Command {
type Result = Result<RespValue, Error>;
}
/// Redis comminucation actor
pub struct RedisActor {
addr: String,
backoff: ExponentialBackoff,
cell: Option<actix::io::FramedWrite<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 mut backoff = ExponentialBackoff::default();
backoff.max_elapsed_time = None;
Supervisor::start(|_| RedisActor {
addr,
cell: None,
backoff,
queue: VecDeque::new(),
})
}
}
impl Actor for RedisActor {
type Context = Context<Self>;
fn started(&mut self, ctx: &mut Context<Self>) {
Resolver::from_registry()
.send(Connect::host(self.addr.as_str()))
.into_actor(self)
.map(|res, act, ctx| match res {
Ok(res) => match res {
Ok(stream) => {
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());
}
}
},
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(rx.map(|res| match res {
Ok(res) => res,
Err(_) => Err(Error::Disconnected),
}))
}
}

View File

@ -1,661 +0,0 @@
use std::cell::RefCell;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{collections::HashMap, iter, rc::Rc};
use actix::prelude::*;
use actix_service::{Service, Transform};
use actix_session::{Session, SessionStatus};
use actix_web::cookie::{Cookie, CookieJar, Key, SameSite};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::header::{self, HeaderValue};
use actix_web::{error, Error, HttpMessage};
use futures::future::{ok, Future, Ready};
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
use redis_async::resp::RespValue;
use time::{self, Duration};
use crate::redis::{Command, RedisActor};
/// Use redis as session storage.
///
/// You need to pass an address of the redis server and random value to the
/// constructor of `RedisSessionBackend`. This is private key for cookie
/// session, When this value is changed, all session data is lost.
///
/// Constructor panics if key length is less than 32 bytes.
pub struct RedisSession(Rc<Inner>);
impl RedisSession {
/// Create new redis session backend
///
/// * `addr` - address of the redis server
pub fn new<S: Into<String>>(addr: S, key: &[u8]) -> RedisSession {
RedisSession(Rc::new(Inner {
key: Key::from_master(key),
cache_keygen: Box::new(|key: &str| format!("session:{}", &key)),
ttl: "7200".to_owned(),
addr: RedisActor::start(addr),
name: "actix-session".to_owned(),
path: "/".to_owned(),
domain: None,
secure: false,
max_age: Some(Duration::days(7)),
same_site: None,
}))
}
/// Set time to live in seconds for session value
pub fn ttl(mut self, ttl: u16) -> Self {
Rc::get_mut(&mut self.0).unwrap().ttl = format!("{}", ttl);
self
}
/// Set custom cookie name for session id
pub fn cookie_name(mut self, name: &str) -> Self {
Rc::get_mut(&mut self.0).unwrap().name = name.to_owned();
self
}
/// Set custom cookie path
pub fn cookie_path(mut self, path: &str) -> Self {
Rc::get_mut(&mut self.0).unwrap().path = path.to_owned();
self
}
/// Set custom cookie domain
pub fn cookie_domain(mut self, domain: &str) -> Self {
Rc::get_mut(&mut self.0).unwrap().domain = Some(domain.to_owned());
self
}
/// Set custom cookie secure
/// If the `secure` field is set, a cookie will only be transmitted when the
/// connection is secure - i.e. `https`
pub fn cookie_secure(mut self, secure: bool) -> Self {
Rc::get_mut(&mut self.0).unwrap().secure = secure;
self
}
/// Set custom cookie max-age
pub fn cookie_max_age(mut self, max_age: Duration) -> Self {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(max_age);
self
}
/// Set custom cookie SameSite
pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
self
}
/// Set a custom cache key generation strategy, expecting session key as input
pub fn cache_keygen(mut self, keygen: Box<dyn Fn(&str) -> String>) -> Self {
Rc::get_mut(&mut self.0).unwrap().cache_keygen = keygen;
self
}
}
impl<S, B> Transform<S> for RedisSession
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>
+ 'static,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = RedisSessionMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(RedisSessionMiddleware {
service: Rc::new(RefCell::new(service)),
inner: self.0.clone(),
})
}
}
/// Cookie session middleware
pub struct RedisSessionMiddleware<S: 'static> {
service: Rc<RefCell<S>>,
inner: Rc<Inner>,
}
impl<S, B> Service for RedisSessionMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>
+ 'static,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.borrow_mut().poll_ready(cx)
}
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let mut srv = self.service.clone();
let inner = self.inner.clone();
Box::pin(async move {
let state = inner.load(&req).await?;
let value = if let Some((state, value)) = state {
Session::set_session(state.into_iter(), &mut req);
Some(value)
} else {
None
};
let mut res = srv.call(req).await?;
match Session::get_changes(&mut res) {
(SessionStatus::Unchanged, None) => Ok(res),
(SessionStatus::Unchanged, Some(state)) => {
if value.is_none() {
// implies the session is new
inner.update(res, state, value).await
} else {
Ok(res)
}
}
(SessionStatus::Changed, Some(state)) => {
inner.update(res, state, value).await
}
(SessionStatus::Purged, Some(_)) => {
if let Some(val) = value {
inner.clear_cache(val).await?;
match inner.remove_cookie(&mut res) {
Ok(_) => Ok(res),
Err(_err) => Err(error::ErrorInternalServerError(_err)),
}
} else {
Err(error::ErrorInternalServerError("unexpected"))
}
}
(SessionStatus::Renewed, Some(state)) => {
if let Some(val) = value {
inner.clear_cache(val).await?;
inner.update(res, state, None).await
} else {
inner.update(res, state, None).await
}
}
(_, None) => unreachable!(),
}
})
}
}
struct Inner {
key: Key,
cache_keygen: Box<dyn Fn(&str) -> String>,
ttl: String,
addr: Addr<RedisActor>,
name: String,
path: String,
domain: Option<String>,
secure: bool,
max_age: Option<Duration>,
same_site: Option<SameSite>,
}
impl Inner {
async fn load(
&self,
req: &ServiceRequest,
) -> Result<Option<(HashMap<String, String>, String)>, Error> {
if let Ok(cookies) = req.cookies() {
for cookie in cookies.iter() {
if cookie.name() == self.name {
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
if let Some(cookie) = jar.signed(&self.key).get(&self.name) {
let value = cookie.value().to_owned();
let cachekey = (self.cache_keygen)(&cookie.value());
return match self
.addr
.send(Command(resp_array!["GET", cachekey]))
.await
{
Err(e) => Err(Error::from(e)),
Ok(res) => match res {
Ok(val) => {
match val {
RespValue::Error(err) => {
return Err(
error::ErrorInternalServerError(err),
);
}
RespValue::SimpleString(s) => {
if let Ok(val) = serde_json::from_str(&s) {
return Ok(Some((val, value)));
}
}
RespValue::BulkString(s) => {
if let Ok(val) = serde_json::from_slice(&s) {
return Ok(Some((val, value)));
}
}
_ => (),
}
Ok(None)
}
Err(err) => Err(error::ErrorInternalServerError(err)),
},
};
} else {
return Ok(None);
}
}
}
}
Ok(None)
}
async fn update<B>(
&self,
mut res: ServiceResponse<B>,
state: impl Iterator<Item = (String, String)>,
value: Option<String>,
) -> Result<ServiceResponse<B>, Error> {
let (value, jar) = if let Some(value) = value {
(value, None)
} else {
let value: String = iter::repeat(())
.map(|()| OsRng.sample(Alphanumeric))
.take(32)
.collect();
// prepare session id cookie
let mut cookie = Cookie::new(self.name.clone(), value.clone());
cookie.set_path(self.path.clone());
cookie.set_secure(self.secure);
cookie.set_http_only(true);
if let Some(ref domain) = self.domain {
cookie.set_domain(domain.clone());
}
if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age);
}
if let Some(same_site) = self.same_site {
cookie.set_same_site(same_site);
}
// set cookie
let mut jar = CookieJar::new();
jar.signed(&self.key).add(cookie);
(value, Some(jar))
};
let cachekey = (self.cache_keygen)(&value);
let state: HashMap<_, _> = state.collect();
match serde_json::to_string(&state) {
Err(e) => Err(e.into()),
Ok(body) => {
match self
.addr
.send(Command(resp_array!["SET", cachekey, body, "EX", &self.ttl]))
.await
{
Err(e) => Err(Error::from(e)),
Ok(redis_result) => match redis_result {
Ok(_) => {
if let Some(jar) = jar {
for cookie in jar.delta() {
let val =
HeaderValue::from_str(&cookie.to_string())?;
res.headers_mut().append(header::SET_COOKIE, val);
}
}
Ok(res)
}
Err(err) => Err(error::ErrorInternalServerError(err)),
},
}
}
}
}
/// removes cache entry
async fn clear_cache(&self, key: String) -> Result<(), Error> {
let cachekey = (self.cache_keygen)(&key);
match self.addr.send(Command(resp_array!["DEL", cachekey])).await {
Err(e) => Err(Error::from(e)),
Ok(res) => {
match res {
// redis responds with number of deleted records
Ok(RespValue::Integer(x)) if x > 0 => Ok(()),
_ => Err(error::ErrorInternalServerError(
"failed to remove session from cache",
)),
}
}
}
}
/// invalidates session cookie
fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> {
let mut cookie = Cookie::named(self.name.clone());
cookie.set_value("");
cookie.set_max_age(Duration::seconds(0));
cookie.set_expires(time::now() - Duration::days(365));
let val = HeaderValue::from_str(&cookie.to_string())
.map_err(error::ErrorInternalServerError)?;
res.headers_mut().append(header::SET_COOKIE, val);
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use actix_session::Session;
use actix_web::{
middleware, test, web,
web::{get, post, resource},
App, HttpResponse, Result,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use time;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct IndexResponse {
user_id: Option<String>,
counter: i32,
}
async fn index(session: Session) -> Result<HttpResponse> {
let user_id: Option<String> = session.get::<String>("user_id").unwrap();
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.unwrap_or(0);
Ok(HttpResponse::Ok().json(IndexResponse { user_id, counter }))
}
async fn do_something(session: Session) -> Result<HttpResponse> {
let user_id: Option<String> = session.get::<String>("user_id").unwrap();
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.map_or(1, |inner| inner + 1);
session.set("counter", counter)?;
Ok(HttpResponse::Ok().json(IndexResponse { user_id, counter }))
}
#[derive(Deserialize)]
struct Identity {
user_id: String,
}
async fn login(
user_id: web::Json<Identity>,
session: Session,
) -> Result<HttpResponse> {
let id = user_id.into_inner().user_id;
session.set("user_id", &id)?;
session.renew();
let counter: i32 = session
.get::<i32>("counter")
.unwrap_or(Some(0))
.unwrap_or(0);
Ok(HttpResponse::Ok().json(IndexResponse {
user_id: Some(id),
counter,
}))
}
async fn logout(session: Session) -> Result<HttpResponse> {
let id: Option<String> = session.get("user_id")?;
if let Some(x) = id {
session.purge();
Ok(format!("Logged out: {}", x).into())
} else {
Ok("Could not log out anonymous user".into())
}
}
#[actix_rt::test]
async fn test_workflow() {
// Step 1: GET index
// - set-cookie actix-session will be in response (session cookie #1)
// - response should be: {"counter": 0, "user_id": None}
// Step 2: GET index, including session cookie #1 in request
// - set-cookie will *not* be in response
// - response should be: {"counter": 0, "user_id": None}
// Step 3: POST to do_something, including session cookie #1 in request
// - adds new session state in redis: {"counter": 1}
// - response should be: {"counter": 1, "user_id": None}
// Step 4: POST again to do_something, including session cookie #1 in request
// - updates session state in redis: {"counter": 2}
// - response should be: {"counter": 2, "user_id": None}
// Step 5: POST to login, including session cookie #1 in request
// - set-cookie actix-session will be in response (session cookie #2)
// - updates session state in redis: {"counter": 2, "user_id": "ferris"}
// Step 6: GET index, including session cookie #2 in request
// - response should be: {"counter": 2, "user_id": "ferris"}
// Step 7: POST again to do_something, including session cookie #2 in request
// - updates session state in redis: {"counter": 3, "user_id": "ferris"}
// - response should be: {"counter": 2, "user_id": None}
// Step 8: GET index, including session cookie #1 in request
// - set-cookie actix-session will be in response (session cookie #3)
// - response should be: {"counter": 0, "user_id": None}
// Step 9: POST to logout, including session cookie #2
// - set-cookie actix-session will be in response with session cookie #2
// invalidation logic
// Step 10: GET index, including session cookie #2 in request
// - set-cookie actix-session will be in response (session cookie #3)
// - response should be: {"counter": 0, "user_id": None}
let srv = test::start(|| {
App::new()
.wrap(
RedisSession::new("127.0.0.1:6379", &[0; 32])
.cookie_name("test-session"),
)
.wrap(middleware::Logger::default())
.service(resource("/").route(get().to(index)))
.service(resource("/do_something").route(post().to(do_something)))
.service(resource("/login").route(post().to(login)))
.service(resource("/logout").route(post().to(logout)))
});
// Step 1: GET index
// - set-cookie actix-session will be in response (session cookie #1)
// - response should be: {"counter": 0, "user_id": None}
let req_1a = srv.get("/").send();
let mut resp_1 = req_1a.await.unwrap();
let cookie_1 = resp_1
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session")
.unwrap();
let result_1 = resp_1.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_1,
IndexResponse {
user_id: None,
counter: 0
}
);
// Step 2: GET index, including session cookie #1 in request
// - set-cookie will *not* be in response
// - response should be: {"counter": 0, "user_id": None}
let req_2 = srv.get("/").cookie(cookie_1.clone()).send();
let resp_2 = req_2.await.unwrap();
let cookie_2 = resp_2
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session");
assert_eq!(cookie_2, None);
// Step 3: POST to do_something, including session cookie #1 in request
// - adds new session state in redis: {"counter": 1}
// - response should be: {"counter": 1, "user_id": None}
let req_3 = srv.post("/do_something").cookie(cookie_1.clone()).send();
let mut resp_3 = req_3.await.unwrap();
let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_3,
IndexResponse {
user_id: None,
counter: 1
}
);
// Step 4: POST again to do_something, including session cookie #1 in request
// - updates session state in redis: {"counter": 2}
// - response should be: {"counter": 2, "user_id": None}
let req_4 = srv.post("/do_something").cookie(cookie_1.clone()).send();
let mut resp_4 = req_4.await.unwrap();
let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_4,
IndexResponse {
user_id: None,
counter: 2
}
);
// Step 5: POST to login, including session cookie #1 in request
// - set-cookie actix-session will be in response (session cookie #2)
// - updates session state in redis: {"counter": 2, "user_id": "ferris"}
let req_5 = srv
.post("/login")
.cookie(cookie_1.clone())
.send_json(&json!({"user_id": "ferris"}));
let mut resp_5 = req_5.await.unwrap();
let cookie_2 = resp_5
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session")
.unwrap();
assert_eq!(
true,
cookie_1.value() != cookie_2.value()
);
let result_5 = resp_5.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_5,
IndexResponse {
user_id: Some("ferris".into()),
counter: 2
}
);
// Step 6: GET index, including session cookie #2 in request
// - response should be: {"counter": 2, "user_id": "ferris"}
let req_6 = srv.get("/").cookie(cookie_2.clone()).send();
let mut resp_6 = req_6.await.unwrap();
let result_6 = resp_6.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_6,
IndexResponse {
user_id: Some("ferris".into()),
counter: 2
}
);
// Step 7: POST again to do_something, including session cookie #2 in request
// - updates session state in redis: {"counter": 3, "user_id": "ferris"}
// - response should be: {"counter": 2, "user_id": None}
let req_7 = srv.post("/do_something").cookie(cookie_2.clone()).send();
let mut resp_7 = req_7.await.unwrap();
let result_7 = resp_7.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_7,
IndexResponse {
user_id: Some("ferris".into()),
counter: 3
}
);
// Step 8: GET index, including session cookie #1 in request
// - set-cookie actix-session will be in response (session cookie #3)
// - response should be: {"counter": 0, "user_id": None}
let req_8 = srv.get("/").cookie(cookie_1.clone()).send();
let mut resp_8 = req_8.await.unwrap();
let cookie_3 = resp_8
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session")
.unwrap();
let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_8,
IndexResponse {
user_id: None,
counter: 0
}
);
assert!(cookie_3.value() != cookie_2.value());
// Step 9: POST to logout, including session cookie #2
// - set-cookie actix-session will be in response with session cookie #2
// invalidation logic
let req_9 = srv.post("/logout").cookie(cookie_2.clone()).send();
let resp_9 = req_9.await.unwrap();
let cookie_4 = resp_9
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session")
.unwrap();
assert_ne!(time::now().tm_year, cookie_4.expires().map(|t| t.tm_year).unwrap());
// Step 10: GET index, including session cookie #2 in request
// - set-cookie actix-session will be in response (session cookie #3)
// - response should be: {"counter": 0, "user_id": None}
let req_10 = srv.get("/").cookie(cookie_2.clone()).send();
let mut resp_10 = req_10.await.unwrap();
let result_10 = resp_10.json::<IndexResponse>().await.unwrap();
assert_eq!(
result_10,
IndexResponse {
user_id: None,
counter: 0
}
);
let cookie_5 = resp_10
.cookies()
.unwrap()
.clone()
.into_iter()
.find(|c| c.name() == "test-session")
.unwrap();
assert!(cookie_5.value() != cookie_2.value());
}
}

View File

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

View File

@ -1,73 +1,240 @@
# Changes
## [Unreleased] - 2020-01-xx
## Unreleased
* Update the `time` dependency to 0.2.5
* [#1292](https://github.com/actix/actix-web/pull/1292) Long lasting auto-prolonged session
- Add `Session::contains_key` method.
- Add `Session::update[_or]()` methods.
- Update `redis` dependency to `0.29`.
## [0.3.0] - 2019-12-20
## 0.10.1
* Release
- Expose `storage::generate_session_key()` without needing to enable a crate feature.
## [0.3.0-alpha.4] - 2019-12-xx
## 0.10.0
* Allow access to sessions also from not mutable references to the request
- 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.3.0-alpha.3] - 2019-12-xx
## 0.9.0
* Add access to the session from RequestHead for use of session from guard methods
- Remove use of `async-trait` on `SessionStore` trait.
- Minimum supported Rust version (MSRV) is now 1.75.
* Migrate to `std::future`
## 0.8.0
* Migrate to `actix-web` 2.0
- Set secure attribute when adding a session removal cookie.
- Update `redis` dependency to `0.23`.
- Minimum supported Rust version (MSRV) is now 1.68.
## [0.2.0] - 2019-07-08
## 0.7.2
* 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).
- Set SameSite attribute when adding a session removal cookie. [#284]
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
## [0.1.1] - 2019-06-03
[#284]: https://github.com/actix/actix-extras/pull/284
* Fix optional cookie session support
## 0.7.1
## [0.1.0] - 2019-05-18
- Fix interaction between session state changes and renewal. [#265]
* Use actix-web 1.0.0-rc
[#265]: https://github.com/actix/actix-extras/pull/265
## [0.1.0-beta.4] - 2019-05-12
## 0.7.0
* Use actix-web 1.0.0-beta.4
- Added `TtlExtensionPolicy` enum to support different strategies for extending the TTL attached to the session state. `TtlExtensionPolicy::OnEveryRequest` now allows for long-lived sessions that do not expire if the user remains active. [#233]
- `SessionLength` is now called `SessionLifecycle`. [#233]
- `SessionLength::Predetermined` is now called `SessionLifecycle::PersistentSession`. [#233]
- The fields for Both `SessionLength` variants have been extracted into separate types (`PersistentSession` and `BrowserSession`). All fields are now private, manipulated via methods, to allow adding more configuration parameters in the future in a non-breaking fashion. [#233]
- `SessionLength::Predetermined::max_session_length` is now called `PersistentSession::session_ttl`. [#233]
- `SessionLength::BrowserSession::state_ttl` is now called `BrowserSession::session_state_ttl`. [#233]
- `SessionMiddlewareBuilder::max_session_length` is now called `SessionMiddlewareBuilder::session_lifecycle`. [#233]
- The `SessionStore` trait requires the implementation of a new method, `SessionStore::update_ttl`. [#233]
- All types used to configure `SessionMiddleware` have been moved to the `config` sub-module. [#233]
- Update `actix` dependency to `0.13`.
- Update `actix-redis` dependency to `0.12`.
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## [0.1.0-beta.2] - 2019-04-28
[#233]: https://github.com/actix/actix-extras/pull/233
* Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest
## 0.6.2
## [0.1.0-beta.1] - 2019-04-20
- Implement `SessionExt` for `GuardContext`. [#234]
- `RedisSessionStore` will prevent connection timeouts from causing user-visible errors. [#235]
- Do not leak internal implementation details to callers when errors occur. [#236]
* Update actix-web to beta.1
[#234]: https://github.com/actix/actix-extras/pull/234
[#236]: https://github.com/actix/actix-extras/pull/236
[#235]: https://github.com/actix/actix-extras/pull/235
* `CookieSession::max_age()` accepts value in seconds
## 0.6.1
## [0.1.0-alpha.6] - 2019-04-14
- No significant changes since `0.6.0`.
* Update actix-web alpha.6
## 0.6.0
## [0.1.0-alpha.4] - 2019-04-08
### Added
* Update actix-web
- `SessionMiddleware`, a middleware to provide support for saving/updating/deleting session state against a pluggable storage backend (see `SessionStore` trait). [#212]
- `CookieSessionStore`, a cookie-based backend to store session state. [#212]
- `RedisActorSessionStore`, a Redis-based backend to store session state powered by `actix-redis`. [#212]
- `RedisSessionStore`, a Redis-based backend to store session state powered by `redis-rs`. [#212]
- Add TLS support for Redis via `RedisSessionStore`. [#212]
- Implement `SessionExt` for `ServiceResponse`. [#212]
## [0.1.0-alpha.3] - 2019-04-02
### Changed
* Update actix-web
- Rename `UserSession` to `SessionExt`. [#212]
## [0.1.0-alpha.2] - 2019-03-29
### Removed
* Update actix-web
- `CookieSession`; replaced with `CookieSessionStore`, a storage backend for `SessionMiddleware`. [#212]
- `Session::set_session`; use `Session::insert` to modify the session state. [#212]
* Use new feature name for secure cookies
[#212]: https://github.com/actix/actix-extras/pull/212
## [0.1.0-alpha.1] - 2019-03-28
## 0.5.0
* Initial impl
- Update `actix-web` dependency to `4`.
## 0.5.0-beta.8
- Update `actix-web` dependency to `4.0.0-rc.1`.
## 0.5.0-beta.7
- 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.5.0-beta.6
- Update `actix-web` dependency to `4.0.0.beta-15`. [#216]
[#216]: https://github.com/actix/actix-extras/pull/216
## 0.5.0-beta.5
- Update `actix-web` dependency to `4.0.0.beta-14`. [#209]
- Remove `UserSession` implementation for `RequestHead`. [#209]
- A session will be created in the storage backend if and only if there is some data inside the session state. This reduces the performance impact of `SessionMiddleware` on routes that do not leverage sessions. [#207]
[#207]: https://github.com/actix/actix-extras/pull/207
[#209]: https://github.com/actix/actix-extras/pull/209
## 0.5.0-beta.4
- No significant changes since `0.5.0-beta.3`.
## 0.5.0-beta.3
- Impl `Clone` for `CookieSession`. [#201]
- Update `actix-web` dependency to v4.0.0-beta.10. [#203]
- Minimum supported Rust version (MSRV) is now 1.52.
[#201]: https://github.com/actix/actix-extras/pull/201
[#203]: https://github.com/actix/actix-extras/pull/203
## 0.5.0-beta.2
- No notable changes.
## 0.5.0-beta.1
- Add `Session::entries`. [#170]
- Rename `Session::{set => insert}` to match standard hash map naming. [#170]
- Return values from `Session::remove`. [#170]
- Add `Session::remove_as` deserializing variation. [#170]
- Simplify `Session::get_changes` now always returning iterator even when empty. [#170]
- Swap order of arguments on `Session::set_session`. [#170]
- Update `actix-web` dependency to 4.0.0 beta.
- Minimum supported Rust version (MSRV) is now 1.46.0.
[#170]: https://github.com/actix/actix-extras/pull/170
## 0.4.1
- `Session::set_session` takes a `IntoIterator` instead of `Iterator`. [#105]
- Fix calls to `session.purge()` from paths other than the one specified in the cookie. [#129]
[#105]: https://github.com/actix/actix-extras/pull/105
[#129]: https://github.com/actix/actix-extras/pull/129
## 0.4.0
- Update `actix-web` dependency to 3.0.0.
- Minimum supported Rust version (MSRV) is now 1.42.0.
## 0.4.0-alpha.1
- Update the `time` dependency to 0.2.7
- Update the `actix-web` dependency to 3.0.0-alpha.1
- Long lasting auto-prolonged session [#1292]
- Minimize `futures` dependency
[#1292]: https://github.com/actix/actix-web/pull/1292
## 0.3.0 - 2019-12-20
- Release
## 0.3.0-alpha.4 - 2019-12-xx
- Allow access to sessions also from not mutable references to the request
## 0.3.0-alpha.3 - 2019-12-xx
- Add access to the session from RequestHead for use of session from guard methods
- Migrate to `std::future`
- Migrate to `actix-web` 2.0
## 0.2.0 - 2019-07-08
- Enhanced `actix-session` to facilitate state changes. Use `Session.renew()` at successful login to cycle a session (new key/cookie but keeps state). Use `Session.purge()` at logout to invalid a session cookie (and remove from redis cache, if applicable).
## 0.1.1 - 2019-06-03
- Fix optional cookie session support
## 0.1.0 - 2019-05-18
- Use actix-web 1.0.0-rc
## 0.1.0-beta.4 - 2019-05-12
- Use actix-web 1.0.0-beta.4
## 0.1.0-beta.2 - 2019-04-28
- Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest
## 0.1.0-beta.1 - 2019-04-20
- Update actix-web to beta.1
- `CookieSession::max_age()` accepts value in seconds
## 0.1.0-alpha.6 - 2019-04-14
- Update actix-web alpha.6
## 0.1.0-alpha.4 - 2019-04-08
- Update actix-web
## 0.1.0-alpha.3 - 2019-04-02
- Update actix-web
## 0.1.0-alpha.2 - 2019-03-29
- Update actix-web
- Use new feature name for secure cookies
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl

View File

@ -1,35 +1,60 @@
[package]
name = "actix-session"
version = "0.3.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Session for actix-web framework."
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-session/"
license = "MIT/Apache-2.0"
edition = "2018"
version = "0.10.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Luca Palmieri <rust@lpalmieri.com>",
]
description = "Session management for Actix Web"
keywords = ["http", "web", "framework", "async", "session"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
[lib]
name = "actix_session"
path = "src/lib.rs"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[features]
default = ["cookie-session"]
# sessions feature, session require "ring" crate and c compiler
cookie-session = ["actix-web/secure-cookies"]
default = []
cookie-session = []
redis-session = ["dep:redis"]
redis-session-native-tls = ["redis-session", "redis/tokio-native-tls-comp"]
redis-session-rustls = ["redis-session", "redis/tokio-rustls-comp"]
redis-pool = ["dep:deadpool-redis"]
[dependencies]
actix-web = "2.0.0"
actix-service = "1.0.1"
bytes = "0.5.3"
derive_more = "0.99.2"
futures = "0.3.1"
serde = "1.0"
serde_json = "1.0"
time = { version = "0.2.5", default-features = false, features = ["std"] }
actix-service = "2"
actix-utils = "3"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies"] }
anyhow = "1"
derive_more = { version = "2", features = ["display", "error", "from"] }
rand = "0.9"
serde = { version = "1" }
serde_json = { version = "1" }
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# redis-session
redis = { version = "0.29", default-features = false, features = ["tokio-comp", "connection-manager"], optional = true }
deadpool-redis = { version = "0.20", optional = true }
[dev-dependencies]
actix-rt = "1.0.0"
actix-session = { path = ".", features = ["cookie-session", "redis-session"] }
actix-test = "0.1"
actix-web = { version = "4", default-features = false, features = ["cookies", "secure-cookies", "macros"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing = "0.1.30"
[lints]
workspace = true
[[example]]
name = "basic"
required-features = ["redis-session"]
[[example]]
name = "authentication"
required-features = ["redis-session"]

View File

@ -1,19 +1,125 @@
# actix-session
[![crates.io](https://img.shields.io/crates/v/actix-session)](https://crates.io/crates/actix-session)
[![Documentation](https://docs.rs/actix-session/badge.svg)](https://docs.rs/actix-session)
[![Dependency Status](https://deps.rs/crate/actix-session/0.3.0/status.svg)](https://deps.rs/crate/actix-session/0.3.0)
[![Build Status](https://travis-ci.org/actix/actix-session.svg?branch=master)](https://travis-ci.org/actix/actix-session)
[![codecov](https://codecov.io/gh/actix/actix-session/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-session)
> Session management for Actix Web.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-session?label=latest)](https://crates.io/crates/actix-session)
[![Documentation](https://docs.rs/actix-session/badge.svg?version=0.10.1)](https://docs.rs/actix-session/0.10.1)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-session)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Dependency Status](https://deps.rs/crate/actix-session/0.10.1/status.svg)](https://deps.rs/crate/actix-session/0.10.1)
> Session for actix-web framework.
<!-- prettier-ignore-end -->
## Documentation & community resources
<!-- cargo-rdme start -->
* [User Guide](https://actix.rs/docs/)
* [API Documentation](https://docs.rs/actix-session/)
* [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-session](https://crates.io/crates/actix-session)
* Minimum supported Rust version: 1.34 or later
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 -->

View File

@ -0,0 +1,111 @@
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware};
use actix_web::{
cookie::{Key, SameSite},
error::InternalError,
middleware, web, App, Error, HttpResponse, HttpServer, Responder,
};
use serde::{Deserialize, Serialize};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
#[derive(Deserialize)]
struct Credentials {
username: String,
password: String,
}
#[derive(Serialize)]
struct User {
id: i64,
username: String,
password: String,
}
impl User {
fn authenticate(credentials: Credentials) -> Result<Self, HttpResponse> {
// to do: figure out why I keep getting hacked /s
if &credentials.password != "hunter2" {
return Err(HttpResponse::Unauthorized().json("Unauthorized"));
}
Ok(User {
id: 42,
username: credentials.username,
password: credentials.password,
})
}
}
pub fn validate_session(session: &Session) -> Result<i64, HttpResponse> {
let user_id: Option<i64> = session.get("user_id").unwrap_or(None);
match user_id {
Some(id) => {
// keep the user's session alive
session.renew();
Ok(id)
}
None => Err(HttpResponse::Unauthorized().json("Unauthorized")),
}
}
async fn login(
credentials: web::Json<Credentials>,
session: Session,
) -> Result<impl Responder, Error> {
let credentials = credentials.into_inner();
match User::authenticate(credentials) {
Ok(user) => session.insert("user_id", user.id).unwrap(),
Err(err) => return Err(InternalError::from_response("", err).into()),
};
Ok("Welcome!")
}
/// some protected resource
async fn secret(session: Session) -> Result<impl Responder, Error> {
// only allow access to this resource if the user has an active session
validate_session(&session).map_err(|err| InternalError::from_response("", err))?;
Ok("secret revealed")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
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.
let signing_key = Key::generate();
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 || {
App::new()
// enable logger
.wrap(middleware::Logger::default())
// cookie session middleware
.wrap(
SessionMiddleware::builder(storage.clone(), signing_key.clone())
// allow the cookie to be accessed from javascript
.cookie_http_only(false)
// allow the cookie only from the current domain
.cookie_same_site(SameSite::Strict)
.build(),
)
.route("/login", web::post().to(login))
.route("/secret", web::get().to(secret))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@ -0,0 +1,51 @@
use actix_session::{storage::RedisSessionStore, Session, SessionMiddleware};
use actix_web::{cookie::Key, middleware, web, App, Error, HttpRequest, HttpServer, Responder};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
/// simple handler
async fn index(req: HttpRequest, session: Session) -> Result<impl Responder, Error> {
println!("{req:?}");
// session
if let Some(count) = session.get::<i32>("counter")? {
println!("SESSION value: {count}");
session.insert("counter", count + 1)?;
} else {
session.insert("counter", 1)?;
}
Ok("Welcome!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
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.
let signing_key = Key::generate();
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 || {
App::new()
// enable logger
.wrap(middleware::Logger::default())
// cookie session middleware
.wrap(SessionMiddleware::new(storage.clone(), signing_key.clone()))
// register simple route, handle all methods
.service(web::resource("/").to(index))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

397
actix-session/src/config.rs Normal file
View File

@ -0,0 +1,397 @@
//! Configuration options to tune the behaviour of [`SessionMiddleware`].
use actix_web::cookie::{time::Duration, Key, SameSite};
use derive_more::derive::From;
use crate::{storage::SessionStore, SessionMiddleware};
/// Determines what type of session cookie should be used and how its lifecycle should be managed.
///
/// Used by [`SessionMiddlewareBuilder::session_lifecycle`].
#[derive(Debug, Clone, From)]
#[non_exhaustive]
pub enum SessionLifecycle {
/// The session cookie will expire when the current browser session ends.
///
/// When does a browser session end? It depends on the browser! Chrome, for example, will often
/// continue running in the background when the browser is closed—session cookies are not
/// deleted and they will still be available when the browser is opened again.
/// Check the documentation of the browsers you are targeting for up-to-date information.
BrowserSession(BrowserSession),
/// The session cookie will be a [persistent cookie].
///
/// Persistent cookies have a pre-determined lifetime, specified via the `Max-Age` or `Expires`
/// attribute. They do not disappear when the current browser session ends.
///
/// [persistent cookie]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
PersistentSession(PersistentSession),
}
/// A [session lifecycle](SessionLifecycle) strategy where the session cookie expires when the
/// browser's current session ends.
///
/// When does a browser session end? It depends on the browser. Chrome, for example, will often
/// continue running in the background when the browser is closed—session cookies are not deleted
/// and they will still be available when the browser is opened again. Check the documentation of
/// the browsers you are targeting for up-to-date information.
///
/// Due to its `Into<SessionLifecycle>` implementation, a `BrowserSession` can be passed directly
/// to [`SessionMiddlewareBuilder::session_lifecycle()`].
#[derive(Debug, Clone)]
pub struct BrowserSession {
state_ttl: Duration,
state_ttl_extension_policy: TtlExtensionPolicy,
}
impl BrowserSession {
/// Sets a time-to-live (TTL) when storing the session state in the storage backend.
///
/// We do not want to store session states indefinitely, otherwise we will inevitably run out of
/// storage by holding on to the state of countless abandoned or expired sessions!
///
/// We are dealing with the lifecycle of two uncorrelated object here: the session cookie
/// and the session state. It is not a big issue if the session state outlives the cookie—
/// we are wasting some space in the backend storage, but it will be cleaned up eventually.
/// What happens, instead, if the cookie outlives the session state? A new session starts—
/// e.g. if sessions are being used for authentication, the user is de-facto logged out.
///
/// It is not possible to predict with certainty how long a browser session is going to
/// last—you need to provide a reasonable upper bound. You do so via `state_ttl`—it dictates
/// what TTL should be used for session state when the lifecycle of the session cookie is
/// tied to the browser session length. [`SessionMiddleware`] will default to 1 day if
/// `state_ttl` is left unspecified.
///
/// You can mitigate the risk of the session cookie outliving the session state by
/// specifying a more aggressive state TTL extension policy - check out
/// [`BrowserSession::state_ttl_extension_policy`] for more details.
pub fn state_ttl(mut self, ttl: Duration) -> Self {
self.state_ttl = ttl;
self
}
/// Determine under what circumstances the TTL of your session state should be extended.
///
/// Defaults to [`TtlExtensionPolicy::OnStateChanges`] if left unspecified.
///
/// See [`TtlExtensionPolicy`] for more details.
pub fn state_ttl_extension_policy(mut self, ttl_extension_policy: TtlExtensionPolicy) -> Self {
self.state_ttl_extension_policy = ttl_extension_policy;
self
}
}
impl Default for BrowserSession {
fn default() -> Self {
Self {
state_ttl: default_ttl(),
state_ttl_extension_policy: default_ttl_extension_policy(),
}
}
}
/// A [session lifecycle](SessionLifecycle) strategy where the session cookie will be [persistent].
///
/// Persistent cookies have a pre-determined expiration, specified via the `Max-Age` or `Expires`
/// attribute. They do not disappear when the current browser session ends.
///
/// Due to its `Into<SessionLifecycle>` implementation, a `PersistentSession` can be passed directly
/// to [`SessionMiddlewareBuilder::session_lifecycle()`].
///
/// # Examples
/// ```
/// use actix_web::cookie::time::Duration;
/// use actix_session::SessionMiddleware;
/// use actix_session::config::{PersistentSession, TtlExtensionPolicy};
///
/// const SECS_IN_WEEK: i64 = 60 * 60 * 24 * 7;
///
/// // a session lifecycle with a time-to-live (expiry) of 1 week and default extension policy
/// PersistentSession::default().session_ttl(Duration::seconds(SECS_IN_WEEK));
///
/// // a session lifecycle with the default time-to-live (expiry) and a custom extension policy
/// PersistentSession::default()
/// // this policy causes the session state's TTL to be refreshed on every request
/// .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest);
/// ```
///
/// [persistent]: https://www.whitehatsec.com/glossary/content/persistent-session-cookie
#[derive(Debug, Clone)]
pub struct PersistentSession {
session_ttl: Duration,
ttl_extension_policy: TtlExtensionPolicy,
}
impl PersistentSession {
/// Specifies how long the session cookie should live.
///
/// The session TTL is also used as the TTL for the session state in the storage backend.
///
/// Defaults to 1 day.
///
/// 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.
#[doc(alias = "max_age", alias = "max age", alias = "expires")]
pub fn session_ttl(mut self, session_ttl: Duration) -> Self {
self.session_ttl = session_ttl;
self
}
/// Determines under what circumstances the TTL of your session should be extended.
/// See [`TtlExtensionPolicy`] for more details.
///
/// Defaults to [`TtlExtensionPolicy::OnStateChanges`].
pub fn session_ttl_extension_policy(
mut self,
ttl_extension_policy: TtlExtensionPolicy,
) -> Self {
self.ttl_extension_policy = ttl_extension_policy;
self
}
}
impl Default for PersistentSession {
fn default() -> Self {
Self {
session_ttl: default_ttl(),
ttl_extension_policy: default_ttl_extension_policy(),
}
}
}
/// Configuration for which events should trigger an extension of the time-to-live for your session.
///
/// If you are using a [`BrowserSession`], `TtlExtensionPolicy` controls how often the TTL of the
/// session state should be refreshed. The browser is in control of the lifecycle of the session
/// cookie.
///
/// If you are using a [`PersistentSession`], `TtlExtensionPolicy` controls both the expiration of
/// the session cookie and the TTL of the session state on the storage backend.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum TtlExtensionPolicy {
/// The TTL is refreshed every time the server receives a request associated with a session.
///
/// # Performance impact
/// Refreshing the TTL on every request is not free. It implies a refresh of the TTL on the
/// session state. This translates into a request over the network if you are using a remote
/// system as storage backend (e.g. Redis). This impacts both the total load on your storage
/// backend (i.e. number of queries it has to handle) and the latency of the requests served by
/// your server.
OnEveryRequest,
/// The TTL is refreshed every time the session state changes or the session key is renewed.
OnStateChanges,
}
/// Determines how to secure the content of the session cookie.
///
/// Used by [`SessionMiddlewareBuilder::cookie_content_security`].
#[derive(Debug, Clone, Copy)]
pub enum CookieContentSecurity {
/// The cookie content is encrypted when using `CookieContentSecurity::Private`.
///
/// Encryption guarantees confidentiality and integrity: the client cannot tamper with the
/// cookie content nor decode it, as long as the encryption key remains confidential.
Private,
/// The cookie content is signed when using `CookieContentSecurity::Signed`.
///
/// Signing guarantees integrity, but it doesn't ensure confidentiality: the client cannot
/// tamper with the cookie content, but they can read it.
Signed,
}
pub(crate) const fn default_ttl() -> Duration {
Duration::days(1)
}
pub(crate) const fn default_ttl_extension_policy() -> TtlExtensionPolicy {
TtlExtensionPolicy::OnStateChanges
}
/// A fluent, customized [`SessionMiddleware`] builder.
#[must_use]
pub struct SessionMiddlewareBuilder<Store: SessionStore> {
storage_backend: Store,
configuration: Configuration,
}
impl<Store: SessionStore> SessionMiddlewareBuilder<Store> {
pub(crate) fn new(store: Store, configuration: Configuration) -> Self {
Self {
storage_backend: store,
configuration,
}
}
/// Set the name of the cookie used to store the session ID.
///
/// Defaults to `id`.
pub fn cookie_name(mut self, name: String) -> Self {
self.configuration.cookie.name = name;
self
}
/// Set the `Secure` attribute for the cookie used to store the session ID.
///
/// If the cookie is set as secure, it will only be transmitted when the connection is secure
/// (using `https`).
///
/// Default is `true`.
pub fn cookie_secure(mut self, secure: bool) -> Self {
self.configuration.cookie.secure = secure;
self
}
/// Determines what type of session cookie should be used and how its lifecycle should be managed.
/// Check out [`SessionLifecycle`]'s documentation for more details on the available options.
///
/// Default is [`SessionLifecycle::BrowserSession`].
///
/// # Examples
/// ```
/// use actix_web::cookie::{Key, time::Duration};
/// use actix_session::{SessionMiddleware, config::PersistentSession};
/// use actix_session::storage::CookieSessionStore;
///
/// const SECS_IN_WEEK: i64 = 60 * 60 * 24 * 7;
///
/// // creates a session middleware with a time-to-live (expiry) of 1 week
/// SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64]))
/// .session_lifecycle(
/// PersistentSession::default().session_ttl(Duration::seconds(SECS_IN_WEEK))
/// )
/// .build();
/// ```
pub fn session_lifecycle<S: Into<SessionLifecycle>>(mut self, session_lifecycle: S) -> Self {
match session_lifecycle.into() {
SessionLifecycle::BrowserSession(BrowserSession {
state_ttl,
state_ttl_extension_policy,
}) => {
self.configuration.cookie.max_age = None;
self.configuration.session.state_ttl = state_ttl;
self.configuration.ttl_extension_policy = state_ttl_extension_policy;
}
SessionLifecycle::PersistentSession(PersistentSession {
session_ttl,
ttl_extension_policy,
}) => {
self.configuration.cookie.max_age = Some(session_ttl);
self.configuration.session.state_ttl = session_ttl;
self.configuration.ttl_extension_policy = ttl_extension_policy;
}
}
self
}
/// Set the `SameSite` attribute for the cookie used to store the session ID.
///
/// By default, the attribute is set to `Lax`.
pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
self.configuration.cookie.same_site = same_site;
self
}
/// Set the `Path` attribute for the cookie used to store the session ID.
///
/// By default, the attribute is set to `/`.
pub fn cookie_path(mut self, path: String) -> Self {
self.configuration.cookie.path = path;
self
}
/// Set the `Domain` attribute for the cookie used to store the session ID.
///
/// Use `None` to leave the attribute unspecified. If unspecified, the attribute defaults
/// to the same host that set the cookie, excluding subdomains.
///
/// By default, the attribute is left unspecified.
pub fn cookie_domain(mut self, domain: Option<String>) -> Self {
self.configuration.cookie.domain = domain;
self
}
/// Choose how the session cookie content should be secured.
///
/// - [`CookieContentSecurity::Private`] selects encrypted cookie content.
/// - [`CookieContentSecurity::Signed`] selects signed cookie content.
///
/// # Default
/// By default, the cookie content is encrypted. Encrypted was chosen instead of signed as
/// default because it reduces the chances of sensitive information being exposed in the session
/// key by accident, regardless of [`SessionStore`] implementation you chose to use.
///
/// For example, if you are using cookie-based storage, you definitely want the cookie content
/// to be encrypted—the whole session state is embedded in the cookie! If you are using
/// Redis-based storage, signed is more than enough - the cookie content is just a unique
/// tamper-proof session key.
pub fn cookie_content_security(mut self, content_security: CookieContentSecurity) -> Self {
self.configuration.cookie.content_security = content_security;
self
}
/// Set the `HttpOnly` attribute for the cookie used to store the session ID.
///
/// If the cookie is set as `HttpOnly`, it will not be visible to any JavaScript snippets
/// running in the browser.
///
/// Default is `true`.
pub fn cookie_http_only(mut self, http_only: bool) -> Self {
self.configuration.cookie.http_only = http_only;
self
}
/// Finalise the builder and return a [`SessionMiddleware`] instance.
#[must_use]
pub fn build(self) -> SessionMiddleware<Store> {
SessionMiddleware::from_parts(self.storage_backend, self.configuration)
}
}
#[derive(Clone)]
pub(crate) struct Configuration {
pub(crate) cookie: CookieConfiguration,
pub(crate) session: SessionConfiguration,
pub(crate) ttl_extension_policy: TtlExtensionPolicy,
}
#[derive(Clone)]
pub(crate) struct SessionConfiguration {
pub(crate) state_ttl: Duration,
}
#[derive(Clone)]
pub(crate) struct CookieConfiguration {
pub(crate) secure: bool,
pub(crate) http_only: bool,
pub(crate) name: String,
pub(crate) same_site: SameSite,
pub(crate) path: String,
pub(crate) domain: Option<String>,
pub(crate) max_age: Option<Duration>,
pub(crate) content_security: CookieContentSecurity,
pub(crate) key: Key,
}
pub(crate) fn default_configuration(key: Key) -> Configuration {
Configuration {
cookie: CookieConfiguration {
secure: true,
http_only: true,
name: "id".into(),
same_site: SameSite::Lax,
path: "/".into(),
domain: None,
max_age: None,
content_security: CookieContentSecurity::Private,
key,
},
session: SessionConfiguration {
state_ttl: default_ttl(),
},
ttl_extension_policy: default_ttl_extension_policy(),
}
}

View File

@ -1,545 +0,0 @@
//! Cookie session.
//!
//! [**CookieSession**](struct.CookieSession.html)
//! uses cookies as session storage. `CookieSession` creates sessions
//! which are limited to storing fewer than 4000 bytes of data, as the payload
//! must fit into a single cookie. An internal server error is generated if a
//! session contains more than 4000 bytes.
//!
//! A cookie may have a security policy of *signed* or *private*. Each has
//! a respective `CookieSession` constructor.
//!
//! A *signed* cookie may be viewed but not modified by the client. A *private*
//! cookie may neither be viewed nor modified by the client.
//!
//! The constructors take a key as an argument. This is the private key
//! for cookie session - when this value is changed, all session data is lost.
use std::collections::HashMap;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::cookie::{Cookie, CookieJar, Key, SameSite};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::{header::SET_COOKIE, HeaderValue};
use actix_web::{Error, HttpMessage, ResponseError};
use derive_more::{Display, From};
use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
use serde_json::error::Error as JsonError;
use time::{Duration, OffsetDateTime};
use crate::{Session, SessionStatus};
/// Errors that can occur during handling cookie session
#[derive(Debug, From, Display)]
pub enum CookieSessionError {
/// Size of the serialized session is greater than 4000 bytes.
#[display(fmt = "Size of the serialized session is greater than 4000 bytes.")]
Overflow,
/// Fail to serialize session.
#[display(fmt = "Fail to serialize session")]
Serialize(JsonError),
}
impl ResponseError for CookieSessionError {}
enum CookieSecurity {
Signed,
Private,
}
struct CookieSessionInner {
key: Key,
security: CookieSecurity,
name: String,
path: String,
domain: Option<String>,
secure: bool,
http_only: bool,
max_age: Option<Duration>,
expires_in: Option<Duration>,
same_site: Option<SameSite>,
}
impl CookieSessionInner {
fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner {
CookieSessionInner {
security,
key: Key::from_master(key),
name: "actix-session".to_owned(),
path: "/".to_owned(),
domain: None,
secure: true,
http_only: true,
max_age: None,
expires_in: None,
same_site: None,
}
}
fn set_cookie<B>(
&self,
res: &mut ServiceResponse<B>,
state: impl Iterator<Item = (String, String)>,
) -> Result<(), Error> {
let state: HashMap<String, String> = state.collect();
let value =
serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?;
if value.len() > 4064 {
return Err(CookieSessionError::Overflow.into());
}
let mut cookie = Cookie::new(self.name.clone(), value);
cookie.set_path(self.path.clone());
cookie.set_secure(self.secure);
cookie.set_http_only(self.http_only);
if let Some(ref domain) = self.domain {
cookie.set_domain(domain.clone());
}
if let Some(expires_in) = self.expires_in {
cookie.set_expires(OffsetDateTime::now() + expires_in);
}
if let Some(max_age) = self.max_age {
cookie.set_max_age(max_age);
}
if let Some(same_site) = self.same_site {
cookie.set_same_site(same_site);
}
let mut jar = CookieJar::new();
match self.security {
CookieSecurity::Signed => jar.signed(&self.key).add(cookie),
CookieSecurity::Private => jar.private(&self.key).add(cookie),
}
for cookie in jar.delta() {
let val = HeaderValue::from_str(&cookie.encoded().to_string())?;
res.headers_mut().append(SET_COOKIE, val);
}
Ok(())
}
/// invalidates session cookie
fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> {
let mut cookie = Cookie::named(self.name.clone());
cookie.set_value("");
cookie.set_max_age(Duration::zero());
cookie.set_expires(OffsetDateTime::now() - Duration::days(365));
let val = HeaderValue::from_str(&cookie.to_string())?;
res.headers_mut().append(SET_COOKIE, val);
Ok(())
}
fn load(&self, req: &ServiceRequest) -> (bool, HashMap<String, String>) {
if let Ok(cookies) = req.cookies() {
for cookie in cookies.iter() {
if cookie.name() == self.name {
let mut jar = CookieJar::new();
jar.add_original(cookie.clone());
let cookie_opt = match self.security {
CookieSecurity::Signed => jar.signed(&self.key).get(&self.name),
CookieSecurity::Private => {
jar.private(&self.key).get(&self.name)
}
};
if let Some(cookie) = cookie_opt {
if let Ok(val) = serde_json::from_str(cookie.value()) {
return (false, val);
}
}
}
}
}
(true, HashMap::new())
}
}
/// Use cookies for session storage.
///
/// `CookieSession` creates sessions which are limited to storing
/// fewer than 4000 bytes of data (as the payload must fit into a single
/// cookie). An Internal Server Error is generated if the session contains more
/// than 4000 bytes.
///
/// A cookie may have a security policy of *signed* or *private*. Each has a
/// respective `CookieSessionBackend` constructor.
///
/// A *signed* cookie is stored on the client as plaintext alongside
/// a signature such that the cookie may be viewed but not modified by the
/// client.
///
/// A *private* cookie is stored on the client as encrypted text
/// such that it may neither be viewed nor modified by the client.
///
/// The constructors take a key as an argument.
/// This is the private key for cookie session - when this value is changed,
/// all session data is lost. The constructors will panic if the key is less
/// than 32 bytes in length.
///
/// The backend relies on `cookie` crate to create and read cookies.
/// By default all cookies are percent encoded, but certain symbols may
/// cause troubles when reading cookie, if they are not properly percent encoded.
///
/// # Example
///
/// ```rust
/// use actix_session::CookieSession;
/// use actix_web::{web, App, HttpResponse, HttpServer};
///
/// fn main() {
/// let app = App::new().wrap(
/// CookieSession::signed(&[0; 32])
/// .domain("www.rust-lang.org")
/// .name("actix_session")
/// .path("/")
/// .secure(true))
/// .service(web::resource("/").to(|| HttpResponse::Ok()));
/// }
/// ```
pub struct CookieSession(Rc<CookieSessionInner>);
impl CookieSession {
/// Construct new *signed* `CookieSessionBackend` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn signed(key: &[u8]) -> CookieSession {
CookieSession(Rc::new(CookieSessionInner::new(
key,
CookieSecurity::Signed,
)))
}
/// Construct new *private* `CookieSessionBackend` instance.
///
/// Panics if key length is less than 32 bytes.
pub fn private(key: &[u8]) -> CookieSession {
CookieSession(Rc::new(CookieSessionInner::new(
key,
CookieSecurity::Private,
)))
}
/// Sets the `path` field in the session cookie being built.
pub fn path<S: Into<String>>(mut self, value: S) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().path = value.into();
self
}
/// Sets the `name` field in the session cookie being built.
pub fn name<S: Into<String>>(mut self, value: S) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().name = value.into();
self
}
/// Sets the `domain` field in the session cookie being built.
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into());
self
}
/// Sets the `secure` field in the session cookie being built.
///
/// If the `secure` field is set, a cookie will only be transmitted when the
/// connection is secure - i.e. `https`
pub fn secure(mut self, value: bool) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().secure = value;
self
}
/// Sets the `http_only` field in the session cookie being built.
pub fn http_only(mut self, value: bool) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().http_only = value;
self
}
/// Sets the `same_site` field in the session cookie being built.
pub fn same_site(mut self, value: SameSite) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().same_site = Some(value);
self
}
/// Sets the `max-age` field in the session cookie being built.
pub fn max_age(self, seconds: i64) -> CookieSession {
self.max_age_time(Duration::seconds(seconds))
}
/// Sets the `max-age` field in the session cookie being built.
pub fn max_age_time(mut self, value: time::Duration) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self
}
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in(self, seconds: i64) -> CookieSession {
self.expires_in_time(Duration::seconds(seconds))
}
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in_time(mut self, value: Duration) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().expires_in = Some(value);
self
}
}
impl<S, B: 'static> Transform<S> for CookieSession
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = CookieSessionMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(CookieSessionMiddleware {
service,
inner: self.0.clone(),
})
}
}
/// Cookie session middleware
pub struct CookieSessionMiddleware<S> {
service: S,
inner: Rc<CookieSessionInner>,
}
impl<S, B: 'static> Service for CookieSessionMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
/// On first request, a new session cookie is returned in response, regardless
/// of whether any session state is set. With subsequent requests, if the
/// session state changes, then set-cookie is returned in response. As
/// a user logs out, call session.purge() to set SessionStatus accordingly
/// and this will trigger removal of the session cookie in the response.
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let inner = self.inner.clone();
let (is_new, state) = self.inner.load(&req);
let prolong_expiration = self.inner.expires_in.is_some();
Session::set_session(state.into_iter(), &mut req);
let fut = self.service.call(req);
async move {
fut.await.map(|mut res| {
match Session::get_changes(&mut res) {
(SessionStatus::Changed, Some(state))
| (SessionStatus::Renewed, Some(state)) => {
res.checked_expr(|res| inner.set_cookie(res, state))
}
(SessionStatus::Unchanged, Some(state)) if prolong_expiration => {
res.checked_expr(|res| inner.set_cookie(res, state))
}
(SessionStatus::Unchanged, _) =>
// set a new session cookie upon first request (new client)
{
if is_new {
let state: HashMap<String, String> = HashMap::new();
res.checked_expr(|res| {
inner.set_cookie(res, state.into_iter())
})
} else {
res
}
}
(SessionStatus::Purged, _) => {
let _ = inner.remove_cookie(&mut res);
res
}
_ => res,
}
})
}
.boxed_local()
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, web, App};
use bytes::Bytes;
#[actix_rt::test]
async fn cookie_session() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn private_cookie() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::private(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn cookie_session_extractor() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn basics() {
let mut app = test::init_service(
App::new()
.wrap(
CookieSession::signed(&[0; 32])
.path("/test/")
.name("actix-test")
.domain("localhost")
.http_only(true)
.same_site(SameSite::Lax)
.max_age(100),
)
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
}))
.service(web::resource("/test/").to(|ses: Session| {
async move {
let val: usize = ses.get("counter").unwrap().unwrap();
format!("counter: {}", val)
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
let cookie = response
.response()
.cookies()
.find(|c| c.name() == "actix-test")
.unwrap()
.clone();
assert_eq!(cookie.path().unwrap(), "/test/");
let request = test::TestRequest::with_uri("/test/")
.cookie(cookie)
.to_request();
let body = test::read_response(&mut app, request).await;
assert_eq!(body, Bytes::from_static(b"counter: 100"));
}
#[actix_rt::test]
async fn prolong_expiration() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
}))
.service(
web::resource("/test/")
.to(|| async move { "no-changes-in-session" }),
),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
let expires_1 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
actix_rt::time::delay_for(std::time::Duration::from_secs(1)).await;
let request = test::TestRequest::with_uri("/test/").to_request();
let response = app.call(request).await.unwrap();
let expires_2 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
assert!(expires_2 - expires_1 >= Duration::seconds(1));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,463 @@
use std::{collections::HashMap, fmt, future::Future, pin::Pin, rc::Rc};
use actix_utils::future::{ready, Ready};
use actix_web::{
body::MessageBody,
cookie::{Cookie, CookieJar, Key},
dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
http::header::{HeaderValue, SET_COOKIE},
HttpResponse,
};
use anyhow::Context;
use crate::{
config::{
self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
TtlExtensionPolicy,
},
storage::{LoadError, SessionKey, SessionStore},
Session, SessionStatus,
};
/// A middleware for session management in Actix Web applications.
///
/// [`SessionMiddleware`] takes care of a few jobs:
///
/// - Instructs the session storage backend to create/update/delete/retrieve the state attached to
/// a session according to its status and the operations that have been performed against it;
/// - Set/remove a cookie, on the client side, to enable a user to be consistently associated with
/// the same session across multiple HTTP requests.
///
/// Use [`SessionMiddleware::new`] to initialize the session framework using the default parameters.
/// To create a new instance of [`SessionMiddleware`] you need to provide:
///
/// - an instance of the session storage backend you wish to use (i.e. an implementation of
/// [`SessionStore`]);
/// - a secret key, to sign or encrypt the content of client-side session cookie.
///
/// # How did we choose defaults?
/// You should not regret adding `actix-session` to your dependencies and going to production using
/// the default configuration. That is why, when in doubt, we opt to use the most secure option for
/// each configuration parameter.
///
/// We expose knobs to change the default to suit your needs—i.e., if you know what you are doing,
/// we will not stop you. But being a subject-matter expert should not be a requirement to deploy
/// reasonably secure implementation of sessions.
///
/// # Examples
/// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
/// use actix_web::cookie::Key;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
///
/// HttpServer::new(move || {
/// App::new()
/// // Add session management to your application using Redis as storage
/// .wrap(SessionMiddleware::new(
/// storage.clone(),
/// secret_key.clone(),
/// ))
/// .default_service(web::to(|| HttpResponse::Ok()))
/// })
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
///
/// If you want to customise use [`builder`](Self::builder) instead of [`new`](Self::new):
///
/// ```no_run
/// use actix_web::{App, cookie::{Key, time}, Error, HttpResponse, HttpServer, web};
/// use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
/// use actix_session::config::PersistentSession;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// let storage = RedisSessionStore::new("127.0.0.1:6379").await.unwrap();
///
/// HttpServer::new(move || {
/// App::new()
/// // Customise session length!
/// .wrap(
/// SessionMiddleware::builder(storage.clone(), secret_key.clone())
/// .session_lifecycle(
/// PersistentSession::default().session_ttl(time::Duration::days(5)),
/// )
/// .build(),
/// )
/// .default_service(web::to(|| HttpResponse::Ok()))
/// })
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
#[derive(Clone)]
pub struct SessionMiddleware<Store: SessionStore> {
storage_backend: Rc<Store>,
configuration: Rc<Configuration>,
}
impl<Store: SessionStore> SessionMiddleware<Store> {
/// Use [`SessionMiddleware::new`] to initialize the session framework using the default
/// parameters.
///
/// To create a new instance of [`SessionMiddleware`] you need to provide:
/// - an instance of the session storage backend you wish to use (i.e. an implementation of
/// [`SessionStore`]);
/// - a secret key, to sign or encrypt the content of client-side session cookie.
pub fn new(store: Store, key: Key) -> Self {
Self::builder(store, key).build()
}
/// A fluent API to configure [`SessionMiddleware`].
///
/// It takes as input the two required inputs to create a new instance of [`SessionMiddleware`]:
/// - an instance of the session storage backend you wish to use (i.e. an implementation of
/// [`SessionStore`]);
/// - a secret key, to sign or encrypt the content of client-side session cookie.
pub fn builder(store: Store, key: Key) -> SessionMiddlewareBuilder<Store> {
SessionMiddlewareBuilder::new(store, config::default_configuration(key))
}
pub(crate) fn from_parts(store: Store, configuration: Configuration) -> Self {
Self {
storage_backend: Rc::new(store),
configuration: Rc::new(configuration),
}
}
}
impl<S, B, Store> Transform<S, ServiceRequest> for SessionMiddleware<Store>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
Store: SessionStore + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = InnerSessionMiddleware<S, Store>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(InnerSessionMiddleware {
service: Rc::new(service),
configuration: Rc::clone(&self.configuration),
storage_backend: Rc::clone(&self.storage_backend),
}))
}
}
/// Short-hand to create an `actix_web::Error` instance that will result in an `Internal Server
/// Error` response while preserving the error root cause (e.g. in logs).
fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
// We do not use `actix_web::error::ErrorInternalServerError` because we do not want to
// leak internal implementation details to the caller.
//
// `actix_web::error::ErrorInternalServerError` includes the error Display representation
// as body of the error responses, leading to messages like "There was an issue persisting
// the session state" reaching API clients. We don't want that, we want opaque 500s.
actix_web::error::InternalError::from_response(
err,
HttpResponse::InternalServerError().finish(),
)
.into()
}
#[doc(hidden)]
#[non_exhaustive]
pub struct InnerSessionMiddleware<S, Store: SessionStore + 'static> {
service: Rc<S>,
configuration: Rc<Configuration>,
storage_backend: Rc<Store>,
}
impl<S, B, Store> Service<ServiceRequest> for InnerSessionMiddleware<S, Store>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
Store: SessionStore + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let service = Rc::clone(&self.service);
let storage_backend = Rc::clone(&self.storage_backend);
let configuration = Rc::clone(&self.configuration);
Box::pin(async move {
let session_key = extract_session_key(&req, &configuration.cookie);
let (session_key, session_state) =
load_session_state(session_key, storage_backend.as_ref()).await?;
Session::set_session(&mut req, session_state);
let mut res = service.call(req).await?;
let (status, session_state) = Session::get_changes(&mut res);
match session_key {
None => {
// we do not create an entry in the session store if there is no state attached
// to a fresh session
if !session_state.is_empty() {
let session_key = storage_backend
.save(session_state, &configuration.session.state_ttl)
.await
.map_err(e500)?;
set_session_cookie(
res.response_mut().head_mut(),
session_key,
&configuration.cookie,
)
.map_err(e500)?;
}
}
Some(session_key) => {
match status {
SessionStatus::Changed => {
let session_key = storage_backend
.update(
session_key,
session_state,
&configuration.session.state_ttl,
)
.await
.map_err(e500)?;
set_session_cookie(
res.response_mut().head_mut(),
session_key,
&configuration.cookie,
)
.map_err(e500)?;
}
SessionStatus::Purged => {
storage_backend.delete(&session_key).await.map_err(e500)?;
delete_session_cookie(
res.response_mut().head_mut(),
&configuration.cookie,
)
.map_err(e500)?;
}
SessionStatus::Renewed => {
storage_backend.delete(&session_key).await.map_err(e500)?;
let session_key = storage_backend
.save(session_state, &configuration.session.state_ttl)
.await
.map_err(e500)?;
set_session_cookie(
res.response_mut().head_mut(),
session_key,
&configuration.cookie,
)
.map_err(e500)?;
}
SessionStatus::Unchanged => {
if matches!(
configuration.ttl_extension_policy,
TtlExtensionPolicy::OnEveryRequest
) {
storage_backend
.update_ttl(&session_key, &configuration.session.state_ttl)
.await
.map_err(e500)?;
if configuration.cookie.max_age.is_some() {
set_session_cookie(
res.response_mut().head_mut(),
session_key,
&configuration.cookie,
)
.map_err(e500)?;
}
}
}
};
}
}
Ok(res)
})
}
}
/// Examines the session cookie attached to the incoming request, if there is one, and tries
/// to extract the session key.
///
/// It returns `None` if there is no session cookie or if the session cookie is considered invalid
/// (e.g., when failing a signature check).
fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
let cookies = req.cookies().ok()?;
let session_cookie = cookies
.iter()
.find(|&cookie| cookie.name() == config.name)?;
let mut jar = CookieJar::new();
jar.add_original(session_cookie.clone());
let verification_result = match config.content_security {
CookieContentSecurity::Signed => jar.signed(&config.key).get(&config.name),
CookieContentSecurity::Private => jar.private(&config.key).get(&config.name),
};
if verification_result.is_none() {
tracing::warn!(
"The session cookie attached to the incoming request failed to pass cryptographic \
checks (signature verification/decryption)."
);
}
match verification_result?.value().to_owned().try_into() {
Ok(session_key) => Some(session_key),
Err(err) => {
tracing::warn!(
error.message = %err,
error.cause_chain = ?err,
"Invalid session key, ignoring."
);
None
}
}
}
async fn load_session_state<Store: SessionStore>(
session_key: Option<SessionKey>,
storage_backend: &Store,
) -> Result<(Option<SessionKey>, HashMap<String, String>), actix_web::Error> {
if let Some(session_key) = session_key {
match storage_backend.load(&session_key).await {
Ok(state) => {
if let Some(state) = state {
Ok((Some(session_key), state))
} else {
// We discard the existing session key given that the state attached to it can
// no longer be found (e.g. it expired or we suffered some data loss in the
// storage). Regenerating the session key will trigger the `save` workflow
// instead of the `update` workflow if the session state is modified during the
// lifecycle of the current request.
tracing::info!(
"No session state has been found for a valid session key, creating a new \
empty session."
);
Ok((None, HashMap::new()))
}
}
Err(err) => match err {
LoadError::Deserialization(err) => {
tracing::warn!(
error.message = %err,
error.cause_chain = ?err,
"Invalid session state, creating a new empty session."
);
Ok((Some(session_key), HashMap::new()))
}
LoadError::Other(err) => Err(e500(err)),
},
}
} else {
Ok((None, HashMap::new()))
}
}
fn set_session_cookie(
response: &mut ResponseHead,
session_key: SessionKey,
config: &CookieConfiguration,
) -> Result<(), anyhow::Error> {
let value: String = session_key.into();
let mut cookie = Cookie::new(config.name.clone(), value);
cookie.set_secure(config.secure);
cookie.set_http_only(config.http_only);
cookie.set_same_site(config.same_site);
cookie.set_path(config.path.clone());
if let Some(max_age) = config.max_age {
cookie.set_max_age(max_age);
}
if let Some(ref domain) = config.domain {
cookie.set_domain(domain.clone());
}
let mut jar = CookieJar::new();
match config.content_security {
CookieContentSecurity::Signed => jar.signed_mut(&config.key).add(cookie),
CookieContentSecurity::Private => jar.private_mut(&config.key).add(cookie),
}
// set cookie
let cookie = jar.delta().next().unwrap();
let val = HeaderValue::from_str(&cookie.encoded().to_string())
.context("Failed to attach a session cookie to the outgoing response")?;
response.headers_mut().append(SET_COOKIE, val);
Ok(())
}
fn delete_session_cookie(
response: &mut ResponseHead,
config: &CookieConfiguration,
) -> Result<(), anyhow::Error> {
let removal_cookie = Cookie::build(config.name.clone(), "")
.path(config.path.clone())
.secure(config.secure)
.http_only(config.http_only)
.same_site(config.same_site);
let mut removal_cookie = if let Some(ref domain) = config.domain {
removal_cookie.domain(domain)
} else {
removal_cookie
}
.finish();
removal_cookie.make_removal();
let val = HeaderValue::from_str(&removal_cookie.to_string())
.context("Failed to attach a session removal cookie to the outgoing response")?;
response.headers_mut().append(SET_COOKIE, val);
Ok(())
}

View File

@ -0,0 +1,424 @@
use std::{
cell::{Ref, RefCell},
collections::HashMap,
error::Error as StdError,
mem,
rc::Rc,
};
use actix_utils::future::{ready, Ready};
use actix_web::{
body::BoxBody,
dev::{Extensions, Payload, ServiceRequest, ServiceResponse},
error::Error,
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use anyhow::Context;
use derive_more::derive::{Display, From};
use serde::{de::DeserializeOwned, Serialize};
/// The primary interface to access and modify session state.
///
/// [`Session`] is an [extractor](#impl-FromRequest)—you can specify it as an input type for your
/// request handlers and it will be automatically extracted from the incoming request.
///
/// ```
/// use actix_session::Session;
///
/// async fn index(session: Session) -> actix_web::Result<&'static str> {
/// // access session data
/// if let Some(count) = session.get::<i32>("counter")? {
/// session.insert("counter", count + 1)?;
/// } else {
/// session.insert("counter", 1)?;
/// }
///
/// // or use the shorthand
/// session.update_or("counter", 1, |count: i32| count + 1);
///
/// Ok("Welcome!")
/// }
/// # actix_web::web::to(index);
/// ```
///
/// You can also retrieve a [`Session`] object from an `HttpRequest` or a `ServiceRequest` using
/// [`SessionExt`].
///
/// [`SessionExt`]: crate::SessionExt
#[derive(Clone)]
pub struct Session(Rc<RefCell<SessionInner>>);
/// Status of a [`Session`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum SessionStatus {
/// Session state has been updated - the changes will have to be persisted to the backend.
Changed,
/// The session has been flagged for deletion - the session cookie will be removed from
/// the client and the session state will be deleted from the session store.
///
/// Most operations on the session after it has been marked for deletion will have no effect.
Purged,
/// The session has been flagged for renewal.
///
/// The session key will be regenerated and the time-to-live of the session state will be
/// extended.
Renewed,
/// The session state has not been modified since its creation/retrieval.
#[default]
Unchanged,
}
#[derive(Default)]
struct SessionInner {
state: HashMap<String, String>,
status: SessionStatus,
}
impl Session {
/// Get a `value` from the session.
///
/// It returns an error if it fails to deserialize as `T` the JSON value associated with `key`.
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, SessionGetError> {
if let Some(val_str) = self.0.borrow().state.get(key) {
Ok(Some(
serde_json::from_str(val_str)
.with_context(|| {
format!(
"Failed to deserialize the JSON-encoded session data attached to key \
`{}` as a `{}` type",
key,
std::any::type_name::<T>()
)
})
.map_err(SessionGetError)?,
))
} else {
Ok(None)
}
}
/// Returns `true` if the session contains a value for the specified `key`.
pub fn contains_key(&self, key: &str) -> bool {
self.0.borrow().state.contains_key(key)
}
/// Get all raw key-value data from the session.
///
/// Note that values are JSON encoded.
pub fn entries(&self) -> Ref<'_, HashMap<String, String>> {
Ref::map(self.0.borrow(), |inner| &inner.state)
}
/// Returns session status.
pub fn status(&self) -> SessionStatus {
Ref::map(self.0.borrow(), |inner| &inner.status).clone()
}
/// Inserts a key-value pair into the session.
///
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
/// only a reference to the value is taken.
///
/// # Errors
///
/// Returns an error if JSON serialization of `value` fails.
pub fn insert<T: Serialize>(
&self,
key: impl Into<String>,
value: T,
) -> Result<(), SessionInsertError> {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
let key = key.into();
let val = serde_json::to_string(&value)
.with_context(|| {
format!(
"Failed to serialize the provided `{}` type instance as JSON in order to \
attach as session data to the `{key}` key",
std::any::type_name::<T>(),
)
})
.map_err(SessionInsertError)?;
inner.state.insert(key, val);
}
Ok(())
}
/// Updates a key-value pair into the session.
///
/// If the key exists then update it to the new value and place it back in. If the key does not
/// exist it will not be updated.
///
/// Any serializable value can be used and will be encoded as JSON in the session data, hence
/// why only a reference to the value is taken.
///
/// # Errors
///
/// Returns an error if JSON serialization of the value fails.
pub fn update<T: Serialize + DeserializeOwned, F>(
&self,
key: impl Into<String>,
updater: F,
) -> Result<(), SessionUpdateError>
where
F: FnOnce(T) -> T,
{
let mut inner = self.0.borrow_mut();
let key_str = key.into();
if let Some(val_str) = inner.state.get(&key_str) {
let value = serde_json::from_str(val_str)
.with_context(|| {
format!(
"Failed to deserialize the JSON-encoded session data attached to key \
`{key_str}` as a `{}` type",
std::any::type_name::<T>()
)
})
.map_err(SessionUpdateError)?;
let val = serde_json::to_string(&updater(value))
.with_context(|| {
format!(
"Failed to serialize the provided `{}` type instance as JSON in order to \
attach as session data to the `{key_str}` key",
std::any::type_name::<T>(),
)
})
.map_err(SessionUpdateError)?;
inner.state.insert(key_str, val);
}
Ok(())
}
/// Updates a key-value pair into the session, or inserts a default value.
///
/// If the key exists then update it to the new value and place it back in. If the key does not
/// exist the default value will be inserted instead.
///
/// Any serializable value can be used and will be encoded as JSON in session data, hence why
/// only a reference to the value is taken.
///
/// # Errors
///
/// Returns error if JSON serialization of a value fails.
pub fn update_or<T: Serialize + DeserializeOwned, F>(
&self,
key: &str,
default_value: T,
updater: F,
) -> Result<(), SessionUpdateError>
where
F: FnOnce(T) -> T,
{
if self.contains_key(key) {
self.update(key, updater)
} else {
self.insert(key, default_value)
.map_err(|err| SessionUpdateError(err.into()))
}
}
/// Remove value from the session.
///
/// If present, the JSON encoded value is returned.
pub fn remove(&self, key: &str) -> Option<String> {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
return inner.state.remove(key);
}
None
}
/// Remove value from the session and deserialize.
///
/// Returns `None` if key was not present in session. Returns `T` if deserialization succeeds,
/// otherwise returns un-deserialized JSON string.
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
self.remove(key)
.map(|val_str| match serde_json::from_str(&val_str) {
Ok(val) => Ok(val),
Err(_err) => {
tracing::debug!(
"Removed value (key: {}) could not be deserialized as {}",
key,
std::any::type_name::<T>()
);
Err(val_str)
}
})
}
/// Clear the session.
pub fn clear(&self) {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
if inner.status != SessionStatus::Renewed {
inner.status = SessionStatus::Changed;
}
inner.state.clear()
}
}
/// Removes session both client and server side.
pub fn purge(&self) {
let mut inner = self.0.borrow_mut();
inner.status = SessionStatus::Purged;
inner.state.clear();
}
/// Renews the session key, assigning existing session state to new key.
pub fn renew(&self) {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
inner.status = SessionStatus::Renewed;
}
}
/// Adds the given key-value pairs to the session on the request.
///
/// Values that match keys already existing on the session will be overwritten. Values should
/// already be JSON serialized.
#[allow(clippy::needless_pass_by_ref_mut)]
pub(crate) fn set_session(
req: &mut ServiceRequest,
data: impl IntoIterator<Item = (String, String)>,
) {
let session = Session::get_session(&mut req.extensions_mut());
let mut inner = session.0.borrow_mut();
inner.state.extend(data);
}
/// Returns session status and iterator of key-value pairs of changes.
///
/// This is a destructive operation - the session state is removed from the request extensions
/// typemap, leaving behind a new empty map. It should only be used when the session is being
/// finalised (i.e. in `SessionMiddleware`).
#[allow(clippy::needless_pass_by_ref_mut)]
pub(crate) fn get_changes<B>(
res: &mut ServiceResponse<B>,
) -> (SessionStatus, HashMap<String, String>) {
if let Some(s_impl) = res
.request()
.extensions()
.get::<Rc<RefCell<SessionInner>>>()
{
let state = mem::take(&mut s_impl.borrow_mut().state);
(s_impl.borrow().status.clone(), state)
} else {
(SessionStatus::Unchanged, HashMap::new())
}
}
pub(crate) fn get_session(extensions: &mut Extensions) -> Session {
if let Some(s_impl) = extensions.get::<Rc<RefCell<SessionInner>>>() {
return Session(Rc::clone(s_impl));
}
let inner = Rc::new(RefCell::new(SessionInner::default()));
extensions.insert(inner.clone());
Session(inner)
}
}
/// Extractor implementation for [`Session`]s.
///
/// # Examples
/// ```
/// # use actix_web::*;
/// use actix_session::Session;
///
/// #[get("/")]
/// async fn index(session: Session) -> Result<impl Responder> {
/// // access session data
/// if let Some(count) = session.get::<i32>("counter")? {
/// session.insert("counter", count + 1)?;
/// } else {
/// session.insert("counter", 1)?;
/// }
///
/// let count = session.get::<i32>("counter")?.unwrap();
/// Ok(format!("Counter: {}", count))
/// }
/// ```
impl FromRequest for Session {
type Error = Error;
type Future = Ready<Result<Session, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(Ok(Session::get_session(&mut req.extensions_mut())))
}
}
/// Error returned by [`Session::get`].
#[derive(Debug, Display, From)]
#[display("{_0}")]
pub struct SessionGetError(anyhow::Error);
impl StdError for SessionGetError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.0.as_ref())
}
}
impl ResponseError for SessionGetError {
fn error_response(&self) -> HttpResponse<BoxBody> {
HttpResponse::new(self.status_code())
}
}
/// Error returned by [`Session::insert`].
#[derive(Debug, Display, From)]
#[display("{_0}")]
pub struct SessionInsertError(anyhow::Error);
impl StdError for SessionInsertError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.0.as_ref())
}
}
impl ResponseError for SessionInsertError {
fn error_response(&self) -> HttpResponse<BoxBody> {
HttpResponse::new(self.status_code())
}
}
/// Error returned by [`Session::update`].
#[derive(Debug, Display, From)]
#[display("{_0}")]
pub struct SessionUpdateError(anyhow::Error);
impl StdError for SessionUpdateError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.0.as_ref())
}
}
impl ResponseError for SessionUpdateError {
fn error_response(&self) -> HttpResponse<BoxBody> {
HttpResponse::new(self.status_code())
}
}

View File

@ -0,0 +1,38 @@
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
guard::GuardContext,
HttpMessage, HttpRequest,
};
use crate::Session;
/// Extract a [`Session`] object from various `actix-web` types (e.g. `HttpRequest`,
/// `ServiceRequest`, `ServiceResponse`).
pub trait SessionExt {
/// Extract a [`Session`] object.
fn get_session(&self) -> Session;
}
impl SessionExt for HttpRequest {
fn get_session(&self) -> Session {
Session::get_session(&mut self.extensions_mut())
}
}
impl SessionExt for ServiceRequest {
fn get_session(&self) -> Session {
Session::get_session(&mut self.extensions_mut())
}
}
impl SessionExt for ServiceResponse {
fn get_session(&self) -> Session {
self.request().get_session()
}
}
impl SessionExt for GuardContext<'_> {
fn get_session(&self) -> Session {
Session::get_session(&mut self.req_data_mut())
}
}

View File

@ -0,0 +1,117 @@
use actix_web::cookie::time::Duration;
use anyhow::Error;
use super::SessionKey;
use crate::storage::{
interface::{LoadError, SaveError, SessionState, UpdateError},
SessionStore,
};
/// Use the session key, stored in the session cookie, as storage backend for the session state.
///
/// ```no_run
/// use actix_web::{cookie::Key, web, App, HttpServer, HttpResponse, Error};
/// use actix_session::{SessionMiddleware, storage::CookieSessionStore};
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// HttpServer::new(move ||
/// App::new()
/// .wrap(SessionMiddleware::new(CookieSessionStore::default(), secret_key.clone()))
/// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
///
/// # Limitations
/// Cookies are subject to size limits so we require session keys to be shorter than 4096 bytes.
/// This translates into a limit on the maximum size of the session state when using cookies as
/// storage backend.
///
/// The session cookie can always be inspected by end users via the developer tools exposed by their
/// browsers. We strongly recommend setting the policy to [`CookieContentSecurity::Private`] when
/// using cookies as storage backend.
///
/// There is no way to invalidate a session before its natural expiry when using cookies as the
/// storage backend.
///
/// [`CookieContentSecurity::Private`]: crate::config::CookieContentSecurity::Private
#[derive(Default)]
#[non_exhaustive]
pub struct CookieSessionStore;
impl SessionStore for CookieSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
serde_json::from_str(session_key.as_ref())
.map(Some)
.map_err(anyhow::Error::new)
.map_err(LoadError::Deserialization)
}
async fn save(
&self,
session_state: SessionState,
_ttl: &Duration,
) -> Result<SessionKey, SaveError> {
let session_key = serde_json::to_string(&session_state)
.map_err(anyhow::Error::new)
.map_err(SaveError::Serialization)?;
session_key
.try_into()
.map_err(Into::into)
.map_err(SaveError::Other)
}
async fn update(
&self,
_session_key: SessionKey,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
self.save(session_state, ttl)
.await
.map_err(|err| match err {
SaveError::Serialization(err) => UpdateError::Serialization(err),
SaveError::Other(err) => UpdateError::Other(err),
})
}
async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
Ok(())
}
async fn delete(&self, _session_key: &SessionKey) -> Result<(), anyhow::Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{storage::utils::generate_session_key, test_helpers::acceptance_test_suite};
#[actix_web::test]
async fn test_session_workflow() {
acceptance_test_suite(CookieSessionStore::default, false).await;
}
#[actix_web::test]
async fn loading_a_random_session_key_returns_deserialization_error() {
let store = CookieSessionStore::default();
let session_key = generate_session_key();
assert!(matches!(
store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_),
));
}
}

View File

@ -0,0 +1,113 @@
use std::{collections::HashMap, future::Future};
use actix_web::cookie::time::Duration;
use derive_more::derive::Display;
use super::SessionKey;
pub(crate) type SessionState = HashMap<String, String>;
/// 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.
pub trait SessionStore {
/// Loads the session state associated to a session key.
fn load(
&self,
session_key: &SessionKey,
) -> impl Future<Output = Result<Option<SessionState>, LoadError>>;
/// Persist the session state for a newly created session.
///
/// Returns the corresponding session key.
fn save(
&self,
session_state: SessionState,
ttl: &Duration,
) -> impl Future<Output = Result<SessionKey, SaveError>>;
/// Updates the session state associated to a pre-existing session key.
fn update(
&self,
session_key: SessionKey,
session_state: SessionState,
ttl: &Duration,
) -> impl Future<Output = Result<SessionKey, UpdateError>>;
/// Updates the TTL of the session state associated to a pre-existing session key.
fn update_ttl(
&self,
session_key: &SessionKey,
ttl: &Duration,
) -> impl Future<Output = Result<(), anyhow::Error>>;
/// Deletes a session from the store.
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:
// `derive_more`'s `#[error(source)]` attribute requires the source implement the `Error` trait,
// while it's actually enough for it to be able to produce a reference to a dyn Error.
/// Possible failures modes for [`SessionStore::load`].
#[derive(Debug, Display)]
pub enum LoadError {
/// Failed to deserialize session state.
#[display("Failed to deserialize session state")]
Deserialization(anyhow::Error),
/// Something went wrong when retrieving the session state.
#[display("Something went wrong when retrieving the session state")]
Other(anyhow::Error),
}
impl std::error::Error for LoadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Deserialization(err) => Some(err.as_ref()),
Self::Other(err) => Some(err.as_ref()),
}
}
}
/// Possible failures modes for [`SessionStore::save`].
#[derive(Debug, Display)]
pub enum SaveError {
/// Failed to serialize session state.
#[display("Failed to serialize session state")]
Serialization(anyhow::Error),
/// Something went wrong when persisting the session state.
#[display("Something went wrong when persisting the session state")]
Other(anyhow::Error),
}
impl std::error::Error for SaveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Serialization(err) => Some(err.as_ref()),
Self::Other(err) => Some(err.as_ref()),
}
}
}
#[derive(Debug, Display)]
/// Possible failures modes for [`SessionStore::update`].
pub enum UpdateError {
/// Failed to serialize session state.
#[display("Failed to serialize session state")]
Serialization(anyhow::Error),
/// Something went wrong when updating the session state.
#[display("Something went wrong when updating the session state.")]
Other(anyhow::Error),
}
impl std::error::Error for UpdateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Serialization(err) => Some(err.as_ref()),
Self::Other(err) => Some(err.as_ref()),
}
}
}

View File

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

View File

@ -0,0 +1,310 @@
use actix::Addr;
use actix_redis::{resp_array, Command, RedisActor, RespValue};
use actix_web::cookie::time::Duration;
use anyhow::Error;
use super::SessionKey;
use crate::storage::{
interface::{LoadError, SaveError, SessionState, UpdateError},
utils::generate_session_key,
SessionStore,
};
/// Use Redis as session storage backend.
///
/// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session::{SessionMiddleware, storage::RedisActorSessionStore};
/// use actix_web::cookie::Key;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// let redis_connection_string = "127.0.0.1:6379";
/// HttpServer::new(move ||
/// App::new()
/// .wrap(
/// SessionMiddleware::new(
/// RedisActorSessionStore::new(redis_connection_string),
/// secret_key.clone()
/// )
/// )
/// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
///
/// # Implementation notes
///
/// `RedisActorSessionStore` leverages `actix-redis`'s `RedisActor` implementation - each thread
/// worker gets its own connection to Redis.
///
/// ## Limitations
///
/// `RedisActorSessionStore` does not currently support establishing authenticated connections to
/// Redis. Use [`RedisSessionStore`] if you need TLS support.
///
/// [`RedisSessionStore`]: crate::storage::RedisSessionStore
pub struct RedisActorSessionStore {
configuration: CacheConfiguration,
addr: Addr<RedisActor>,
}
impl RedisActorSessionStore {
/// A fluent API to configure [`RedisActorSessionStore`].
///
/// It takes as input the only required input to create a new instance of
/// [`RedisActorSessionStore`]—a connection string for Redis.
pub fn builder<S: Into<String>>(connection_string: S) -> RedisActorSessionStoreBuilder {
RedisActorSessionStoreBuilder {
configuration: CacheConfiguration::default(),
connection_string: connection_string.into(),
}
}
/// Create a new instance of [`RedisActorSessionStore`] using the default configuration.
/// It takes as input the only required input to create a new instance of [`RedisActorSessionStore`] - a
/// connection string for Redis.
pub fn new<S: Into<String>>(connection_string: S) -> RedisActorSessionStore {
Self::builder(connection_string).build()
}
}
struct CacheConfiguration {
cache_keygen: Box<dyn Fn(&str) -> String>,
}
impl Default for CacheConfiguration {
fn default() -> Self {
Self {
cache_keygen: Box::new(str::to_owned),
}
}
}
/// A fluent builder to construct a [`RedisActorSessionStore`] instance with custom configuration
/// parameters.
#[must_use]
pub struct RedisActorSessionStoreBuilder {
connection_string: String,
configuration: CacheConfiguration,
}
impl RedisActorSessionStoreBuilder {
/// Set a custom cache key generation strategy, expecting a session key as input.
pub fn cache_keygen<F>(mut self, keygen: F) -> Self
where
F: Fn(&str) -> String + 'static,
{
self.configuration.cache_keygen = Box::new(keygen);
self
}
/// Finalise the builder and return a [`RedisActorSessionStore`] instance.
#[must_use]
pub fn build(self) -> RedisActorSessionStore {
RedisActorSessionStore {
configuration: self.configuration,
addr: RedisActor::start(self.connection_string),
}
}
}
impl SessionStore for RedisActorSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let val = self
.addr
.send(Command(resp_array!["GET", cache_key]))
.await
.map_err(Into::into)
.map_err(LoadError::Other)?
.map_err(Into::into)
.map_err(LoadError::Other)?;
match val {
RespValue::Error(err) => Err(LoadError::Other(anyhow::anyhow!(err))),
RespValue::SimpleString(s) => Ok(serde_json::from_str(&s)
.map_err(Into::into)
.map_err(LoadError::Deserialization)?),
RespValue::BulkString(s) => Ok(serde_json::from_slice(&s)
.map_err(Into::into)
.map_err(LoadError::Deserialization)?),
_ => Ok(None),
}
}
async fn save(
&self,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, SaveError> {
let body = serde_json::to_string(&session_state)
.map_err(Into::into)
.map_err(SaveError::Serialization)?;
let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let cmd = Command(resp_array![
"SET",
cache_key,
body,
"NX", // NX: only set the key if it does not already exist
"EX", // EX: set expiry
format!("{}", ttl.whole_seconds())
]);
let result = self
.addr
.send(cmd)
.await
.map_err(Into::into)
.map_err(SaveError::Other)?
.map_err(Into::into)
.map_err(SaveError::Other)?;
match result {
RespValue::SimpleString(_) => Ok(session_key),
RespValue::Nil => Err(SaveError::Other(anyhow::anyhow!(
"Failed to save session state. A record with the same key already existed in Redis"
))),
err => Err(SaveError::Other(anyhow::anyhow!(
"Failed to save session state. {:?}",
err
))),
}
}
async fn update(
&self,
session_key: SessionKey,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
let body = serde_json::to_string(&session_state)
.map_err(Into::into)
.map_err(UpdateError::Serialization)?;
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let cmd = Command(resp_array![
"SET",
cache_key,
body,
"XX", // XX: Only set the key if it already exist.
"EX", // EX: set expiry
format!("{}", ttl.whole_seconds())
]);
let result = self
.addr
.send(cmd)
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?
.map_err(Into::into)
.map_err(UpdateError::Other)?;
match result {
RespValue::Nil => {
// The SET operation was not performed because the XX condition was not verified.
// This can happen if the session state expired between the load operation and the
// update operation. Unlucky, to say the least. We fall back to the `save` routine
// to ensure that the new key is unique.
self.save(session_state, ttl)
.await
.map_err(|err| match err {
SaveError::Serialization(err) => UpdateError::Serialization(err),
SaveError::Other(err) => UpdateError::Other(err),
})
}
RespValue::SimpleString(_) => Ok(session_key),
val => Err(UpdateError::Other(anyhow::anyhow!(
"Failed to update session state. {:?}",
val
))),
}
}
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let cmd = Command(resp_array![
"EXPIRE",
cache_key,
ttl.whole_seconds().to_string()
]);
match self.addr.send(cmd).await? {
Ok(RespValue::Integer(_)) => Ok(()),
val => Err(anyhow::anyhow!(
"Failed to update the session state TTL: {:?}",
val
)),
}
}
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let res = self
.addr
.send(Command(resp_array!["DEL", cache_key]))
.await?;
match res {
// Redis returns the number of deleted records
Ok(RespValue::Integer(_)) => Ok(()),
val => Err(anyhow::anyhow!(
"Failed to remove session from cache. {:?}",
val
)),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::test_helpers::acceptance_test_suite;
fn redis_actor_store() -> RedisActorSessionStore {
RedisActorSessionStore::new("127.0.0.1:6379")
}
#[actix_web::test]
async fn test_session_workflow() {
acceptance_test_suite(redis_actor_store, true).await;
}
#[actix_web::test]
async fn loading_a_missing_session_returns_none() {
let store = redis_actor_store();
let session_key = generate_session_key();
assert!(store.load(&session_key).await.unwrap().is_none());
}
#[actix_web::test]
async fn updating_of_an_expired_state_is_handled_gracefully() {
let store = redis_actor_store();
let session_key = generate_session_key();
let initial_session_key = session_key.as_ref().to_owned();
let updated_session_key = store
.update(session_key, HashMap::new(), &Duration::seconds(1))
.await
.unwrap();
assert_ne!(initial_session_key, updated_session_key.as_ref());
}
}

View File

@ -0,0 +1,476 @@
use std::sync::Arc;
use actix_web::cookie::time::Duration;
use anyhow::Error;
use redis::{aio::ConnectionManager, AsyncCommands, Client, Cmd, FromRedisValue, Value};
use super::SessionKey;
use crate::storage::{
interface::{LoadError, SaveError, SessionState, UpdateError},
utils::generate_session_key,
SessionStore,
};
/// Use Redis as session storage backend.
///
/// ```no_run
/// use actix_web::{web, App, HttpServer, HttpResponse, Error};
/// use actix_session::{SessionMiddleware, storage::RedisSessionStore};
/// use actix_web::cookie::Key;
///
/// // The secret key would usually be read from a configuration file/environment variables.
/// fn get_secret_key() -> Key {
/// # todo!()
/// // [...]
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let secret_key = get_secret_key();
/// let redis_connection_string = "redis://127.0.0.1:6379";
/// let store = RedisSessionStore::new(redis_connection_string).await.unwrap();
///
/// HttpServer::new(move ||
/// App::new()
/// .wrap(SessionMiddleware::new(
/// store.clone(),
/// secret_key.clone()
/// ))
/// .default_service(web::to(|| HttpResponse::Ok())))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
///
/// # TLS support
/// Add the `redis-session-native-tls` or `redis-session-rustls` feature flag to enable TLS support. You can then establish a TLS
/// connection to Redis using the `rediss://` URL scheme:
///
/// ```no_run
/// use actix_session::{storage::RedisSessionStore};
///
/// # actix_web::rt::System::new().block_on(async {
/// let redis_connection_string = "rediss://127.0.0.1:6379";
/// let store = RedisSessionStore::new(redis_connection_string).await.unwrap();
/// # })
/// ```
///
/// # Pooled Redis Connections
///
/// When the `redis-pool` crate feature is enabled, a pre-existing pool from [`deadpool_redis`] can
/// be provided.
///
/// ```no_run
/// use actix_session::storage::RedisSessionStore;
/// use deadpool_redis::{Config, Runtime};
///
/// let redis_cfg = Config::from_url("redis://127.0.0.1:6379");
/// let redis_pool = redis_cfg.create_pool(Some(Runtime::Tokio1)).unwrap();
///
/// let store = RedisSessionStore::new_pooled(redis_pool);
/// ```
///
/// # Implementation notes
///
/// `RedisSessionStore` leverages the [`redis`] crate as the underlying Redis client.
#[derive(Clone)]
pub struct RedisSessionStore {
configuration: CacheConfiguration,
client: RedisSessionConn,
}
#[derive(Clone)]
enum RedisSessionConn {
/// Single connection.
Single(ConnectionManager),
/// Connection pool.
#[cfg(feature = "redis-pool")]
Pool(deadpool_redis::Pool),
}
#[derive(Clone)]
struct CacheConfiguration {
cache_keygen: Arc<dyn Fn(&str) -> String + Send + Sync>,
}
impl Default for CacheConfiguration {
fn default() -> Self {
Self {
cache_keygen: Arc::new(str::to_owned),
}
}
}
impl 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.
pub fn builder(connection_string: impl Into<String>) -> RedisSessionStoreBuilder {
RedisSessionStoreBuilder {
configuration: CacheConfiguration::default(),
conn_builder: RedisSessionConnBuilder::Single(connection_string.into()),
}
}
/// Returns a fluent API builder to configure [`RedisSessionStore`].
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a pool object for Redis.
#[cfg(feature = "redis-pool")]
pub fn builder_pooled(pool: impl Into<deadpool_redis::Pool>) -> RedisSessionStoreBuilder {
RedisSessionStoreBuilder {
configuration: CacheConfiguration::default(),
conn_builder: RedisSessionConnBuilder::Pool(pool.into()),
}
}
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a connection string for Redis.
pub async fn new(connection_string: impl Into<String>) -> Result<RedisSessionStore, Error> {
Self::builder(connection_string).build().await
}
/// Creates a new instance of [`RedisSessionStore`] using the default configuration.
///
/// It takes as input the only required input to create a new instance of [`RedisSessionStore`]
/// - a pool object for Redis.
#[cfg(feature = "redis-pool")]
pub async fn new_pooled(
pool: impl Into<deadpool_redis::Pool>,
) -> anyhow::Result<RedisSessionStore> {
Self::builder_pooled(pool).build().await
}
}
/// A fluent builder to construct a [`RedisSessionStore`] instance with custom configuration
/// parameters.
#[must_use]
pub struct RedisSessionStoreBuilder {
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 {
/// Set a custom cache key generation strategy, expecting a session key as input.
pub fn cache_keygen<F>(mut self, keygen: F) -> Self
where
F: Fn(&str) -> String + 'static + Send + Sync,
{
self.configuration.cache_keygen = Arc::new(keygen);
self
}
/// Finalises builder and returns a [`RedisSessionStore`] instance.
pub async fn build(self) -> anyhow::Result<RedisSessionStore> {
let client = self.conn_builder.into_client().await?;
Ok(RedisSessionStore {
configuration: self.configuration,
client,
})
}
}
impl SessionStore for RedisSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let value: Option<String> = self
.execute_command(redis::cmd("GET").arg(&[&cache_key]))
.await
.map_err(LoadError::Other)?;
match value {
None => Ok(None),
Some(value) => Ok(serde_json::from_str(&value)
.map_err(Into::into)
.map_err(LoadError::Deserialization)?),
}
}
async fn save(
&self,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, SaveError> {
let body = serde_json::to_string(&session_state)
.map_err(Into::into)
.map_err(SaveError::Serialization)?;
let session_key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.execute_command::<()>(
redis::cmd("SET")
.arg(&[
&cache_key, // key
&body, // value
"NX", // only set the key if it does not already exist
"EX", // set expiry / TTL
])
.arg(
ttl.whole_seconds(), // EXpiry in seconds
),
)
.await
.map_err(SaveError::Other)?;
Ok(session_key)
}
async fn update(
&self,
session_key: SessionKey,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
let body = serde_json::to_string(&session_state)
.map_err(Into::into)
.map_err(UpdateError::Serialization)?;
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let v: Value = self
.execute_command(redis::cmd("SET").arg(&[
&cache_key,
&body,
"XX", // XX: Only set the key if it already exist.
"EX", // EX: set expiry
&format!("{}", ttl.whole_seconds()),
]))
.await
.map_err(UpdateError::Other)?;
match v {
Value::Nil => {
// The SET operation was not performed because the XX condition was not verified.
// This can happen if the session state expired between the load operation and the
// update operation. Unlucky, to say the least. We fall back to the `save` routine
// to ensure that the new key is unique.
self.save(session_state, ttl)
.await
.map_err(|err| match err {
SaveError::Serialization(err) => UpdateError::Serialization(err),
SaveError::Other(err) => UpdateError::Other(err),
})
}
Value::Int(_) | Value::Okay | Value::SimpleString(_) => Ok(session_key),
val => Err(UpdateError::Other(anyhow::anyhow!(
"Failed to update session state. {:?}",
val
))),
}
}
async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> anyhow::Result<()> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
match self.client {
RedisSessionConn::Single(ref conn) => {
conn.clone()
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
}
#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
pool.get()
.await?
.expire::<_, ()>(&cache_key, ttl.whole_seconds())
.await?;
}
}
Ok(())
}
async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> {
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
self.execute_command::<()>(redis::cmd("DEL").arg(&[&cache_key]))
.await
.map_err(UpdateError::Other)?;
Ok(())
}
}
impl RedisSessionStore {
/// Execute Redis command and retry once in certain cases.
///
/// `ConnectionManager` automatically reconnects when it encounters an error talking to Redis.
/// The request that bumped into the error, though, fails.
///
/// This is generally OK, but there is an unpleasant edge case: Redis client timeouts. The
/// server is configured to drop connections who have been active longer than a pre-determined
/// threshold. `redis-rs` does not proactively detect that the connection has been dropped - you
/// only find out when you try to use it.
///
/// This helper method catches this case (`.is_connection_dropped`) to execute a retry. The
/// retry will be executed on a fresh connection, therefore it is likely to succeed (or fail for
/// a different more meaningful reason).
#[allow(clippy::needless_pass_by_ref_mut)]
async fn execute_command<T: FromRedisValue>(&self, cmd: &mut Cmd) -> anyhow::Result<T> {
let mut can_retry = true;
match self.client {
RedisSessionConn::Single(ref conn) => {
let mut conn = conn.clone();
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());
}
}
}
}
}
#[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());
}
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use actix_web::cookie::time;
#[cfg(not(feature = "redis-session"))]
use deadpool_redis::{Config, Runtime};
use super::*;
use crate::test_helpers::acceptance_test_suite;
async fn redis_store() -> RedisSessionStore {
#[cfg(feature = "redis-session")]
{
RedisSessionStore::new("redis://127.0.0.1:6379")
.await
.unwrap()
}
#[cfg(not(feature = "redis-session"))]
{
let redis_pool = Config::from_url("redis://127.0.0.1:6379")
.create_pool(Some(Runtime::Tokio1))
.unwrap();
RedisSessionStore::new(redis_pool.clone())
}
}
#[actix_web::test]
async fn test_session_workflow() {
let redis_store = redis_store().await;
acceptance_test_suite(move || redis_store.clone(), true).await;
}
#[actix_web::test]
async fn loading_a_missing_session_returns_none() {
let store = redis_store().await;
let session_key = generate_session_key();
assert!(store.load(&session_key).await.unwrap().is_none());
}
#[actix_web::test]
async fn loading_an_invalid_session_state_returns_deserialization_error() {
let store = redis_store().await;
let session_key = generate_session_key();
match store.client {
RedisSessionConn::Single(ref conn) => conn
.clone()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await
.unwrap(),
#[cfg(feature = "redis-pool")]
RedisSessionConn::Pool(ref pool) => {
pool.get()
.await
.unwrap()
.set::<_, _, ()>(session_key.as_ref(), "random-thing-which-is-not-json")
.await
.unwrap();
}
}
assert!(matches!(
store.load(&session_key).await.unwrap_err(),
LoadError::Deserialization(_),
));
}
#[actix_web::test]
async fn updating_of_an_expired_state_is_handled_gracefully() {
let store = redis_store().await;
let session_key = generate_session_key();
let initial_session_key = session_key.as_ref().to_owned();
let updated_session_key = store
.update(session_key, HashMap::new(), &time::Duration::seconds(1))
.await
.unwrap();
assert_ne!(initial_session_key, updated_session_key.as_ref());
}
}

View File

@ -0,0 +1,55 @@
use derive_more::derive::{Display, From};
/// A session key, the string stored in a client-side cookie to associate a user with its session
/// state on the backend.
///
/// # Validation
/// Session keys are stored as cookies, therefore they cannot be arbitrary long. Session keys are
/// required to be smaller than 4064 bytes.
///
/// ```
/// use actix_session::storage::SessionKey;
///
/// let key: String = std::iter::repeat('a').take(4065).collect();
/// let session_key: Result<SessionKey, _> = key.try_into();
/// assert!(session_key.is_err());
/// ```
#[derive(Debug, PartialEq, Eq)]
pub struct SessionKey(String);
impl TryFrom<String> for SessionKey {
type Error = InvalidSessionKeyError;
fn try_from(val: String) -> Result<Self, Self::Error> {
if val.len() > 4064 {
return Err(anyhow::anyhow!(
"The session key is bigger than 4064 bytes, the upper limit on cookie content."
)
.into());
}
Ok(SessionKey(val))
}
}
impl AsRef<str> for SessionKey {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<SessionKey> for String {
fn from(key: SessionKey) -> Self {
key.0
}
}
#[derive(Debug, Display, From)]
#[display("The provided string is not a valid session key")]
pub struct InvalidSessionKeyError(anyhow::Error);
impl std::error::Error for InvalidSessionKeyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_ref())
}
}

View File

@ -0,0 +1,13 @@
use rand::distr::{Alphanumeric, SampleString as _};
use crate::storage::SessionKey;
/// Session key generation routine that follows [OWASP recommendations].
///
/// [OWASP recommendations]: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
pub fn generate_session_key() -> SessionKey {
Alphanumeric
.sample_string(&mut rand::rng(), 64)
.try_into()
.expect("generated string should be within size range for a session key")
}

View File

@ -0,0 +1,56 @@
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{
cookie::{time::Duration, Key},
test, web, App, Responder,
};
async fn login(session: Session) -> impl Responder {
session.insert("user_id", "id").unwrap();
"Logged in"
}
async fn logout(session: Session) -> impl Responder {
session.purge();
"Logged out"
}
#[actix_web::test]
async fn cookie_storage() -> std::io::Result<()> {
let signing_key = Key::generate();
let app = test::init_service(
App::new()
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), signing_key.clone())
.cookie_path("/test".to_string())
.cookie_domain(Some("localhost".to_string()))
.build(),
)
.route("/login", web::post().to(login))
.route("/logout", web::post().to(logout)),
)
.await;
let login_request = test::TestRequest::post().uri("/login").to_request();
let login_response = test::call_service(&app, login_request).await;
let session_cookie = login_response.response().cookies().next().unwrap();
assert_eq!(session_cookie.name(), "id");
assert_eq!(session_cookie.path().unwrap(), "/test");
assert!(session_cookie.secure().unwrap());
assert!(session_cookie.http_only().unwrap());
assert!(session_cookie.max_age().is_none());
assert_eq!(session_cookie.domain().unwrap(), "localhost");
let logout_request = test::TestRequest::post()
.cookie(session_cookie)
.uri("/logout")
.to_request();
let logout_response = test::call_service(&app, logout_request).await;
let deletion_cookie = logout_response.response().cookies().next().unwrap();
assert_eq!(deletion_cookie.name(), "id");
assert_eq!(deletion_cookie.path().unwrap(), "/test");
assert!(deletion_cookie.secure().unwrap());
assert!(deletion_cookie.http_only().unwrap());
assert_eq!(deletion_cookie.max_age().unwrap(), Duration::ZERO);
assert_eq!(deletion_cookie.domain().unwrap(), "localhost");
Ok(())
}

View File

@ -0,0 +1,93 @@
use std::collections::HashMap;
use actix_session::{
storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError},
Session, SessionMiddleware,
};
use actix_web::{
body::MessageBody,
cookie::{time::Duration, Key},
dev::Service,
http::StatusCode,
test, web, App, Responder,
};
use anyhow::Error;
#[actix_web::test]
async fn errors_are_opaque() {
let signing_key = Key::generate();
let app = test::init_service(
App::new()
.wrap(SessionMiddleware::new(MockStore, signing_key.clone()))
.route("/create_session", web::post().to(create_session))
.route(
"/load_session_with_error",
web::post().to(load_session_with_error),
),
)
.await;
let req = test::TestRequest::post()
.uri("/create_session")
.to_request();
let response = test::call_service(&app, req).await;
let session_cookie = response.response().cookies().next().unwrap();
let req = test::TestRequest::post()
.cookie(session_cookie)
.uri("/load_session_with_error")
.to_request();
let response = app.call(req).await.unwrap_err().error_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(response.into_body().try_into_bytes().unwrap().is_empty());
}
struct MockStore;
impl SessionStore for MockStore {
async fn load(
&self,
_session_key: &SessionKey,
) -> Result<Option<HashMap<String, String>>, LoadError> {
Err(LoadError::Other(anyhow::anyhow!(
"My error full of implementation details"
)))
}
async fn save(
&self,
_session_state: HashMap<String, String>,
_ttl: &Duration,
) -> Result<SessionKey, SaveError> {
Ok("random_value".to_string().try_into().unwrap())
}
async fn update(
&self,
_session_key: SessionKey,
_session_state: HashMap<String, String>,
_ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
#![allow(clippy::diverging_sub_expression)]
unimplemented!()
}
async fn update_ttl(&self, _session_key: &SessionKey, _ttl: &Duration) -> Result<(), Error> {
#![allow(clippy::diverging_sub_expression)]
unimplemented!()
}
async fn delete(&self, _session_key: &SessionKey) -> Result<(), Error> {
#![allow(clippy::diverging_sub_expression)]
unimplemented!()
}
}
async fn create_session(session: Session) -> impl Responder {
session.insert("user_id", "id").unwrap();
"Created"
}
async fn load_session_with_error(_session: Session) -> impl Responder {
"Loaded"
}

View File

@ -0,0 +1,151 @@
use actix_session::{SessionExt, SessionStatus};
use actix_web::{test, HttpResponse};
#[actix_web::test]
async fn session() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
session.insert("key", "value").unwrap();
let res = session.get::<String>("key").unwrap();
assert_eq!(res, Some("value".to_string()));
session.insert("key2", "value2").unwrap();
session.remove("key");
let res = req.into_response(HttpResponse::Ok().finish());
let state: Vec<_> = res.get_session().entries().clone().into_iter().collect();
assert_eq!(
state.as_slice(),
[("key2".to_string(), "\"value2\"".to_string())]
);
}
#[actix_web::test]
async fn get_session() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
session.insert("key", true).unwrap();
let res = session.get("key").unwrap();
assert_eq!(res, Some(true));
}
#[actix_web::test]
async fn get_session_from_request_head() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
session.insert("key", 10).unwrap();
let res = session.get::<u32>("key").unwrap();
assert_eq!(res, Some(10));
}
#[actix_web::test]
async fn purge_session() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
assert_eq!(session.status(), SessionStatus::Unchanged);
session.purge();
assert_eq!(session.status(), SessionStatus::Purged);
}
#[actix_web::test]
async fn renew_session() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
assert_eq!(session.status(), SessionStatus::Unchanged);
session.renew();
assert_eq!(session.status(), SessionStatus::Renewed);
}
#[actix_web::test]
async fn session_entries() {
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();
let map = session.entries();
map.contains_key("test_str");
map.contains_key("test_num");
}
#[actix_web::test]
async fn session_contains_key() {
let req = test::TestRequest::default().to_srv_request();
let session = req.get_session();
session.insert("test_str", "val").unwrap();
session.insert("test_str", 1).unwrap();
assert!(session.contains_key("test_str"));
assert!(!session.contains_key("test_num"));
}
#[actix_web::test]
async fn insert_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session();
session.insert("test_val", "val").unwrap();
assert_eq!(session.status(), SessionStatus::Changed);
session.renew();
assert_eq!(session.status(), SessionStatus::Renewed);
session.insert("test_val1", "val1").unwrap();
assert_eq!(session.status(), SessionStatus::Renewed);
}
#[actix_web::test]
async fn update_session() {
let session = test::TestRequest::default().to_srv_request().get_session();
session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.status(), SessionStatus::Unchanged);
session.insert("test_val", 0).unwrap();
assert_eq!(session.status(), SessionStatus::Changed);
session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(1));
session.update("test_val", |c: u32| c + 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(2));
}
#[actix_web::test]
async fn update_or_session() {
let session = test::TestRequest::default().to_srv_request().get_session();
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
assert_eq!(session.status(), SessionStatus::Changed);
assert_eq!(session.get("test_val").unwrap(), Some(1));
session.update_or("test_val", 1, |c: u32| c + 1).unwrap();
assert_eq!(session.get("test_val").unwrap(), Some(2));
}
#[actix_web::test]
async fn remove_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session();
session.insert("test_val", "val").unwrap();
session.remove("test_val").unwrap();
assert_eq!(session.status(), SessionStatus::Changed);
session.renew();
session.insert("test_val", "val").unwrap();
session.remove("test_val").unwrap();
assert_eq!(session.status(), SessionStatus::Renewed);
}
#[actix_web::test]
async fn clear_session_after_renew() {
let session = test::TestRequest::default().to_srv_request().get_session();
session.clear();
assert_eq!(session.status(), SessionStatus::Changed);
session.renew();
assert_eq!(session.status(), SessionStatus::Renewed);
session.clear();
assert_eq!(session.status(), SessionStatus::Renewed);
}

40
actix-settings/CHANGES.md Normal file
View File

@ -0,0 +1,40 @@
# Changes
## 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`.
- Remove `AtResult` type alias.
- Update `toml` dependency to `0.8`.
- Remove `ioe` dependency; `std::io::Error` is now used directly.
- Remove `Clone` implementation for `Error`.
- Implement `Display` for `Error`.
- Implement std's `Error` for `Error`.
- Minimum supported Rust version (MSRV) is now 1.68.
## 0.6.0
- Update Actix Web dependencies to v4 ecosystem.
- Rename `actix.ssl` settings object to `actix.tls`.
- `NoSettings` is now marked `#[non_exhaustive]`.
## 0.5.2
- Adopted into @actix org from <https://github.com/jjpe/actix-settings>.

37
actix-settings/Cargo.toml Normal file
View File

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

View File

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

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

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

31
actix-settings/README.md Normal file
View File

@ -0,0 +1,31 @@
# actix-settings
> Easily manage Actix Web's settings from a TOML file and environment variables.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-settings?label=latest)](https://crates.io/crates/actix-settings)
[![Documentation](https://docs.rs/actix-settings/badge.svg?version=0.8.0)](https://docs.rs/actix-settings/0.8.0)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-settings)
[![Dependency Status](https://deps.rs/crate/actix-settings/0.8.0/status.svg)](https://deps.rs/crate/actix-settings/0.8.0)
<!-- prettier-ignore-end -->
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-settings)
- [Usage Example][usage]
- Minimum Supported Rust Version (MSRV): 1.57
### Custom Settings
There is a way to extend the available settings. This can be used to combine the settings provided by Actix Web and those provided by application server built using `actix`.
Have a look at [the usage example][usage] to see how.
## Special Thanks
This crate was made possible by support from Accept B.V and [@jjpe].
[usage]: https://github.com/actix/actix-extras/blob/master/actix-settings/examples/actix.rs
[@jjpe]: https://github.com/jjpe

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