actix_ratelimit/
lib.rs

1#![doc(html_root_url = "https://docs.rs/actix-ratelimit/0.3.1")]
2//! Rate limiting middleware framework for [actix-web](https://actix.rs/)
3//!
4//! This crate provides an asynchronous and concurrent rate limiting middleware based on [actor](https://www.wikiwand.com/en/Actor_model)
5//! model which can be wraped around an [Actix](https://actix.rs/) application. Middleware contains a store which is used to
6//! identify client request.
7//!
8//! Check out the [documentation here](https://docs.rs/actix-ratelimit/).
9//!
10//! Comments, suggesstions and critiques are welcomed!
11//!
12//! # Usage
13//! Add this to your Cargo.toml:
14//! ```toml
15//! [dependencies]
16//! actix-ratelimit = "0.3.1"
17//! ```
18//!
19//! Minimal example:
20//!
21//! ```
22//! # #[cfg(feature = "default")] {
23//! # use std::time::Duration;
24//! use actix_web::{web, App, HttpRequest, HttpServer, Responder};
25//! use actix_ratelimit::{RateLimiter, MemoryStore, MemoryStoreActor};
26//!
27//! async fn greet(req: HttpRequest) -> impl Responder{
28//!     let name = req.match_info().get("name").unwrap_or("World!");
29//!     format!("Hello {}!", &name)
30//! }
31//!
32//! #[actix_web::main]
33//! async fn main() -> std::io::Result<()> {
34//!     // Initialize store
35//!     let store = MemoryStore::new();
36//!     HttpServer::new(move ||{
37//!         App::new()
38//!             // Register the middleware
39//!             // which allows for a maximum of
40//!             // 100 requests per minute per client
41//!             // based on IP address
42//!             .wrap(
43//!                 RateLimiter::new(
44//!                 MemoryStoreActor::from(store.clone()).start())
45//!                     .with_interval(Duration::from_secs(60))
46//!                     .with_max_requests(100)
47//!             )
48//!             .route("/", web::get().to(greet))
49//!             .route("/{name}", web::get().to(greet))
50//!     })
51//!     .bind("127.0.0.1:8000")?
52//!     .run()
53//!     .await
54//! }
55//! # }
56//! ```
57//! Sending a request returns a response with the ratelimiting headers:
58//! ```shell
59//! $ curl -i "http://localhost:8000/"
60//!
61//! HTTP/1.1 200 OK
62//! content-length: 13
63//! content-type: text/plain; charset=utf-8
64//! x-ratelimit-remaining: 99
65//! x-ratelimit-reset: 52
66//! x-ratelimit-limit: 100
67//! date: Tue, 04 Feb 2020 21:53:27 GMT
68//!
69//! Hello World!
70//! ```
71//! Exceeding the limit returns HTTP 429 Error code.
72//!
73//! # Stores
74//!
75//! A _store_ is a data structure, database connection or anything which can be used to store
76//! _ratelimit_ data associated with a _client_. A _store actor_ which acts on this store is
77//! responsible for performiing all sorts of operations(SET, GET, DEL, etc). It is Important to
78//! note that there are multiple store actors acting on a _single_ store.
79//!
80//!
81//! ## Supported
82//! - In-memory (based on concurrent [hashmap](https://github.com/xacrimon/dashmap))
83//! - Redis (based on [redis-rs](https://github.com/mitsuhiko/redis-rs))
84//!
85//! ## Planned
86//! - Memcached (not yet implemented)
87//!
88//! # Implementing your own store
89//!
90//! To implement your own store, you have to implement an [Actor](https://actix.rs/actix/actix/trait.Actor.html) which can handle [ActorMessage](enum.ActorMessage.html) type
91//! and return [ActorResponse](enum.ActorResponse.html) type. Check the [module level documentation](stores/index.html) for
92//! more details and a basic example.
93//!
94//! # Note to developers
95//!
96//! * To use redis store, put this to your Cargo.toml:
97//! ```toml
98//! [dependencies]
99//! actix-ratelimit = {version = "0.3.1", default-features = false, features = ["redis-store"]}
100//! ```
101//!
102//! * By default, the client's IP address is used as the identifier which can be customized
103//! using [ServiceRequest](https://docs.rs/actix-web/3.3.2/actix_web/dev/struct.ServiceRequest.html) instance.
104//! For example, using api key header to identify client:
105//! ```rust
106//! # #[cfg(feature = "default")] {
107//! # use std::time::Duration;
108//! # use actix_web::{web, App, HttpRequest, HttpServer, Responder};
109//! # use actix_ratelimit::{RateLimiter, MemoryStore, MemoryStoreActor};
110//! # async fn greet(req: HttpRequest) -> impl Responder{
111//! #     let name = req.match_info().get("name").unwrap_or("World!");
112//! #     format!("Hello {}!", &name)
113//! # }
114//! #[actix_web::main]
115//! async fn main() -> std::io::Result<()> {
116//!     // Initialize store
117//!     let store = MemoryStore::new();
118//!     HttpServer::new(move ||{
119//!         App::new()
120//!             .wrap(
121//!                 RateLimiter::new(
122//!                 MemoryStoreActor::from(store.clone()).start())
123//!                     .with_interval(Duration::from_secs(60))
124//!                     .with_max_requests(100)
125//!                     .with_identifier(|req| {
126//!                         let key = req.headers().get("x-api-key").unwrap();
127//!                         let key = key.to_str().unwrap();
128//!                         Ok(key.to_string())
129//!                     })
130//!             )
131//!             .route("/", web::get().to(greet))
132//!             .route("/{name}", web::get().to(greet))
133//!     })
134//!     .bind("127.0.0.1:8000")?
135//!     .run()
136//!     .await
137//! }
138//! # }
139//! ```
140//!
141//! * It is **important** to initialize store before creating HttpServer instance, or else a store
142//! will be created for each web worker. This may lead to instability and inconsistency! For
143//! example, initializing your app in the following manner would create more than one stores:
144//! ```rust
145//! # #[cfg(feature = "default")] {
146//! # use std::time::Duration;
147//! # use actix_web::{web, App, HttpRequest, HttpServer, Responder};
148//! # use actix_ratelimit::{RateLimiter, MemoryStore, MemoryStoreActor};
149//! # async fn greet(req: HttpRequest) -> impl Responder{
150//! #     let name = req.match_info().get("name").unwrap_or("World!");
151//! #     format!("Hello {}!", &name)
152//! # }
153//! #[actix_web::main]
154//! async fn main() -> std::io::Result<()> {
155//!     HttpServer::new(move ||{
156//!         App::new()
157//!             .wrap(
158//!                 RateLimiter::new(
159//!                 MemoryStoreActor::from(MemoryStore::new()).start())
160//!                     .with_interval(Duration::from_secs(60))
161//!                     .with_max_requests(100)
162//!             )
163//!             .route("/", web::get().to(greet))
164//!             .route("/{name}", web::get().to(greet))
165//!     })
166//!     .bind("127.0.0.1:8000")?
167//!     .run()
168//!     .await
169//! }
170//! # }
171//! ```
172//!
173//! * The exception is redis, where multiple connections will be
174//! created for each worker. Since redis store is based on Multiplexed connection, sharing once
175//! connection across multiple store actors should suffice for most use cases.
176//!
177//!
178//! # Status
179//! This project has not reached v1.0, so some instability and breaking changes are to be expected
180//! till then.
181//!
182//! You can use the [issue tracker](https://github.com/TerminalWitchcraft/actix-ratelimit/issues) in case you encounter any problems.
183//!
184//! # LICENSE
185//! This project is licensed under MIT license.
186
187pub mod errors;
188pub mod middleware;
189pub mod stores;
190use errors::ARError;
191pub use middleware::RateLimiter;
192
193#[cfg(feature = "memory")]
194pub use stores::memory::{MemoryStore, MemoryStoreActor};
195#[cfg(feature = "redis-store")]
196pub use stores::redis::{RedisStore, RedisStoreActor};
197#[cfg(feature = "memcached")]
198pub use stores::memcached::{MemcacheStore, MemcacheStoreActor};
199
200use std::future::Future;
201use std::marker::Send;
202use std::pin::Pin;
203use std::time::Duration;
204
205use actix::dev::*;
206
207/// Represents message that can be handled by a `StoreActor`
208pub enum ActorMessage {
209    /// Get the remaining count based on the provided identifier
210    Get(String),
211    /// Set the count of the client identified by `key` to `value` valid for `expiry`
212    Set {
213        key: String,
214        value: usize,
215        expiry: Duration,
216    },
217    /// Change the value of count for the client identified by `key` by `value`
218    Update { key: String, value: usize },
219    /// Get the expiration time for the client.
220    Expire(String),
221    /// Remove the client from the store
222    Remove(String),
223}
224
225impl Message for ActorMessage {
226    type Result = ActorResponse;
227}
228
229/// Wrapper type for `Pin<Box<dyn Future>>` type
230pub type Output<T> = Pin<Box<dyn Future<Output = Result<T, ARError>> + Send>>;
231
232/// Represents data returned in response to `Messages` by a `StoreActor`
233pub enum ActorResponse {
234    /// Returned in response to [Messages::Get](enum.Messages.html)
235    Get(Output<Option<usize>>),
236    /// Returned in response to [Messages::Set](enum.Messages.html)
237    Set(Output<()>),
238    /// Returned in response to [Messages::Update](enum.Messages.html)
239    Update(Output<usize>),
240    /// Returned in response to [Messages::Expire](enum.Messages.html)
241    Expire(Output<Duration>),
242    /// Returned in response to [Messages::Remove](enum.Messages.html)
243    Remove(Output<usize>),
244}
245
246impl<A, M> MessageResponse<A, M> for ActorResponse
247where
248    A: Actor,
249    M: actix::Message<Result = ActorResponse>,
250{
251    fn handle<R: ResponseChannel<M>>(self, _: &mut A::Context, tx: Option<R>) {
252        if let Some(tx) = tx {
253            tx.send(self);
254        }
255    }
256}