mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-27 09:12:57 +01:00
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
This commit is contained in:
parent
e8ebf525ad
commit
bb553b2308
@ -3,17 +3,20 @@ resolver = "2"
|
||||
members = [
|
||||
"actix-cors",
|
||||
"actix-identity",
|
||||
"actix-limitation",
|
||||
"actix-protobuf",
|
||||
# TODO: move this example to examples repo
|
||||
# "actix-protobuf/examples/prost-example",
|
||||
"actix-redis",
|
||||
"actix-session",
|
||||
"actix-web-httpauth",
|
||||
]
|
||||
|
||||
# TODO: move this example to examples repo
|
||||
# "actix-protobuf/examples/prost-example",
|
||||
|
||||
[patch.crates-io]
|
||||
actix-cors = { path = "./actix-cors" }
|
||||
actix-identity = { path = "./actix-identity" }
|
||||
actix-limitation = { path = "./actix-limitation" }
|
||||
actix-protobuf = { path = "./actix-protobuf" }
|
||||
actix-redis = { path = "./actix-redis" }
|
||||
actix-session = { path = "./actix-session" }
|
||||
|
11
actix-limitation/CHANGES.md
Normal file
11
actix-limitation/CHANGES.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2022-xx-xx
|
||||
- Update Actix Web dependency to v4 ecosystem. [#229]
|
||||
- Update Tokio dependencies to v1 ecosystem. [#229]
|
||||
|
||||
[#229]: https://github.com/actix/actix-extras/pull/229
|
||||
|
||||
|
||||
## 0.1.4 - 2022-03-18
|
||||
- Adopted into @actix org from <https://github.com/0xmad/actix-limitation>.
|
24
actix-limitation/Cargo.toml
Normal file
24
actix-limitation/Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "actix-limitation"
|
||||
version = "0.1.4"
|
||||
authors = ["0xmad <0xmad@users.noreply.github.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.git"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix-session = "0.5"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4", default-features = false }
|
||||
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
redis = { version = "0.21", default-features = false, features = ["aio", "tokio-comp"] }
|
||||
time = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "4"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
51
actix-limitation/README.md
Normal file
51
actix-limitation/README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# 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>.
|
||||
|
||||
[![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.1.4)](https://docs.rs/actix-limitation/0.1.4)
|
||||
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-limitation)
|
||||
[![Dependency Status](https://deps.rs/crate/actix-limitation/0.1.4/status.svg)](https://deps.rs/crate/actix-limitation/0.1.4)
|
||||
|
||||
## Examples
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-limitation = "0.1.4"
|
||||
actix-web = "4"
|
||||
```
|
||||
|
||||
```rust
|
||||
use std::time::Duration;
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
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::build("redis://127.0.0.1")
|
||||
.cookie_name("session-id".to_owned())
|
||||
.session_key("rate-api-id".to_owned())
|
||||
.limit(5000)
|
||||
.period(Duration::from_secs(3600)) // 60 minutes
|
||||
.finish()
|
||||
.expect("Can't build actix-limiter"),
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(RateLimiter)
|
||||
.app_data(limiter.clone())
|
||||
.service(index)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
```
|
51
actix-limitation/src/core/builder/mod.rs
Normal file
51
actix-limitation/src/core/builder/mod.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use redis::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{core::errors::Error, Limiter};
|
||||
|
||||
pub struct Builder<'builder> {
|
||||
pub(crate) redis_url: &'builder str,
|
||||
pub(crate) limit: usize,
|
||||
pub(crate) period: Duration,
|
||||
pub(crate) cookie_name: String,
|
||||
pub(crate) session_key: String,
|
||||
}
|
||||
|
||||
impl Builder<'_> {
|
||||
pub fn limit(&mut self, limit: usize) -> &mut Self {
|
||||
self.limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn period(&mut self, period: Duration) -> &mut Self {
|
||||
self.period = period;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cookie_name(&mut self, cookie_name: String) -> &mut Self {
|
||||
self.cookie_name = cookie_name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn session_key(&mut self, session_key: String) -> &mut Self {
|
||||
self.session_key = session_key;
|
||||
self
|
||||
}
|
||||
|
||||
/// Finializes 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 finish(&self) -> Result<Limiter, Error> {
|
||||
Ok(Limiter {
|
||||
client: Client::open(self.redis_url)?,
|
||||
limit: self.limit,
|
||||
period: self.period,
|
||||
cookie_name: self.cookie_name.to_string(),
|
||||
session_key: self.session_key.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
62
actix-limitation/src/core/builder/test.rs
Normal file
62
actix-limitation/src/core/builder/test.rs
Normal file
@ -0,0 +1,62 @@
|
||||
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,
|
||||
limit: 100,
|
||||
period,
|
||||
cookie_name: "session".to_string(),
|
||||
session_key: "rate-api".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(builder.redis_url, redis_url);
|
||||
assert_eq!(builder.limit, 100);
|
||||
assert_eq!(builder.period, period);
|
||||
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,
|
||||
limit: 100,
|
||||
period: Duration::from_secs(10),
|
||||
session_key: "key".to_string(),
|
||||
cookie_name: "sid".to_string(),
|
||||
};
|
||||
|
||||
let limiter = builder
|
||||
.limit(200)
|
||||
.period(period)
|
||||
.cookie_name("session".to_string())
|
||||
.session_key("rate-api".to_string())
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(limiter.limit, 200);
|
||||
assert_eq!(limiter.period, period);
|
||||
assert_eq!(limiter.session_key, "rate-api");
|
||||
assert_eq!(limiter.cookie_name, "session");
|
||||
}
|
||||
|
||||
#[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,
|
||||
limit: 100,
|
||||
period: Duration::from_secs(10),
|
||||
session_key: "key".to_string(),
|
||||
cookie_name: "sid".to_string(),
|
||||
};
|
||||
|
||||
builder.limit(200).period(period).finish().unwrap();
|
||||
}
|
51
actix-limitation/src/core/errors/mod.rs
Normal file
51
actix-limitation/src/core/errors/mod.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use std::{error, fmt};
|
||||
|
||||
use crate::core::status::Status;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The Redis client failed to connect or run a query.
|
||||
Client(redis::RedisError),
|
||||
|
||||
/// The limit is exceeded for a key.
|
||||
LimitExceeded(Status),
|
||||
|
||||
/// A time conversion failed.
|
||||
Time(time::error::ComponentRange),
|
||||
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::Client(ref err) => write!(f, "Client error ({})", err),
|
||||
Error::LimitExceeded(ref status) => write!(f, "Rate limit exceeded ({:?})", status),
|
||||
Error::Time(ref err) => write!(f, "Time conversion error ({})", err),
|
||||
Error::Other(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match self {
|
||||
Error::Client(ref err) => err.source(),
|
||||
Error::LimitExceeded(_) => None,
|
||||
Error::Time(ref err) => err.source(),
|
||||
Error::Other(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for Error {
|
||||
fn from(err: redis::RedisError) -> Self {
|
||||
Error::Client(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::error::ComponentRange> for Error {
|
||||
fn from(err: time::error::ComponentRange) -> Self {
|
||||
Error::Time(err)
|
||||
}
|
||||
}
|
3
actix-limitation/src/core/mod.rs
Normal file
3
actix-limitation/src/core/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod builder;
|
||||
pub mod errors;
|
||||
pub mod status;
|
58
actix-limitation/src/core/status/mod.rs
Normal file
58
actix-limitation/src/core/status/mod.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use crate::Error as LimitationError;
|
||||
use chrono::SubsecRound;
|
||||
use std::{convert::TryInto, ops::Add, time::Duration};
|
||||
|
||||
/// A report for a given key containing the limit status.
|
||||
///
|
||||
/// The status contains the following information:
|
||||
///
|
||||
/// - [`limit`]: the maximum number of requests allowed in the current period
|
||||
/// - [`remaining`]: how many requests are left in the current period
|
||||
/// - [`reset_epoch_utc`]: a UNIX timestamp in UTC approximately when the next period will begin
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Status {
|
||||
pub(crate) limit: usize,
|
||||
pub(crate) remaining: usize,
|
||||
pub(crate) reset_epoch_utc: usize,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn limit(&self) -> usize {
|
||||
self.limit
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.remaining
|
||||
}
|
||||
|
||||
pub fn reset_epoch_utc(&self) -> usize {
|
||||
self.reset_epoch_utc
|
||||
}
|
||||
|
||||
pub(crate) fn build_status(count: usize, limit: usize, reset_epoch_utc: usize) -> Self {
|
||||
let remaining = if count >= limit { 0 } else { limit - count };
|
||||
|
||||
Status {
|
||||
limit,
|
||||
remaining,
|
||||
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 test;
|
54
actix-limitation/src/core/status/test.rs
Normal file
54
actix-limitation/src/core/status/test.rs
Normal file
@ -0,0 +1,54 @@
|
||||
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::build_status(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::build_status(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();
|
||||
}
|
123
actix-limitation/src/lib.rs
Normal file
123
actix-limitation/src/lib.rs
Normal file
@ -0,0 +1,123 @@
|
||||
/*!
|
||||
Rate limiter using a fixed window counter for arbitrary keys, backed by Redis for Actix Web
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-limitation = "0.1.4"
|
||||
actix-web = "4"
|
||||
```
|
||||
|
||||
```no_run
|
||||
use std::time::Duration;
|
||||
use actix_web::{get, web, App, HttpServer, Responder};
|
||||
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::build("redis://127.0.0.1")
|
||||
.cookie_name("session-id".to_owned())
|
||||
.session_key("rate-api-id".to_owned())
|
||||
.limit(5000)
|
||||
.period(Duration::from_secs(3600)) // 60 minutes
|
||||
.finish()
|
||||
.expect("Can't build actix-limiter"),
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(RateLimiter)
|
||||
.app_data(limiter.clone())
|
||||
.service(index)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use redis::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use crate::core::{builder::Builder, errors::Error, status::Status};
|
||||
pub use crate::middleware::RateLimiter;
|
||||
|
||||
pub const DEFAULT_REQUEST_LIMIT: usize = 5000;
|
||||
pub const DEFAULT_PERIOD_SECS: u64 = 3600;
|
||||
pub const DEFAULT_COOKIE_NAME: &str = "sid";
|
||||
pub const DEFAULT_SESSION_KEY: &str = "rate-api-id";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Limiter {
|
||||
client: Client,
|
||||
limit: usize,
|
||||
period: Duration,
|
||||
cookie_name: String,
|
||||
session_key: String,
|
||||
}
|
||||
|
||||
impl Limiter {
|
||||
pub fn build(redis_url: &str) -> Builder {
|
||||
Builder {
|
||||
redis_url,
|
||||
limit: DEFAULT_REQUEST_LIMIT,
|
||||
period: Duration::from_secs(DEFAULT_PERIOD_SECS),
|
||||
cookie_name: DEFAULT_COOKIE_NAME.to_string(),
|
||||
session_key: DEFAULT_SESSION_KEY.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn count<K: Into<String>>(&self, key: K) -> Result<Status, Error> {
|
||||
let (count, reset) = self.track(key).await?;
|
||||
let status = Status::build_status(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<K: Into<String>>(&self, key: K) -> Result<(usize, usize), Error> {
|
||||
let key = key.into();
|
||||
let exipres = self.period.as_secs();
|
||||
|
||||
let mut connection = self.client.get_tokio_connection().await?;
|
||||
|
||||
// The seed of this approach is outlined Atul R in a blog post about rate limiting
|
||||
// using NodeJS and Redis. For more details, see
|
||||
// https://blog.atulr.com/rate-limiter/
|
||||
let mut pipe = redis::pipe();
|
||||
pipe.atomic()
|
||||
.cmd("SET")
|
||||
.arg(&key)
|
||||
.arg(0)
|
||||
.arg("EX")
|
||||
.arg(exipres)
|
||||
.arg("NX")
|
||||
.ignore()
|
||||
.cmd("INCR")
|
||||
.arg(&key)
|
||||
.cmd("TTL")
|
||||
.arg(&key);
|
||||
|
||||
let (count, ttl): (usize, u64) = pipe.query_async(&mut connection).await?;
|
||||
let reset = Status::epoch_utc_plus(Duration::from_secs(ttl))?;
|
||||
Ok((count, reset))
|
||||
}
|
||||
}
|
||||
|
||||
mod core;
|
||||
mod middleware;
|
||||
#[cfg(test)]
|
||||
mod test;
|
109
actix-limitation/src/middleware/mod.rs
Normal file
109
actix-limitation/src/middleware/mod.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
use actix_session::UserSession;
|
||||
use actix_utils::future::{ok, Ready};
|
||||
use actix_web::{
|
||||
body::EitherBody,
|
||||
cookie::Cookie,
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
http::header::COOKIE,
|
||||
web, Error, HttpResponse,
|
||||
};
|
||||
|
||||
use crate::Limiter;
|
||||
|
||||
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 InitError = ();
|
||||
type Transform = RateLimiterMiddleware<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(RateLimiterMiddleware {
|
||||
service: Rc::new(service),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 mis-configuration 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 forbidden = HttpResponse::Forbidden().finish().map_into_right_body();
|
||||
let (key, fallback) = key(&req, limiter.clone());
|
||||
|
||||
let service = Rc::clone(&self.service);
|
||||
let key = match key {
|
||||
Some(key) => key,
|
||||
None => match fallback {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
return Box::pin(async move {
|
||||
service
|
||||
.call(req)
|
||||
.await
|
||||
.map(ServiceResponse::map_into_left_body)
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let service = Rc::clone(&self.service);
|
||||
Box::pin(async move {
|
||||
let status = limiter.count(key.to_string()).await;
|
||||
if status.is_err() {
|
||||
warn!("403. Rate limit exceed error for {}", key);
|
||||
Ok(req.into_response(forbidden))
|
||||
} else {
|
||||
service
|
||||
.call(req)
|
||||
.await
|
||||
.map(ServiceResponse::map_into_left_body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn key(req: &ServiceRequest, limiter: web::Data<Limiter>) -> (Option<String>, Option<String>) {
|
||||
let session = req.get_session();
|
||||
let result: Option<String> = session.get(&limiter.session_key).unwrap_or(None);
|
||||
let cookies = req.headers().get_all(COOKIE);
|
||||
let cookie = cookies
|
||||
.filter_map(|i| i.to_str().ok())
|
||||
.find(|i| i.contains(&limiter.cookie_name));
|
||||
|
||||
let fallback = match cookie {
|
||||
Some(value) => Cookie::parse(value).ok().map(|i| i.to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
(result, fallback)
|
||||
}
|
65
actix-limitation/src/test/mod.rs
Normal file
65
actix-limitation/src/test/mod.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_limiter() {
|
||||
let builder = Limiter::build("redis://127.0.0.1:6379/1");
|
||||
let limiter = builder.finish();
|
||||
assert!(limiter.is_ok());
|
||||
|
||||
let limiter = limiter.unwrap();
|
||||
assert_eq!(limiter.limit, 5000);
|
||||
assert_eq!(limiter.period, Duration::from_secs(3600));
|
||||
assert_eq!(limiter.cookie_name, DEFAULT_COOKIE_NAME);
|
||||
assert_eq!(limiter.session_key, DEFAULT_SESSION_KEY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "Redis URL did not parse"]
|
||||
fn test_create_limiter_error() {
|
||||
Limiter::build("127.0.0.1").finish().unwrap();
|
||||
}
|
||||
|
||||
// TODO: figure out whats wrong with this test
|
||||
#[ignore]
|
||||
#[actix_web::test]
|
||||
async fn test_limiter_count() -> Result<(), Error> {
|
||||
let builder = Limiter::build("redis://127.0.0.1:6379/2");
|
||||
let limiter = builder.finish().unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
for i in 0..5000 {
|
||||
let status = limiter.count(id.to_string()).await?;
|
||||
assert_eq!(5000 - status.remaining(), i + 1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: figure out whats wrong with this test
|
||||
#[ignore]
|
||||
#[actix_web::test]
|
||||
async fn test_limiter_count_error() -> Result<(), Error> {
|
||||
let builder = Limiter::build("redis://127.0.0.1:6379/3");
|
||||
let limiter = builder.finish().unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
for i in 0..5000 {
|
||||
let status = limiter.count(id.to_string()).await?;
|
||||
assert_eq!(5000 - 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..5000 {
|
||||
let status = limiter.count(id.to_string()).await?;
|
||||
assert_eq!(5000 - status.remaining(), i + 1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user