Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

Variable Rate Limiters? #10

Open
vicky5124 opened this issue Dec 12, 2020 · 5 comments
Open

Variable Rate Limiters? #10

vicky5124 opened this issue Dec 12, 2020 · 5 comments
Labels
enhancement New feature or request

Comments

@vicky5124
Copy link

vicky5124 commented Dec 12, 2020

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

vicky5124 commented Dec 29, 2020

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
Copy link
Owner

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,

@TerminalWitchcraft TerminalWitchcraft added the enhancement New feature or request label Dec 30, 2020
@vicky5124 vicky5124 changed the title Multiple Rate Limiters? Variable Rate Limiters? Dec 31, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants