From 438d10a3f9777fc87e15327d2744f944c5b0505b Mon Sep 17 00:00:00 2001 From: Raphael C Date: Thu, 9 Nov 2023 23:29:38 +0100 Subject: [PATCH] doc: example for scoped limiters --- actix-limitation/Cargo.toml | 1 + actix-limitation/examples/README.md | 39 ++++++++++ actix-limitation/examples/scoped_limiters.rs | 82 ++++++++++++++++++++ actix-limitation/src/lib.rs | 4 +- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 actix-limitation/examples/README.md create mode 100644 actix-limitation/examples/scoped_limiters.rs diff --git a/actix-limitation/Cargo.toml b/actix-limitation/Cargo.toml index 90e6e9c55..c92e813b1 100644 --- a/actix-limitation/Cargo.toml +++ b/actix-limitation/Cargo.toml @@ -38,3 +38,4 @@ actix-session = { version = "0.8", optional = true } actix-web = "4" static_assertions = "1" uuid = { version = "1", features = ["v4"] } +pretty_env_logger = "0.5" \ No newline at end of file diff --git a/actix-limitation/examples/README.md b/actix-limitation/examples/README.md new file mode 100644 index 000000000..5e06ce61c --- /dev/null +++ b/actix-limitation/examples/README.md @@ -0,0 +1,39 @@ +# Examples + +We leverage redis to store state of the ratelimiting. +So you will need to have a redis instance available on localhost. + +You can start this redis instance with Docker: +``` +docker run -d -p 6379:6379 --name limiter-redis redis +# Clean up: you can rm the docker this way +# docker rm -f limiter-redis +``` + + +## scoped_limiters + +This example present how to use multiple limiters. +This allow different configurations and the ability to scope them. + +### Starting the example server + +```bash +RUST_LOG=debug cargo run --example scoped_limiters +``` +> RUST_LOG=debug is used to print logs, see crate pretty_env_logger for more details. + +### Testing with curl + +```bash +curl -X PUT localhost:8080/scoped/sms -v +``` +first request should work fine +doing a second request within 60 seconds should yield `HTTP/1.1 429 Too Many Requests` +after 60 seconds you should be able to make 1 request again + + +```bash +curl localhost:8080 +``` +This route should work 30 times, or 29 if you previously requested the /scoped/sms route diff --git a/actix-limitation/examples/scoped_limiters.rs b/actix-limitation/examples/scoped_limiters.rs new file mode 100644 index 000000000..714422a0f --- /dev/null +++ b/actix-limitation/examples/scoped_limiters.rs @@ -0,0 +1,82 @@ +use std::{collections::HashMap, time::Duration}; + +use actix_limitation::{Limiter, RateLimiter}; +use actix_web::{dev::ServiceRequest, get, put, web, App, HttpServer, Responder}; +use redis::Client; + +#[get("/")] +async fn index() -> impl Responder { + "index" +} + +#[put("/sms")] +async fn send_sms() -> impl Responder { + "sending an expensive sms" +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + pretty_env_logger::init(); + + // Create an Hashmap to store the multiples [Limiter](Limiter) + let mut limiters = HashMap::new(); + + // Create and connect a redis Client. + let redis_client = Client::open("redis://127.0.0.1/").expect("creation of the redis client"); + + // Create a default limiter + let default_limiter = Limiter::builder_with_redis_client(redis_client.clone()) + // specifying with key_by that we take the user IP address as a identifier. + .key_by(|req: &ServiceRequest| { + req.connection_info() + .realip_remote_addr() + .map(|ip| ip.to_string()) + }) + // Allowing a maximum of 30 requests per minute + .limit(30) + .period(Duration::from_secs(60)) + .build() + .unwrap(); + limiters.insert("default", default_limiter); + + let scope_limiter = Limiter::builder_with_redis_client(redis_client) + .key_by(|req: &ServiceRequest| { + req.connection_info() + .realip_remote_addr() + // ⚠️ we prepend "scoped" to the key in order to isolate this count from the default count + // + // If we were using the same key, a request to this route would always return too many requests + // in this context because the default limiter at the root would be reached first and would count 1 before we check for this. + // To mitigate this issue you could also specify a different namespace with the redis_client passed as parameter: `redis://127.0.0.1/2` + .map(|ip| format!("scoped-{}", ip)) + }) + // Allowing only 1 request per minute + .limit(1) + .period(Duration::from_secs(60)) + .build() + .unwrap(); + limiters.insert("scoped", scope_limiter); + + // Passing this limiters as app_data so it can be accessed by the middleware. + let limiters = web::Data::new(limiters); + HttpServer::new(move || { + App::new() + // Using the default limiter for all the routes + // ⚠️ This limiter will count and apply the limits before the one in "/scoped" + .wrap(RateLimiter::scoped("default")) + .app_data(limiters.clone()) + .service( + web::scope("/scoped") + // Wrapping only for this scope the scoped limiter + .wrap(RateLimiter::scoped("scoped")) + // This route will only be available 1 time every minutes + // Note: the root limiter default will also limit this route + .service(send_sms), + ) + // This route is only limited by the default limiter + .service(index) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} diff --git a/actix-limitation/src/lib.rs b/actix-limitation/src/lib.rs index 04f0be938..955038440 100644 --- a/actix-limitation/src/lib.rs +++ b/actix-limitation/src/lib.rs @@ -140,10 +140,10 @@ impl Limiter { /// Consumes one rate limit unit, returning the status. pub async fn count(&self, key: impl Into) -> Result { - let (count, reset) = self.track(key).await?; + let (count, reset) = dbg!(self.track(key).await?); let status = Status::new(count, self.limit, reset); - if count > self.limit { + if dbg!(count) > dbg!(self.limit) { Err(Error::LimitExceeded(status)) } else { Ok(status)