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}