diff --git a/README.md b/README.md index 6fbd968..b57fd04 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ end Use any [ActiveSupport Time extension to Numeric](http://edgeguides.rubyonrails.org/active_support_core_extensions.html#time) object. +--- + By default it will use the request ip address to identity the client making the request. You can pass your own identifier using a `Proc`: @@ -83,6 +85,36 @@ class MyApi < Grape::API end ``` +--- + +By default requests to each endpoint's methods will be counted separately, if you would like the requests to the method to be counted for and checked against a global total, you can specify `global_throttling: true`. When `global_throttling` is set to true, `max` and `per` are ignored on the endpoint and instead, `global_throttling_max`, and `global_throttling_per` from the [configuration](#configuration) are looked at. + +> `global_throttling` can also be set to true in the [Grape::Attack configuration](#configuration). + +```ruby +class MyApi < Grape::API + + use Grape::Attack::Throttle + + helpers do + def current_user + @current_user ||= User.authorize!(env) + end + end + + resources :comments do + + throttle global_throttling: true + get do + Comment.all + end + + end +end +``` + +--- + When rate limit is reached, it will raise `Grape::Attack::RateLimitExceededError` exception. You can catch the exception using `rescue_from`: @@ -115,6 +147,8 @@ Content-Type: application/json {"message":"API rate limit exceeded for xxx.xxx.xxx.xxx."} ``` +--- + Finally the following headers will automatically be set: * `X-RateLimit-Limit` -- The maximum number of requests that the consumer is permitted to make per specified period. @@ -128,6 +162,21 @@ Currently there is only a Redis adapter. You can set redis client url through `e Defaults to `redis://localhost:6379/0`. +## Configuration + +If you wish to set a different [adapter](#adapters) or provide configuration options for `global_throttling`, you can configure `Grape::Attack` in a Rails initializer. + +```ruby +# config/initializers/grape_attack.rb + +Grape::Attack.configure do |c| + c.adapter = Grape::Attack::Adapters::Memory.new # defaults to Grape::Attack::Adapters::Redis.new + c.global_throttling = true # defaults to false + c.global_throttling_max = 1000 # defaults to 500 + c.global_throttling_per = 1.day # defaults to 1.day +end +``` + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. @@ -142,4 +191,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). - diff --git a/lib/grape/attack/configuration.rb b/lib/grape/attack/configuration.rb index c933a32..90a37be 100644 --- a/lib/grape/attack/configuration.rb +++ b/lib/grape/attack/configuration.rb @@ -2,11 +2,14 @@ module Grape module Attack class Configuration - attr_accessor :adapter, :disable + attr_accessor :adapter, :disable, :global_throttling, :global_throttling_max, :global_throttling_per def initialize @adapter = ::Grape::Attack::Adapters::Redis.new @disable = Proc.new { false } + @global_throttling = false + @global_throttling_max = 500 + @global_throttling_per = 1.day end end diff --git a/lib/grape/attack/counter.rb b/lib/grape/attack/counter.rb index 1f8ea9c..d9d9762 100644 --- a/lib/grape/attack/counter.rb +++ b/lib/grape/attack/counter.rb @@ -28,7 +28,11 @@ def update private def key - "#{request.method}:#{request.path}:#{request.client_identifier}" + if request.throttle_options.global_throttling + "#{request.client_identifier}" + else + "#{request.method}:#{request.path}:#{request.client_identifier}" + end end def ttl_in_seconds diff --git a/lib/grape/attack/options.rb b/lib/grape/attack/options.rb index 9f9625c..d5b7107 100644 --- a/lib/grape/attack/options.rb +++ b/lib/grape/attack/options.rb @@ -5,15 +5,26 @@ module Attack class Options include ActiveModel::Model - attr_accessor :max, :per, :identifier, :remaining + attr_accessor :max, :per, :identifier, :global_throttling, :remaining - validates :max, presence: true - validates :per, presence: true + validates :max, presence: true, unless: :global_throttling + validates :per, presence: true, unless: :global_throttling def identifier @identifier || Proc.new {} end + def max + global_throttling ? ::Grape::Attack.config.global_throttling_max : @max + end + + def per + global_throttling ? ::Grape::Attack.config.global_throttling_per : @per + end + + def global_throttling + @global_throttling.nil? ? ::Grape::Attack.config.global_throttling : @global_throttling + end end end end diff --git a/lib/grape/attack/throttle.rb b/lib/grape/attack/throttle.rb index e06a1a4..f30f32e 100644 --- a/lib/grape/attack/throttle.rb +++ b/lib/grape/attack/throttle.rb @@ -17,9 +17,9 @@ def after return if ::Grape::Attack.config.disable.call return unless request.throttle? - @app_response['X-RateLimit-Limit'] = request.context.route_setting(:throttle)[:max].to_s - @app_response['X-RateLimit-Remaining'] = request.context.route_setting(:throttle)[:remaining].to_s - @app_response['X-RateLimit-Reset'] = request.context.route_setting(:throttle)[:per].from_now.to_i.to_s + @app_response['X-RateLimit-Limit'] = request.throttle_options.max.to_s + @app_response['X-RateLimit-Remaining'] = request.throttle_options.remaining.to_s + @app_response['X-RateLimit-Reset'] = request.throttle_options.per.from_now.to_i.to_s @app_response end