Compare commits
No commits in common. "79ff05d91dcf2c7a457ab7832baae8312b263b10" and "eb295d5b25d227d2d4d635ff6a5902d860719482" have entirely different histories.
79ff05d91d
...
eb295d5b25
12 changed files with 322 additions and 282 deletions
|
@ -1,25 +0,0 @@
|
||||||
name: Rust CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rust_ci:
|
|
||||||
name: Rust CI
|
|
||||||
runs-on: debian
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: https://codeberg.org/TheAwiteb/rust-action@v1.75
|
|
||||||
- name: Check MSRV
|
|
||||||
run: cargo +1.75 build
|
|
||||||
- name: Build the source code
|
|
||||||
run: cargo build
|
|
||||||
- name: Check the code format
|
|
||||||
run: cargo fmt -- --check
|
|
||||||
- name: Run cargo-check
|
|
||||||
run: cargo check
|
|
||||||
- name: Run cargo-clippy
|
|
||||||
run: cargo clippy -- -D warnings
|
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
github: "TheAwiteb"
|
34
.github/workflows/auto_close_pr.yml
vendored
34
.github/workflows/auto_close_pr.yml
vendored
|
@ -1,34 +0,0 @@
|
||||||
name: Auto close PR
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close_pr:
|
|
||||||
name: Auto close PR
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Send close comment
|
|
||||||
run: |
|
|
||||||
curl -L \
|
|
||||||
-X POST \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer $PAT" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.number }}/comments \
|
|
||||||
-d '{"body":"${{ env.BODY }}"}'
|
|
||||||
env:
|
|
||||||
PAT: ${{ secrets.PAT }}
|
|
||||||
BODY: This repository is mirror only and you cannot create a pull request for it. Please open your PR in https://git.4rs.nl/awiteb/salvo-captcha
|
|
||||||
- name: Close the PR
|
|
||||||
run: |
|
|
||||||
curl -L \
|
|
||||||
-X PATCH \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer $PAT" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.number }} \
|
|
||||||
-d '{"state":"closed"}'
|
|
||||||
env:
|
|
||||||
PAT: ${{ secrets.PAT }}
|
|
35
.github/workflows/cd.yml
vendored
Normal file
35
.github/workflows/cd.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v[0-9]+.[0-9]+.[0-9]+
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Build Changelog 🏗
|
||||||
|
id: changelog
|
||||||
|
uses: mikepenz/release-changelog-builder-action@v4.0.0
|
||||||
|
with:
|
||||||
|
configuration: "./.github/config/changelog-builder-action.json"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
- name: Create Release 🖋
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'pre') }}
|
||||||
|
body: ${{steps.changelog.outputs.changelog}}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
- name: Publish to crates.io 🚀
|
||||||
|
uses: katyo/publish-crates@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PAT }}
|
||||||
|
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
44
.github/workflows/ci.yml
vendored
Normal file
44
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Rust build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo build --workspace --all-features --all-targets
|
||||||
|
rustfmt:
|
||||||
|
name: Rust format
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo fmt -- --check
|
||||||
|
check:
|
||||||
|
name: Rust check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo check --workspace --all-features --all-targets
|
||||||
|
clippy:
|
||||||
|
name: Rust clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo clippy --workspace --all-features -- -D warnings
|
||||||
|
test:
|
||||||
|
name: Rust test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo test --workspace --all-features
|
22
Cargo.toml
22
Cargo.toml
|
@ -14,21 +14,19 @@ categories = ["web-programming", "network-programming"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cacache = { version = "13", default-features = false, features = ["tokio-runtime", "mmap"], optional = true }
|
async-trait = "0.1.77"
|
||||||
|
cacache = { version = "12.0.0", default-features = false, features = ["tokio-runtime", "mmap"], optional = true }
|
||||||
captcha = { version = "0.0.9", default-features = false }
|
captcha = { version = "0.0.9", default-features = false }
|
||||||
log = "0.4"
|
easy-ext = "1.0.1"
|
||||||
salvo_core = { version = ">= 0.65, < 0.69", default-features = false }
|
log = "0.4.20"
|
||||||
uuid = { version = "1.7", features = ["v4"], optional = true }
|
salvo_core = "^ 0.65"
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
cacache-storage = ["dep:cacache", "dep:uuid"]
|
cacache-storage = ["dep:cacache", "dep:uuid"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.9"
|
tempfile = ">= 3.9"
|
||||||
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = ">= 1.35", features = ["macros", "rt-multi-thread"] }
|
||||||
base64 = "0.21"
|
base64 = ">= 0.21"
|
||||||
salvo = { version = ">= 0.65, < 0.69", default-features = false, features = ["server", "http1","http2", "affix"] }
|
salvo = { version = ">= 0.65", features = ["affix"] }
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "simple_login"
|
|
||||||
required-features = ["cacache-storage"]
|
|
46
README.md
46
README.md
|
@ -8,8 +8,7 @@ A captcha middleware for [salvo](salvo.rs) framework. It uses [`captcha`](https:
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Add to your project
|
### Add to your project
|
||||||
|
|
||||||
First, add the following to your `Cargo.toml`:
|
First, add the following to your `Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
@ -23,41 +22,36 @@ Or use [cargo-add](https://doc.rust-lang.org/cargo/commands/cargo-add.html) to a
|
||||||
$ cargo add salvo-captcha
|
$ cargo add salvo-captcha
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
### Usage
|
||||||
|
|
||||||
See the [examples](examples) directory for a complete example.
|
See the [examples](examples) directory for a complete example.
|
||||||
|
|
||||||
## Storage
|
|
||||||
|
|
||||||
The storage of the captcha is handled by a [`CaptchaStore`] trait. You can implement your own storage or use the [`cacache-rs`] by enabling the `cacache-storage` feature.
|
### Storage
|
||||||
|
The storage of the captcha is handled by a [`CaptchaStore`] trait. You can implement your own storage or use the `cacache-rs` by enabling the `cacache-storage` feature.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
salvo-captcha = { version = "0.1", features = ["cacache-storage"] }
|
salvo-captcha = { version = "0.1", features = ["cacache-storage"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
## Captcha name and difficulty
|
### Captcha name and difficulty
|
||||||
|
In this table you can see the different between the difficulties and the name of the captcha.
|
||||||
|
|
||||||
In this table, you can see the difference between the difficulties and the name of the captcha.
|
| Name | Difficulty | Image |
|
||||||
|
|------|------------|-------|
|
||||||
| Name | Easy | Medium | Hard |
|
| `Amelia` | Easy | ![Simple](https://i.suar.me/1JaxG/s) |
|
||||||
| :----: | :----------------------------------: | :----------------------------------: | :----------------------------------: |
|
| `Amelia` | Medium | ![Simple](https://i.suar.me/l7zBl/s) |
|
||||||
| Amelia | ![Simple](https://i.suar.me/1JaxG/s) | ![Simple](https://i.suar.me/l7zBl/s) | ![Simple](https://i.suar.me/qXAlx/s) |
|
| `Amelia` | Hard | ![Simple](https://i.suar.me/qXAlx/s) |
|
||||||
| Lucy | ![Simple](https://i.suar.me/edwBG/s) | ![Simple](https://i.suar.me/NJmg0/s) | ![Simple](https://i.suar.me/OJK7M/s) |
|
| `Lucy` | Easy | ![Simple](https://i.suar.me/edwBG/s) |
|
||||||
| Mila | ![Simple](https://i.suar.me/dO78z/s) | ![Simple](https://i.suar.me/PXBwK/s) | ![Simple](https://i.suar.me/8edgE/s) |
|
| `Lucy` | Medium | ![Simple](https://i.suar.me/NJmg0/s) |
|
||||||
|
| `Lucy` | Hard | ![Simple](https://i.suar.me/OJK7M/s) |
|
||||||
## Mirrors
|
| `Mila` | Easy | ![Simple](https://i.suar.me/dO78z/s) |
|
||||||
|
| `Mila` | Medium | ![Simple](https://i.suar.me/PXBwK/s) |
|
||||||
- Github (https://github.com/TheAwiteb/salvo-captcha)
|
| `Mila` | Hard | ![Simple](https://i.suar.me/8edgE/s) |
|
||||||
- Codeberg (https://codeberg.org/awiteb/salvo-captcha)
|
|
||||||
|
|
||||||
### Main Repository
|
|
||||||
|
|
||||||
- My Git (https://git.4rs.nl/awiteb/salvo-captcha)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
|
### License
|
||||||
This project is licensed under the MIT license for more details see [LICENSE](LICENSE) or http://opensource.org/licenses/MIT.
|
This project is licensed under the MIT license for more details see [LICENSE](LICENSE) or http://opensource.org/licenses/MIT.
|
||||||
|
|
||||||
|
|
||||||
[`CaptchaStore`]: https://docs.rs/salvo_captcha/0.1.0/salvo_captcha/trait.CaptchaStore.html
|
[`CaptchaStore`]: https://docs.rs/salvo_captcha/0.1.0/salvo_captcha/trait.CaptchaStore.html
|
||||||
[`cacache-rs`]: https://github.com/zkat/cacache-rs
|
[cacache-rs]: https://github.com/zkat/cacache-rs
|
|
@ -3,6 +3,15 @@
|
||||||
// You can see a video of this example here:
|
// You can see a video of this example here:
|
||||||
//
|
//
|
||||||
// Run the example with `cargo run --example simple_login --features cacache-storage`
|
// Run the example with `cargo run --example simple_login --features cacache-storage`
|
||||||
|
//
|
||||||
|
// Or set up a crate for it, with the following `Cargo.toml`:
|
||||||
|
// ```toml
|
||||||
|
// [dependencies]
|
||||||
|
// base64 = ">= 0.21"
|
||||||
|
// salvo = { version = ">= 0.65", features = ["affix"] }
|
||||||
|
// salvo-captcha = { version = ">= 0.1", features = ["cacache-storage"] }
|
||||||
|
// tokio = { version = ">= 1.35", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
|
// ```
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
@ -10,6 +19,9 @@ use base64::{engine::GeneralPurpose, Engine};
|
||||||
use salvo::prelude::*;
|
use salvo::prelude::*;
|
||||||
use salvo_captcha::*;
|
use salvo_captcha::*;
|
||||||
|
|
||||||
|
// The type of the captcha
|
||||||
|
type MyCaptcha = Captcha<CacacheStorage, CaptchaFormFinder<String, String>>;
|
||||||
|
|
||||||
// To convert the image to base64, to show it in the browser
|
// To convert the image to base64, to show it in the browser
|
||||||
const BASE_64_ENGINE: GeneralPurpose = GeneralPurpose::new(
|
const BASE_64_ENGINE: GeneralPurpose = GeneralPurpose::new(
|
||||||
&base64::alphabet::STANDARD,
|
&base64::alphabet::STANDARD,
|
||||||
|
@ -17,7 +29,7 @@ const BASE_64_ENGINE: GeneralPurpose = GeneralPurpose::new(
|
||||||
);
|
);
|
||||||
|
|
||||||
#[handler]
|
#[handler]
|
||||||
async fn index(res: &mut Response, depot: &mut Depot) {
|
async fn index(_req: &mut Request, res: &mut Response, depot: &mut Depot) {
|
||||||
// Get the captcha from the depot
|
// Get the captcha from the depot
|
||||||
let captcha_storage = depot.obtain::<Arc<CacacheStorage>>().unwrap();
|
let captcha_storage = depot.obtain::<Arc<CacacheStorage>>().unwrap();
|
||||||
|
|
||||||
|
@ -39,7 +51,7 @@ async fn index(res: &mut Response, depot: &mut Depot) {
|
||||||
#[handler]
|
#[handler]
|
||||||
async fn auth(req: &mut Request, res: &mut Response, depot: &mut Depot) {
|
async fn auth(req: &mut Request, res: &mut Response, depot: &mut Depot) {
|
||||||
// Get the captcha state from the depot, where we can know if the captcha is passed
|
// Get the captcha state from the depot, where we can know if the captcha is passed
|
||||||
let captcha_state = depot.get_captcha_state();
|
let captcha_state = depot.get_captcha_state().unwrap();
|
||||||
// Not important, just for demo
|
// Not important, just for demo
|
||||||
let Some(username) = req.form::<String>("username").await else {
|
let Some(username) = req.form::<String>("username").await else {
|
||||||
res.status_code(StatusCode::BAD_REQUEST);
|
res.status_code(StatusCode::BAD_REQUEST);
|
||||||
|
@ -64,7 +76,7 @@ async fn auth(req: &mut Request, res: &mut Response, depot: &mut Depot) {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let captcha_middleware = Captcha::new(
|
let captcha_middleware = MyCaptcha::new(
|
||||||
CacacheStorage::new("./captcha-cache"),
|
CacacheStorage::new("./captcha-cache"),
|
||||||
CaptchaFormFinder::new(),
|
CaptchaFormFinder::new(),
|
||||||
)
|
)
|
||||||
|
@ -103,7 +115,6 @@ async fn main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let acceptor = TcpListener::new(("127.0.0.1", 5800)).bind().await;
|
let acceptor = TcpListener::new(("127.0.0.1", 5800)).bind().await;
|
||||||
println!("Starting server on http://127.0.0.1:5800");
|
|
||||||
Server::new(acceptor).serve(router).await;
|
Server::new(acceptor).serve(router).await;
|
||||||
captcha_cleaner.await.ok();
|
captcha_cleaner.await.ok();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
// Copyright (c) 2024, Awiteb <a@4rs.nl>
|
|
||||||
// A captcha middleware for Salvo framework.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
use crate::{CaptchaDifficulty, CaptchaName, CaptchaStorage};
|
use crate::{CaptchaDifficulty, CaptchaName, CaptchaStorage};
|
||||||
|
|
||||||
/// Captcha generator, used to generate a new captcha image. This trait are implemented for all [`CaptchaStorage`].
|
/// Captcha generator, used to generate a new captcha image. This trait are implemented for all [`CaptchaStorage`].
|
||||||
|
@ -22,7 +11,7 @@ pub trait CaptchaGenerator: CaptchaStorage {
|
||||||
&self,
|
&self,
|
||||||
name: CaptchaName,
|
name: CaptchaName,
|
||||||
difficulty: CaptchaDifficulty,
|
difficulty: CaptchaDifficulty,
|
||||||
) -> impl std::future::Future<Output = Result<Option<(String, Vec<u8>)>, Self::Error>> + Send
|
) -> impl std::future::Future<Output = Result<Option<(Self::Token, Vec<u8>)>, Self::Error>> + Send
|
||||||
{
|
{
|
||||||
async {
|
async {
|
||||||
let Some((captcha_answer, captcha_image)) =
|
let Some((captcha_answer, captcha_image)) =
|
||||||
|
@ -31,7 +20,7 @@ pub trait CaptchaGenerator: CaptchaStorage {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = self.store_answer(captcha_answer).await?;
|
let token = self.store_answer(captcha_answer.into()).await?;
|
||||||
Ok(Some((token, captcha_image)))
|
Ok(Some((token, captcha_image)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
258
src/finder.rs
258
src/finder.rs
|
@ -9,37 +9,49 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use salvo_core::http::header::HeaderName;
|
use salvo_core::http::header::HeaderName;
|
||||||
use salvo_core::http::Request;
|
use salvo_core::http::Request;
|
||||||
|
|
||||||
/// Trait to find the captcha token and answer from the request.
|
/// Trait to find the captcha token and answer from the request.
|
||||||
pub trait CaptchaFinder: Send + Sync {
|
pub trait CaptchaFinder: Send + Sync {
|
||||||
|
/// The token type
|
||||||
|
type Token: TryFrom<String> + Sync + Send;
|
||||||
|
/// The answer type
|
||||||
|
type Answer: TryFrom<String> + Sync + Send;
|
||||||
|
|
||||||
|
/// The token error type
|
||||||
|
type TError: std::fmt::Debug + Send;
|
||||||
|
/// The answer error type
|
||||||
|
type AError: std::fmt::Debug + Send;
|
||||||
|
|
||||||
/// Find the captcha token from the request.
|
/// Find the captcha token from the request.
|
||||||
///
|
///
|
||||||
/// ### Returns
|
/// Return [`None`] if the request does not contain a captcha token. An error is returned if the token is invalid format.
|
||||||
/// - None: If the token is not found
|
|
||||||
/// - Some(None): If the token is found but is invalid (e.g. not a valid string)
|
|
||||||
/// - Some(Some(token)): If the token is found
|
|
||||||
fn find_token(
|
fn find_token(
|
||||||
&self,
|
&self,
|
||||||
req: &mut Request,
|
req: &mut Request,
|
||||||
) -> impl std::future::Future<Output = Option<Option<String>>> + std::marker::Send;
|
) -> impl std::future::Future<Output = Result<Option<Self::Token>, Self::TError>> + std::marker::Send;
|
||||||
|
|
||||||
/// Find the captcha answer from the request.
|
/// Find the captcha answer from the request.
|
||||||
///
|
///
|
||||||
/// ### Returns
|
/// Return [`None`] if the request does not contain a captcha answer. An error is returned if the answer is invalid format.
|
||||||
/// - None: If the answer is not found
|
|
||||||
/// - Some(None): If the answer is found but is invalid (e.g. not a valid string)
|
|
||||||
/// - Some(Some(answer)): If the answer is found
|
|
||||||
fn find_answer(
|
fn find_answer(
|
||||||
&self,
|
&self,
|
||||||
req: &mut Request,
|
req: &mut Request,
|
||||||
) -> impl std::future::Future<Output = Option<Option<String>>> + std::marker::Send;
|
) -> impl std::future::Future<Output = Result<Option<Self::Answer>, Self::AError>> + std::marker::Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the captcha token and answer from the header
|
/// Find the captcha token and answer from the header
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CaptchaHeaderFinder {
|
pub struct CaptchaHeaderFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
|
phantom: PhantomData<(T, A)>,
|
||||||
|
|
||||||
/// The header name of the captcha token
|
/// The header name of the captcha token
|
||||||
///
|
///
|
||||||
/// Default: "x-captcha-token"
|
/// Default: "x-captcha-token"
|
||||||
|
@ -53,7 +65,13 @@ pub struct CaptchaHeaderFinder {
|
||||||
|
|
||||||
/// Find the captcha token and answer from the form
|
/// Find the captcha token and answer from the form
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CaptchaFormFinder {
|
pub struct CaptchaFormFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
|
phantom: PhantomData<(T, A)>,
|
||||||
|
|
||||||
/// The form name of the captcha token
|
/// The form name of the captcha token
|
||||||
///
|
///
|
||||||
/// Default: "captcha_token"
|
/// Default: "captcha_token"
|
||||||
|
@ -65,7 +83,11 @@ pub struct CaptchaFormFinder {
|
||||||
pub answer_name: String,
|
pub answer_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptchaHeaderFinder {
|
impl<T, A> CaptchaHeaderFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
/// Create a new CaptchaHeaderFinder
|
/// Create a new CaptchaHeaderFinder
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
@ -84,7 +106,11 @@ impl CaptchaHeaderFinder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptchaFormFinder {
|
impl<T, A> CaptchaFormFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
/// Create a new CaptchaFormFinder
|
/// Create a new CaptchaFormFinder
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
|
@ -103,57 +129,99 @@ impl CaptchaFormFinder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CaptchaHeaderFinder {
|
impl<T, A> Default for CaptchaHeaderFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
/// Create a default CaptchaHeaderFinder with:
|
/// Create a default CaptchaHeaderFinder with:
|
||||||
/// - token_header: "x-captcha-token"
|
/// - token_header: "x-captcha-token"
|
||||||
/// - answer_header: "x-captcha-answer"
|
/// - answer_header: "x-captcha-answer"
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
phantom: PhantomData,
|
||||||
token_header: HeaderName::from_static("x-captcha-token"),
|
token_header: HeaderName::from_static("x-captcha-token"),
|
||||||
answer_header: HeaderName::from_static("x-captcha-answer"),
|
answer_header: HeaderName::from_static("x-captcha-answer"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CaptchaFormFinder {
|
impl<T, A> Default for CaptchaFormFinder<T, A>
|
||||||
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
{
|
||||||
/// Create a default CaptchaFormFinder with:
|
/// Create a default CaptchaFormFinder with:
|
||||||
/// - token_name: "captcha_token"
|
/// - token_name: "captcha_token"
|
||||||
/// - answer_name: "captcha_answer"
|
/// - answer_name: "captcha_answer"
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
phantom: PhantomData,
|
||||||
token_name: "captcha_token".to_string(),
|
token_name: "captcha_token".to_string(),
|
||||||
answer_name: "captcha_answer".to_string(),
|
answer_name: "captcha_answer".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptchaFinder for CaptchaHeaderFinder {
|
impl<T, A> CaptchaFinder for CaptchaHeaderFinder<T, A>
|
||||||
async fn find_token(&self, req: &mut Request) -> Option<Option<String>> {
|
where
|
||||||
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
<T as TryFrom<String>>::Error: Send,
|
||||||
|
<T as TryFrom<String>>::Error: std::fmt::Debug,
|
||||||
|
<A as TryFrom<String>>::Error: Send,
|
||||||
|
<A as TryFrom<String>>::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
type Token = T;
|
||||||
|
type Answer = A;
|
||||||
|
|
||||||
|
type TError = <T as TryFrom<String>>::Error;
|
||||||
|
type AError = <A as TryFrom<String>>::Error;
|
||||||
|
|
||||||
|
async fn find_token(&self, req: &mut Request) -> Result<Option<Self::Token>, Self::TError> {
|
||||||
req.headers()
|
req.headers()
|
||||||
.get(&self.token_header)
|
.get(&self.token_header)
|
||||||
.map(|t| t.to_str().map(ToString::to_string).ok())
|
.and_then(|t| t.to_str().ok())
|
||||||
|
.map(|t| Self::Token::try_from(t.to_string()))
|
||||||
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_answer(&self, req: &mut Request) -> Option<Option<String>> {
|
async fn find_answer(&self, req: &mut Request) -> Result<Option<Self::Answer>, Self::AError> {
|
||||||
req.headers()
|
req.headers()
|
||||||
.get(&self.answer_header)
|
.get(&self.answer_header)
|
||||||
.map(|a| a.to_str().map(ToString::to_string).ok())
|
.and_then(|a| a.to_str().ok())
|
||||||
|
.map(|a| Self::Answer::try_from(a.to_string()))
|
||||||
|
.transpose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptchaFinder for CaptchaFormFinder {
|
impl<T, A> CaptchaFinder for CaptchaFormFinder<T, A>
|
||||||
async fn find_token(&self, req: &mut Request) -> Option<Option<String>> {
|
where
|
||||||
req.form_data()
|
T: TryFrom<String> + Sync + Send,
|
||||||
|
A: TryFrom<String> + Sync + Send,
|
||||||
|
<T as TryFrom<String>>::Error: Send,
|
||||||
|
<T as TryFrom<String>>::Error: std::fmt::Debug,
|
||||||
|
<A as TryFrom<String>>::Error: Send,
|
||||||
|
<A as TryFrom<String>>::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
type Token = T;
|
||||||
|
type Answer = A;
|
||||||
|
|
||||||
|
type TError = <T as TryFrom<String>>::Error;
|
||||||
|
type AError = <A as TryFrom<String>>::Error;
|
||||||
|
|
||||||
|
async fn find_token(&self, req: &mut Request) -> Result<Option<Self::Token>, Self::TError> {
|
||||||
|
req.form::<String>(&self.token_name)
|
||||||
.await
|
.await
|
||||||
.map(|form| form.fields.get(&self.token_name).cloned())
|
.map(|t| Self::Token::try_from(t.to_string()))
|
||||||
.ok()
|
.transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_answer(&self, req: &mut Request) -> Option<Option<String>> {
|
async fn find_answer(&self, req: &mut Request) -> Result<Option<Self::Answer>, Self::AError> {
|
||||||
req.form_data()
|
req.form::<String>(&self.answer_name)
|
||||||
.await
|
.await
|
||||||
.map(|form| form.fields.get(&self.answer_name).cloned())
|
.map(|a| Self::Answer::try_from(a.to_string()))
|
||||||
.ok()
|
.transpose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,129 +234,77 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_header_finder() {
|
async fn test_captcha_header_finder() {
|
||||||
let finder = CaptchaHeaderFinder::new();
|
let finder = CaptchaHeaderFinder::<String, String>::new();
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
let headers = req.headers_mut();
|
let headers = req.headers_mut();
|
||||||
|
let token = uuid::Uuid::new_v4();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
HeaderName::from_static("x-captcha-token"),
|
HeaderName::from_static("x-captcha-token"),
|
||||||
HeaderValue::from_str("token").unwrap(),
|
HeaderValue::from_str(&token.to_string()).unwrap(),
|
||||||
);
|
);
|
||||||
headers.insert(
|
headers.insert(
|
||||||
HeaderName::from_static("x-captcha-answer"),
|
HeaderName::from_static("x-captcha-answer"),
|
||||||
HeaderValue::from_static("answer"),
|
HeaderValue::from_static("answer"),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
finder.find_token(&mut req).await,
|
finder.find_token(&mut req).await,
|
||||||
Some(Some("token".to_owned()))
|
Ok(Some(token.to_string()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
finder.find_answer(&mut req).await,
|
finder.find_answer(&mut req).await,
|
||||||
Some(Some("answer".to_owned()))
|
Ok(Some(a)) if a == *"answer"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_header_finder_customized() {
|
async fn test_captcha_header_finder_customized() {
|
||||||
let finder = CaptchaHeaderFinder::new()
|
let finder = CaptchaHeaderFinder::<String, String>::new()
|
||||||
.token_header(HeaderName::from_static("token"))
|
.token_header(HeaderName::from_static("token"))
|
||||||
.answer_header(HeaderName::from_static("answer"));
|
.answer_header(HeaderName::from_static("answer"));
|
||||||
|
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
let headers = req.headers_mut();
|
let headers = req.headers_mut();
|
||||||
|
let token = uuid::Uuid::new_v4();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
HeaderName::from_static("token"),
|
HeaderName::from_static("token"),
|
||||||
HeaderValue::from_str("token").unwrap(),
|
HeaderValue::from_str(&token.to_string()).unwrap(),
|
||||||
);
|
);
|
||||||
headers.insert(
|
headers.insert(
|
||||||
HeaderName::from_static("answer"),
|
HeaderName::from_static("answer"),
|
||||||
HeaderValue::from_static("answer"),
|
HeaderValue::from_static("answer"),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
finder.find_token(&mut req).await,
|
finder.find_token(&mut req).await,
|
||||||
Some(Some("token".to_owned()))
|
Ok(Some(token.to_string()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
finder.find_answer(&mut req).await,
|
finder.find_answer(&mut req).await,
|
||||||
Some(Some("answer".to_owned()))
|
Ok(Some(a)) if a == *"answer"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_header_finder_none() {
|
async fn test_captcha_header_finder_none() {
|
||||||
let finder = CaptchaHeaderFinder::new();
|
let finder = CaptchaHeaderFinder::<String, String>::new();
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, Some(None));
|
assert_eq!(finder.find_token(&mut req).await, Ok(None));
|
||||||
assert_eq!(finder.find_answer(&mut req).await, Some(None));
|
assert_eq!(finder.find_answer(&mut req).await, Ok(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_header_finder_customized_none() {
|
async fn test_captcha_header_finder_customized_none() {
|
||||||
let finder = CaptchaHeaderFinder::new()
|
let finder = CaptchaHeaderFinder::<String, String>::new()
|
||||||
.token_header(HeaderName::from_static("token"))
|
.token_header(HeaderName::from_static("token"))
|
||||||
.answer_header(HeaderName::from_static("answer"));
|
.answer_header(HeaderName::from_static("answer"));
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
let headers = req.headers_mut();
|
|
||||||
|
|
||||||
headers.insert(
|
assert_eq!(finder.find_token(&mut req).await, Ok(None));
|
||||||
HeaderName::from_static("token"),
|
assert_eq!(finder.find_answer(&mut req).await, Ok(None));
|
||||||
HeaderValue::from_str("token").unwrap(),
|
|
||||||
);
|
|
||||||
headers.insert(
|
|
||||||
HeaderName::from_static("answer"),
|
|
||||||
HeaderValue::from_static("answer"),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
finder.find_token(&mut req).await,
|
|
||||||
Some(Some("token".to_owned()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
finder.find_answer(&mut req).await,
|
|
||||||
Some(Some("answer".to_owned()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, Some(None));
|
|
||||||
assert_eq!(finder.find_answer(&mut req).await, Some(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_captcha_header_finder_invalid_string() {
|
|
||||||
let finder = CaptchaHeaderFinder::new()
|
|
||||||
.token_header(HeaderName::from_static("token"))
|
|
||||||
.answer_header(HeaderName::from_static("answer"));
|
|
||||||
let mut req = Request::default();
|
|
||||||
let headers = req.headers_mut();
|
|
||||||
|
|
||||||
headers.insert(
|
|
||||||
HeaderName::from_static("token"),
|
|
||||||
HeaderValue::from_str("مرحبا").unwrap(),
|
|
||||||
);
|
|
||||||
headers.insert(
|
|
||||||
HeaderName::from_static("answer"),
|
|
||||||
HeaderValue::from_static("مرحبا"),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
finder.find_token(&mut req).await,
|
|
||||||
Some(Some("token".to_owned()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
finder.find_answer(&mut req).await,
|
|
||||||
Some(Some("answer".to_owned()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, None);
|
|
||||||
assert_eq!(finder.find_answer(&mut req).await, None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_form_finder() {
|
async fn test_captcha_form_finder() {
|
||||||
let finder = CaptchaFormFinder::new();
|
let finder = CaptchaFormFinder::<String, String>::new();
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
*req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into());
|
*req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into());
|
||||||
let headers = req.headers_mut();
|
let headers = req.headers_mut();
|
||||||
|
@ -299,17 +315,17 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
finder.find_token(&mut req).await,
|
finder.find_token(&mut req).await,
|
||||||
Some(Some("token".to_owned()))
|
Ok(Some("token".to_string()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
finder.find_answer(&mut req).await,
|
finder.find_answer(&mut req).await,
|
||||||
Some(Some("answer".to_owned()))
|
Ok(Some(a)) if a == *"answer"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_form_finder_customized() {
|
async fn test_captcha_form_finder_customized() {
|
||||||
let finder = CaptchaFormFinder::new()
|
let finder = CaptchaFormFinder::<String, String>::new()
|
||||||
.token_name("token".to_string())
|
.token_name("token".to_string())
|
||||||
.answer_name("answer".to_string());
|
.answer_name("answer".to_string());
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
|
@ -322,17 +338,17 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
finder.find_token(&mut req).await,
|
finder.find_token(&mut req).await,
|
||||||
Some(Some("token".to_owned()))
|
Ok(Some("token".to_string()))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert!(matches!(
|
||||||
finder.find_answer(&mut req).await,
|
finder.find_answer(&mut req).await,
|
||||||
Some(Some("answer".to_owned()))
|
Ok(Some(a)) if a == *"answer"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_form_finder_none() {
|
async fn test_captcha_form_finder_none() {
|
||||||
let finder = CaptchaFormFinder::new();
|
let finder = CaptchaFormFinder::<String, String>::new();
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
*req.body_mut() = ReqBody::Once("".into());
|
*req.body_mut() = ReqBody::Once("".into());
|
||||||
let headers = req.headers_mut();
|
let headers = req.headers_mut();
|
||||||
|
@ -341,13 +357,13 @@ mod tests {
|
||||||
HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(),
|
HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, Some(None));
|
assert_eq!(finder.find_token(&mut req).await, Ok(None));
|
||||||
assert_eq!(finder.find_answer(&mut req).await, Some(None));
|
assert_eq!(finder.find_answer(&mut req).await, Ok(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_form_finder_customized_none() {
|
async fn test_captcha_form_finder_customized_none() {
|
||||||
let finder = CaptchaFormFinder::new()
|
let finder = CaptchaFormFinder::<String, String>::new()
|
||||||
.token_name("token".to_string())
|
.token_name("token".to_string())
|
||||||
.answer_name("answer".to_string());
|
.answer_name("answer".to_string());
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
|
@ -358,13 +374,13 @@ mod tests {
|
||||||
HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(),
|
HeaderValue::from_str(&ContentType::form_url_encoded().to_string()).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, Some(None));
|
assert_eq!(finder.find_token(&mut req).await, Ok(None));
|
||||||
assert_eq!(finder.find_answer(&mut req).await, Some(None));
|
assert_eq!(finder.find_answer(&mut req).await, Ok(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_captcha_form_finder_invalid() {
|
async fn test_captcha_form_finder_invalid() {
|
||||||
let finder = CaptchaFormFinder::new();
|
let finder = CaptchaFormFinder::<String, String>::new();
|
||||||
let mut req = Request::default();
|
let mut req = Request::default();
|
||||||
*req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into());
|
*req.body_mut() = ReqBody::Once("captcha_token=token&captcha_answer=answer".into());
|
||||||
let headers = req.headers_mut();
|
let headers = req.headers_mut();
|
||||||
|
@ -373,7 +389,7 @@ mod tests {
|
||||||
HeaderValue::from_str(&ContentType::json().to_string()).unwrap(),
|
HeaderValue::from_str(&ContentType::json().to_string()).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(finder.find_token(&mut req).await, Some(None));
|
assert_eq!(finder.find_token(&mut req).await, Ok(None));
|
||||||
assert_eq!(finder.find_answer(&mut req).await, Some(None));
|
assert_eq!(finder.find_answer(&mut req).await, Ok(None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
src/lib.rs
53
src/lib.rs
|
@ -36,7 +36,7 @@ pub const CAPTCHA_STATE_KEY: &str = "::salvo_captcha::captcha_state";
|
||||||
pub struct Captcha<S, F>
|
pub struct Captcha<S, F>
|
||||||
where
|
where
|
||||||
S: CaptchaStorage,
|
S: CaptchaStorage,
|
||||||
F: CaptchaFinder,
|
F: CaptchaFinder<Token = S::Token, Answer = S::Answer>,
|
||||||
{
|
{
|
||||||
/// The captcha finder, used to find the captcha token and answer from the request.
|
/// The captcha finder, used to find the captcha token and answer from the request.
|
||||||
finder: F,
|
finder: F,
|
||||||
|
@ -47,20 +47,20 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The captcha states of the request
|
/// The captcha states of the request
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CaptchaState {
|
pub enum CaptchaState {
|
||||||
/// The captcha check is skipped. This depends on the skipper.
|
|
||||||
#[default]
|
|
||||||
Skipped,
|
|
||||||
/// The captcha is checked and passed. If the captcha is passed, it will be cleared from the storage.
|
/// The captcha is checked and passed. If the captcha is passed, it will be cleared from the storage.
|
||||||
Passed,
|
Passed,
|
||||||
|
/// The captcha check is skipped. This depends on the skipper.
|
||||||
|
Skipped,
|
||||||
/// Can't find the captcha token in the request
|
/// Can't find the captcha token in the request
|
||||||
TokenNotFound,
|
TokenNotFound,
|
||||||
/// Can't find the captcha answer in the request
|
/// Can't find the captcha answer in the request
|
||||||
AnswerNotFound,
|
AnswerNotFound,
|
||||||
/// Can't find the captcha token in the storage or the token is wrong (not valid string)
|
/// The captcha token is wrong, can't find the captcha in the storage.
|
||||||
|
/// Maybe the captcha token entered by the user is wrong, or the captcha is expired, because the storage has been cleared.
|
||||||
WrongToken,
|
WrongToken,
|
||||||
/// Can't find the captcha answer in the storage or the answer is wrong (not valid string)
|
/// The captcha answer is wrong. This will not clear the captcha from the storage.
|
||||||
WrongAnswer,
|
WrongAnswer,
|
||||||
/// Storage error
|
/// Storage error
|
||||||
StorageError,
|
StorageError,
|
||||||
|
@ -69,13 +69,13 @@ pub enum CaptchaState {
|
||||||
impl<S, F> Captcha<S, F>
|
impl<S, F> Captcha<S, F>
|
||||||
where
|
where
|
||||||
S: CaptchaStorage,
|
S: CaptchaStorage,
|
||||||
F: CaptchaFinder,
|
F: CaptchaFinder<Token = S::Token, Answer = S::Answer>,
|
||||||
{
|
{
|
||||||
/// Create a new Captcha
|
/// Create a new Captcha
|
||||||
pub fn new(storage: S, finder: F) -> Self {
|
pub fn new(storage: impl Into<S>, finder: impl Into<F>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
finder,
|
finder: finder.into(),
|
||||||
storage,
|
storage: storage.into(),
|
||||||
skipper: Box::new(none_skipper),
|
skipper: Box::new(none_skipper),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,22 +94,19 @@ where
|
||||||
|
|
||||||
/// The captcha extension of the depot.
|
/// The captcha extension of the depot.
|
||||||
/// Used to get the captcha info from the depot.
|
/// Used to get the captcha info from the depot.
|
||||||
pub trait CaptchaDepotExt {
|
#[easy_ext::ext(CaptchaDepotExt)]
|
||||||
|
impl Depot {
|
||||||
/// Get the captcha state from the depot
|
/// Get the captcha state from the depot
|
||||||
fn get_captcha_state(&self) -> CaptchaState;
|
pub fn get_captcha_state(&self) -> Option<&CaptchaState> {
|
||||||
}
|
self.get(CAPTCHA_STATE_KEY).ok()
|
||||||
|
|
||||||
impl CaptchaDepotExt for Depot {
|
|
||||||
fn get_captcha_state(&self) -> CaptchaState {
|
|
||||||
self.get(CAPTCHA_STATE_KEY).cloned().unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[salvo_core::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl<S, F> Handler for Captcha<S, F>
|
impl<S, F> Handler for Captcha<S, F>
|
||||||
where
|
where
|
||||||
S: CaptchaStorage,
|
S: CaptchaStorage,
|
||||||
F: CaptchaFinder + 'static, // why?
|
F: CaptchaFinder<Token = S::Token, Answer = S::Answer> + 'static,
|
||||||
{
|
{
|
||||||
async fn handle(
|
async fn handle(
|
||||||
&self,
|
&self,
|
||||||
|
@ -125,28 +122,28 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = match self.finder.find_token(req).await {
|
let token = match self.finder.find_token(req).await {
|
||||||
Some(Some(token)) => token,
|
Ok(Some(token)) => token,
|
||||||
Some(None) => {
|
Ok(None) => {
|
||||||
log::info!("Captcha token is not found in request");
|
log::info!("Captcha token is not found in request");
|
||||||
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::TokenNotFound);
|
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::TokenNotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
None => {
|
Err(err) => {
|
||||||
log::error!("Invalid token found in request");
|
log::error!("Failed to find captcha token from request: {err:?}");
|
||||||
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongToken);
|
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let answer = match self.finder.find_answer(req).await {
|
let answer = match self.finder.find_answer(req).await {
|
||||||
Some(Some(answer)) => answer,
|
Ok(Some(answer)) => answer,
|
||||||
Some(None) => {
|
Ok(None) => {
|
||||||
log::info!("Captcha answer is not found in request");
|
log::info!("Captcha answer is not found in request");
|
||||||
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::AnswerNotFound);
|
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::AnswerNotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
None => {
|
Err(err) => {
|
||||||
log::error!("Invalid answer found in request");
|
log::error!("Failed to find captcha answer from request: {err:?}");
|
||||||
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongAnswer);
|
depot.insert(CAPTCHA_STATE_KEY, CaptchaState::WrongAnswer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,20 +26,24 @@ pub trait CaptchaStorage: Send + Sync + 'static
|
||||||
where
|
where
|
||||||
Self: Clone + std::fmt::Debug,
|
Self: Clone + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
|
/// The token type
|
||||||
|
type Token: TryFrom<String> + std::fmt::Display + Send + Sync;
|
||||||
|
/// The answer type
|
||||||
|
type Answer: std::cmp::PartialEq + From<String> + Send + Sync;
|
||||||
/// The error type of the storage.
|
/// The error type of the storage.
|
||||||
type Error: std::fmt::Display + std::fmt::Debug + Send;
|
type Error: std::fmt::Display + std::fmt::Debug + Send;
|
||||||
|
|
||||||
/// Store the captcha token and answer.
|
/// Store the captcha token and answer.
|
||||||
fn store_answer(
|
fn store_answer(
|
||||||
&self,
|
&self,
|
||||||
answer: String,
|
answer: Self::Answer,
|
||||||
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send;
|
) -> impl std::future::Future<Output = Result<Self::Token, Self::Error>> + Send;
|
||||||
|
|
||||||
/// Returns the answer of the captcha token. This method will return None if the token is not exist.
|
/// Returns the answer of the captcha token. This method will return None if the token is not exist.
|
||||||
fn get_answer(
|
fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &Self::Token,
|
||||||
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send;
|
) -> impl std::future::Future<Output = Result<Option<Self::Answer>, Self::Error>> + Send;
|
||||||
|
|
||||||
/// Clear the expired captcha.
|
/// Clear the expired captcha.
|
||||||
fn clear_expired(
|
fn clear_expired(
|
||||||
|
@ -50,7 +54,7 @@ where
|
||||||
/// Clear the captcha by token.
|
/// Clear the captcha by token.
|
||||||
fn clear_by_token(
|
fn clear_by_token(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &Self::Token,
|
||||||
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
|
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,17 +86,19 @@ impl CacacheStorage {
|
||||||
#[cfg(feature = "cacache-storage")]
|
#[cfg(feature = "cacache-storage")]
|
||||||
impl CaptchaStorage for CacacheStorage {
|
impl CaptchaStorage for CacacheStorage {
|
||||||
type Error = cacache::Error;
|
type Error = cacache::Error;
|
||||||
|
type Token = String;
|
||||||
|
type Answer = String;
|
||||||
|
|
||||||
async fn store_answer(&self, answer: String) -> Result<String, Self::Error> {
|
async fn store_answer(&self, answer: Self::Answer) -> Result<Self::Token, Self::Error> {
|
||||||
let token = uuid::Uuid::new_v4();
|
let token = uuid::Uuid::new_v4();
|
||||||
log::info!("Storing captcha answer to cacache for token: {token}");
|
log::info!("Storing captcha answer to cacache for token: {token}");
|
||||||
cacache::write(&self.cache_dir, token.to_string(), answer.as_bytes()).await?;
|
cacache::write(&self.cache_dir, token.to_string(), answer.as_bytes()).await?;
|
||||||
Ok(token.to_string())
|
Ok(token.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_answer(&self, token: &str) -> Result<Option<String>, Self::Error> {
|
async fn get_answer(&self, token: &Self::Token) -> Result<Option<Self::Answer>, Self::Error> {
|
||||||
log::info!("Getting captcha answer from cacache for token: {token}");
|
log::info!("Getting captcha answer from cacache for token: {token}");
|
||||||
match cacache::read(&self.cache_dir, token).await {
|
match cacache::read(&self.cache_dir, token.to_string()).await {
|
||||||
Ok(answer) => {
|
Ok(answer) => {
|
||||||
log::info!("Captcha answer is exist in cacache for token: {token}");
|
log::info!("Captcha answer is exist in cacache for token: {token}");
|
||||||
Ok(Some(
|
Ok(Some(
|
||||||
|
@ -114,7 +120,7 @@ impl CaptchaStorage for CacacheStorage {
|
||||||
async fn clear_expired(&self, expired_after: Duration) -> Result<(), Self::Error> {
|
async fn clear_expired(&self, expired_after: Duration) -> Result<(), Self::Error> {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.expect("SystemTime before UNIX EPOCH!")
|
.expect("SystemTime must be later than UNIX_EPOCH")
|
||||||
.as_millis();
|
.as_millis();
|
||||||
let expired_after = expired_after.as_millis();
|
let expired_after = expired_after.as_millis();
|
||||||
|
|
||||||
|
@ -137,10 +143,10 @@ impl CaptchaStorage for CacacheStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_by_token(&self, token: &str) -> Result<(), Self::Error> {
|
async fn clear_by_token(&self, token: &Self::Token) -> Result<(), Self::Error> {
|
||||||
log::info!("Clearing captcha token from cacache: {token}");
|
log::info!("Clearing captcha token from cacache: {token}");
|
||||||
let remove_opts = cacache::RemoveOpts::new().remove_fully(true);
|
let remove_opts = cacache::RemoveOpts::new().remove_fully(true);
|
||||||
remove_opts.remove(&self.cache_dir, token).await
|
remove_opts.remove(&self.cache_dir, token.to_string()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,18 +155,20 @@ where
|
||||||
T: CaptchaStorage,
|
T: CaptchaStorage,
|
||||||
{
|
{
|
||||||
type Error = T::Error;
|
type Error = T::Error;
|
||||||
|
type Token = T::Token;
|
||||||
|
type Answer = T::Answer;
|
||||||
|
|
||||||
fn store_answer(
|
fn store_answer(
|
||||||
&self,
|
&self,
|
||||||
answer: String,
|
answer: Self::Answer,
|
||||||
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send {
|
) -> impl std::future::Future<Output = Result<Self::Token, Self::Error>> + Send {
|
||||||
self.as_ref().store_answer(answer)
|
self.as_ref().store_answer(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_answer(
|
fn get_answer(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &Self::Token,
|
||||||
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send {
|
) -> impl std::future::Future<Output = Result<Option<Self::Answer>, Self::Error>> + Send {
|
||||||
self.as_ref().get_answer(token)
|
self.as_ref().get_answer(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,18 +181,18 @@ where
|
||||||
|
|
||||||
fn clear_by_token(
|
fn clear_by_token(
|
||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &Self::Token,
|
||||||
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
|
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send {
|
||||||
self.as_ref().clear_by_token(token)
|
self.as_ref().clear_by_token(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "cacache-storage")]
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_store_captcha() {
|
async fn cacache_store_captcha() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
@ -207,6 +215,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_clear_expired() {
|
async fn cacache_clear_expired() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
@ -231,6 +240,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_clear_by_token() {
|
async fn cacache_clear_by_token() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
@ -255,6 +265,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_is_token_exist() {
|
async fn cacache_is_token_exist() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
@ -273,13 +284,14 @@ mod tests {
|
||||||
.expect("failed to check if token is exist")
|
.expect("failed to check if token is exist")
|
||||||
.is_some());
|
.is_some());
|
||||||
assert!(storage
|
assert!(storage
|
||||||
.get_answer("token")
|
.get_answer(&"token".to_owned())
|
||||||
.await
|
.await
|
||||||
.expect("failed to check if token is exist")
|
.expect("failed to check if token is exist")
|
||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_get_answer() {
|
async fn cacache_get_answer() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
@ -300,13 +312,14 @@ mod tests {
|
||||||
Some("answer".to_owned())
|
Some("answer".to_owned())
|
||||||
);
|
);
|
||||||
assert!(storage
|
assert!(storage
|
||||||
.get_answer("token")
|
.get_answer(&"token".to_owned())
|
||||||
.await
|
.await
|
||||||
.expect("failed to get captcha answer")
|
.expect("failed to get captcha answer")
|
||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_cache_dir() {
|
async fn cacache_cache_dir() {
|
||||||
let cache_dir = tempfile::tempdir()
|
let cache_dir = tempfile::tempdir()
|
||||||
.expect("failed to create temp file")
|
.expect("failed to create temp file")
|
||||||
|
@ -317,6 +330,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[cfg(feature = "cacache-storage")]
|
||||||
async fn cacache_clear_expired_with_expired_after() {
|
async fn cacache_clear_expired_with_expired_after() {
|
||||||
let storage = CacacheStorage::new(
|
let storage = CacacheStorage::new(
|
||||||
tempfile::tempdir()
|
tempfile::tempdir()
|
||||||
|
|
Loading…
Reference in a new issue