TerminalWitchcraft / actix-ratelimit

Rate limiter framework for Actix web
MIT License
127 stars 25 forks source link

Variable Rate Limiters? #10

Open vicky5124 opened 3 years ago

vicky5124 commented 3 years ago

Is it possible to have multiple rate limiters that get triggered depending on different factors? like for example, if i wanted to have a much higher rate limit for users who provide a valid authorization token, but fall back to the normal rate limit for unauthorized requests. I tried adding multiple RateLimiter warps, but it seems that only the first one ever does anything.

Using master branch with the redis feature on actix-web 3

TerminalWitchcraft commented 3 years ago

Hey, yes you can using scopes. Each scope can register independent middlewares and it's also a nice utility to group related resources/endpoints! More information here: https://docs.rs/actix-web/3.3.2/actix_web/struct.Scope.html

I haven't personally tried it, but it should work if I understand the documentation correctly. Let me know if you run into any trouble using this.

Best,

vicky5124 commented 3 years ago

Hey, yes you can using scopes. Each scope can register independent middlewares and it's also a nice utility to group related resources/endpoints! More information here: https://docs.rs/actix-web/3.3.2/actix_web/struct.Scope.html

I do not think this solves my issue, i want everyone to be able to have access to the same endpoints, but only change their rate limit values depending on their authorization, but this library having static values for an endpoint makes it not possible to do this. I already know i can use multiple rate limiters with different scopes, as i'm already doing that for different end points.

It would be nice if there was a method that could return the rate limit values dynamically, that would override the default, static values.

TerminalWitchcraft commented 3 years ago

Hey @nitsuga5124 , can you elaborate more about the authorization part? I'm trying to understand where in the request lifecycle is your authorization taking place, and depending on that, maybe I can come up with something.

vicky5124 commented 3 years ago

Here's a cut down version of what's currently being done. Note that the values for the interval and max requests are static, and will always be the same no matter the request coming.

web::scope("/api")
    .wrap(
        // Create a new redis rate limiter
        RateLimiter::new(RedisStoreActor::from(store.clone()).start())
            // Each rate limit session will last 120 seconds
            .with_interval(Duration::from_secs(120))
            // Each session will be able to do a maximum of 60 requers in that time.
            .with_max_requests(60)
            // Set the identifier that's used for the rate limit
            .with_identifier(|req| {
                // Get the authorization value from the headers
                let key = match req.headers().get("Authorization") {
                    Some(x) => x,
                    // If it doesn'e exist
                    None => {
                        // Get the x-real-ip header generated by nginx
                        if let Some(ip) = &req.headers().get("x-real-ip") {
                            return Ok(ip.to_str().unwrap().to_string());
                        // of if there's no nginx, use the source ip address
                        } else {
                            return Ok(req.peer_addr().unwrap().to_string());
                        }
                    }
                };
                // Redefining a variable, but it looks cleaner this way.
                let key = key.to_str().unwrap();
                Ok(key.to_string())
            }),
    )
    .service(web::resource("/get_stuff").to(verification::verify)),

The idea i have is to be able to use a function to provide both of those static values dynamically. It doesn't even need to be called every request, it could be made so only if there's no current rate limit set for the identifier, it runs to get them.

web::scope("/api")
    .wrap(
        RateLimiter::new(RedisStoreActor::from(store.clone()).start())
            // Dynamically set the interval and max_requests
            // rather than use static values for all requests of this scope.
            .with_dynamic_values(|req| {
                // Set default values
                let interval = Duration::from_secs(120);
                let mut max_req = 20;

                // Check if there's an authorization header present.
                if let Some(raw_token) = req.headers().get("Authorization") {
                    // Get the token to a usable state
                    let token = raw_token.to_str().unwrap().to_string();

                    /* code to verify the token */

                    if token_is_valid {
                        max_req = 60;
                    }
                }

                // Return a Result<(StdDuration, usize), ARError>
                Ok((interval, max_req))
            })
            .with_identifier(|req| {
                // Same code as before
            }),
    )
    .service(web::resource("/get_stuff").to(verification::verify)),

I have not looked at the source code of the library, so i don't know if this is possible, but it would be a very elegant way to solve this issue.

A piece of code like this could be used for allowing different users to use the API at different rates, such as giving administrators very high or no rate limits, while normal users get some default rate limit, and unverified users an even lower rate limit.

TerminalWitchcraft commented 3 years ago

Hi @nitsuga5124 , thanks for the explanation. This sounds more like a policy to me which is in the list of things I had in mind for this project. However, one thing to keep in mind is that rates(and limits) should always be defined statically(as in before the web worker starts). This is an important part of middleware. As for your use case, I can imagine something along the lines of this(using derive macros):

use actix_ratelimit::Policy;

#[derive(Policy)
enum MyPolicy {
    // Admin has 100 requests for 60 seconds
    #[limit(duration="60", requests="100")]
    Admin,

     // Registered user has 60 requests for 60 seconds
    #[limit(duration="60", requests="60")]
    Registered,

    // Unregistered user has 20 requests for 60 seconds
    #[limit(duration="60", requests="20")]
    Unregistered
}

web::scope("/api")
    .wrap(
        RateLimiter::new(RedisStoreActor::from(store.clone()).start())
            // Get the policy from the enum above
           .with_policy(MyPolicy)
           .with_policy_identifier(|req| {
                // Here, return the enum variant for that request

                // Your code goes here
                // Example returning for Registered users. This will automatically allow 60 requests from this client within 60 secs.
                Ok(MyPolicy::Registered)
            }),
    )
    .service(web::resource("/get_stuff").to(verification::verify)),

I think this is a better way to do what your trying to achieve. The function names are tentative, I still have to come up with some meaningful name and patterns. As for implementation, this involves rewriting some core pieces of the library, so maybe I can add this feature in the next major release(it might come with some breaking changes as well!). I also need to look into derive macros since I'm not too familiar with it. Let me know what you think about this approach.

Best,