rack-attack-rate-limit 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae6161b3d1a8760682d587b41b570240aea058f4
4
- data.tar.gz: 96c6e87f6143f46feb7c38c136a16c00fc6e7188
3
+ metadata.gz: 1d10bdcfab72d5a2a4b398d240d1944189da6318
4
+ data.tar.gz: 86f9d556c90a96e7d5a5f2e57231a8c24b602203
5
5
  SHA512:
6
- metadata.gz: 4b487f260716def9aa4bba218fcbda154a214132eaaa0055cc13e6e90702f9008d2bc146fd0c00d86f4f77ac1c186c9a0f6d6898a276d2f9ad723c2f78f520b8
7
- data.tar.gz: 6a34a589b16e7ea377ed4e1ec751ffee171a0897d11e6087b3f1ac339aef2bcbcb9ca1f966eadd0bb21682f273640bda3f2fd8e91441cd141a8a3689e1eceaa1
6
+ metadata.gz: 037cb6dee3e51d86c6f8b29f16fa6eeb64c304debabb1f8381e45bdacb239bce77b03fa11aae869bede31ca8e8955c113fc09af09d047376f315b07564761e2a
7
+ data.tar.gz: 97a2e8aeb2734b1c3681bd196c72b82be999641ec6a7def2ff0f76f3573fb6ea87146bf69a518cd257532a1902d8ac0b7a29b380c1bad89a99a06af03522a635
@@ -1,5 +1,9 @@
1
1
  # rack-attack-rate-limit changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ * Add support for multiple throttles.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  * Support for Rails 4
data/README.md CHANGED
@@ -26,7 +26,7 @@ bundle
26
26
 
27
27
  ## Usage
28
28
 
29
- Rack::Attack::RateLimit expects a Rack::Attack throttle to be defined:
29
+ Rack::Attack::RateLimit expects at least one Rack::Attack throttle to be defined:
30
30
 
31
31
  ```ruby
32
32
  Rack::Attack.throttle('my_throttle') do |req|
@@ -34,20 +34,20 @@ Rack::Attack.throttle('my_throttle') do |req|
34
34
  end
35
35
  ```
36
36
 
37
- To include rate limit headers for this throttle, include the Rack::Attack::RateLimit middleware
37
+ To include rate limit headers for throttles, include the Rack::Attack::RateLimit middleware, and provide it with the names of the throttles you want to add rate limit headers for. A single throttle name can be provided as a string, while multiple throttle names must be provided as an array of strings.
38
38
 
39
39
  For Rails 3+:
40
40
 
41
41
  ```ruby
42
- config.middleware.use Rack::Attack::RateLimit, throttle: 'my_throttle'
42
+ config.middleware.use Rack::Attack::RateLimit, throttle: ['my_throttle', 'my_other_throttle']
43
43
  ```
44
44
 
45
- Currently, Rack::Attack::RateLimit can only be configured to return rate limit headers for a single throttle, whose name can be specified as an option.
46
-
47
45
  Rate limit headers are:
48
46
 
49
47
  * 'X-RateLimit-Limit' - The total number of requests allowed.
50
- * 'X-RateLimit-Remaining' - The number of remaining requests.
48
+ * 'X-RateLimit-Remaining' - The number of remaining requests.
49
+
50
+ If a request triggers multiple throttles, the gem will add headers for the throttle with the lowest number of remaining requests.
51
51
 
52
52
  ## Contributing
53
53
 
@@ -41,7 +41,7 @@ module Rack
41
41
  end
42
42
 
43
43
  def throttle
44
- options[:throttle] || ''
44
+ Array(options[:throttle]) || []
45
45
  end
46
46
 
47
47
  # Return hash of headers with Rate Limiting data
@@ -50,8 +50,9 @@ module Rack
50
50
  #
51
51
  # Returns hash
52
52
  def add_rate_limit_headers!(headers, env)
53
- headers['X-RateLimit-Limit'] = rate_limit_limit(env).to_s
54
- headers['X-RateLimit-Remaining'] = rate_limit_remaining(env).to_s
53
+ throttle_data = throttle_data_closest_to_limit(env)
54
+ headers['X-RateLimit-Limit'] = rate_limit_limit(throttle_data).to_s
55
+ headers['X-RateLimit-Remaining'] = rate_limit_remaining(throttle_data).to_s
55
56
  headers
56
57
  end
57
58
 
@@ -62,8 +63,8 @@ module Rack
62
63
  # env - Hash
63
64
  #
64
65
  # Returns Fixnum
65
- def rate_limit_limit(env)
66
- env[rack_attack_key][throttle][:limit]
66
+ def rate_limit_limit(throttle_data)
67
+ throttle_data[:limit]
67
68
  end
68
69
 
69
70
  # RateLimit remaining request from Rack::Attack
@@ -71,18 +72,41 @@ module Rack
71
72
  # env - Hash
72
73
  #
73
74
  # Returns Fixnum
74
- def rate_limit_remaining(env)
75
- rate_limit_limit(env) - env[rack_attack_key][throttle][:count]
75
+ def rate_limit_remaining(throttle_data)
76
+ rate_limit_limit(throttle_data) - throttle_data[:count]
76
77
  end
77
78
 
78
79
  # Rate Limit available method for Rack::Attack provider
79
- # Checks the key identifed by options[:namespace] under the rack.attak.throttle_data env hash key
80
+ # Checks that at least one of the keys provided by the user are in the rack.attack.throttle_data env hash key
80
81
  #
81
82
  # env - Hash
82
83
  #
83
84
  # Returns boolean
84
85
  def rate_limit_available?(env)
85
- env.key?(rack_attack_key) && env[rack_attack_key].key?(throttle)
86
+ env.key?(rack_attack_key) && (env[rack_attack_key].keys & throttle).any?
87
+ end
88
+
89
+ # Throttle Data of Interest
90
+ # Filters the rack.attack.throttle_data env hash key for the throttle names provided by the user
91
+ #
92
+ # env - Hash
93
+ #
94
+ # Returns Hash
95
+ def throttle_data_of_interest(env)
96
+ env[rack_attack_key].select { |k, _v| throttle.include?(k) }
97
+ end
98
+
99
+ # Throttle Data Closest to Limit
100
+ # Selects the hash in throttle_data_of_interest where the user is closest to the limit
101
+ #
102
+ # env - Hash
103
+ #
104
+ # Returns Hash
105
+ def throttle_data_closest_to_limit(env)
106
+ min_array = throttle_data_of_interest(env).min_by { |_k, v| v[:limit] - v[:count] }
107
+ # The min_by method returns an array of the form [key, value]
108
+ # We only need the values
109
+ min_array.last
86
110
  end
87
111
  end
88
112
  end
@@ -1,7 +1,7 @@
1
1
  module Rack
2
2
  class Attack
3
3
  class RateLimit
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
6
6
  end
7
7
  end
@@ -1,59 +1,101 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Rack::Attack::RateLimit do
4
-
4
+
5
5
  include Rack::Test::Methods
6
-
7
- let(:throttle) { 'rack_attack_throttle' }
8
-
6
+
7
+ let(:throttle_one) { 'foo_throttle' }
8
+ let(:throttle_two) { 'bar_throttle' }
9
+ let(:throttle_three) { 'baz_throttle' }
10
+
9
11
  let(:app) do
10
- use_throttle = throttle
11
- Rack::Builder.new {
12
+ use_throttle = throttle_one
13
+ Rack::Builder.new do
12
14
  use Rack::Attack::RateLimit, throttle: use_throttle
13
- run lambda { |env| [200, {}, 'Hello, World!'] }
14
- }.to_app
15
-
15
+ run ->(_env) { [200, {}, 'Hello, World!'] }
16
+ end.to_app
16
17
  end
17
-
18
-
18
+
19
19
  context 'Throttle data not present from Rack::Attack' do
20
-
20
+
21
21
  before(:each) do
22
22
  get '/'
23
- end
24
-
23
+ end
24
+
25
25
  it 'should not create RateLimit headers' do
26
26
  last_response.header.key?('X-RateLimit-Limit').should be false
27
27
  last_response.header.key?('X-RateLimit-Remaining').should be false
28
28
  end
29
-
29
+
30
30
  end
31
31
 
32
32
  context 'Throttle data present from Rack::Attack' do
33
-
34
- let(:request_limit) { (1..10000).to_a.sample }
35
- let(:request_count) { (1..(request_limit-10)).to_a.sample }
36
-
37
- let(:rack_attack_throttle_data) do
38
- { "#{throttle}" => { count: request_count, limit: request_limit } }
39
- end
40
-
41
33
  before(:each) do
42
- get "/", {}, { "#{Rack::Attack::RateLimit::RACK_ATTACK_KEY}" => rack_attack_throttle_data }
34
+ get '/', {}, "#{Rack::Attack::RateLimit::RACK_ATTACK_KEY}" => rack_attack_throttle_data
43
35
  end
44
-
45
- it 'should include RateLimit headers' do
46
- last_response.header.key?('X-RateLimit-Limit').should be true
47
- last_response.header.key?('X-RateLimit-Remaining').should be true
48
- end
49
-
50
- it 'should return correct rate limit in header' do
51
- last_response.header['X-RateLimit-Limit'].to_i.should eq request_limit
36
+
37
+ let(:request_limit) { (1..10_000).to_a.sample }
38
+ let(:request_count) { (1..(request_limit - 10)).to_a.sample }
39
+
40
+ context 'one throttle only' do
41
+
42
+ let(:rack_attack_throttle_data) do
43
+ { "#{throttle_one}" => { count: request_count, limit: request_limit } }
44
+ end
45
+
46
+ it 'should include RateLimit headers' do
47
+ last_response.header.key?('X-RateLimit-Limit').should be true
48
+ last_response.header.key?('X-RateLimit-Remaining').should be true
49
+ end
50
+
51
+ it 'should return correct rate limit in header' do
52
+ last_response.header['X-RateLimit-Limit'].to_i.should eq request_limit
53
+ end
54
+
55
+ it 'should return correct remaining calls in header' do
56
+ last_response.header['X-RateLimit-Remaining'].to_i.should eq(request_limit - request_count)
57
+ end
52
58
  end
53
-
54
- it 'should return correct remaining calls in header' do
55
- last_response.header['X-RateLimit-Remaining'].to_i.should eq (request_limit-request_count)
59
+
60
+ context 'multiple throttles' do
61
+
62
+ let(:app) do
63
+ use_throttle = [throttle_one, throttle_two, throttle_three]
64
+ Rack::Builder.new do
65
+ use Rack::Attack::RateLimit, throttle: use_throttle
66
+ run ->(_env) { [200, {}, 'Hello, World!'] }
67
+ end.to_app
68
+ end
69
+
70
+ let(:request_limits) { 3.times.map { (1..10_000).to_a.sample } }
71
+ let(:request_counts) { 3.times.map { |index| (1..(request_limits[index] - 10)).to_a.sample } }
72
+
73
+ let(:rack_attack_throttle_data) do
74
+ data = {}
75
+ [throttle_one, throttle_two, throttle_three].each_with_index do |thr, thr_index|
76
+ data["#{thr}"] = { count: request_counts[thr_index], limit: request_limits[thr_index] }
77
+ end
78
+ data
79
+ end
80
+ it 'should include RateLimit headers' do
81
+ last_response.header.key?('X-RateLimit-Limit').should be true
82
+ last_response.header.key?('X-RateLimit-Remaining').should be true
83
+ end
84
+
85
+ describe 'header values' do
86
+ let(:request_differences) do
87
+ request_limits.map.each_with_index { |limit, index| limit - request_counts[index] }
88
+ end
89
+ let(:min_index) { request_differences.each_with_index.min.last }
90
+
91
+ it 'should return correct rate limit' do
92
+ last_response.header['X-RateLimit-Limit'].to_i.should eq request_limits[min_index]
93
+ end
94
+
95
+ it 'should return correct remaining calls' do
96
+ last_response.header['X-RateLimit-Remaining'].to_i.should eq(request_differences[min_index])
97
+ end
98
+ end
56
99
  end
57
100
  end
58
-
59
- end
101
+ end
@@ -6,7 +6,7 @@ require 'rack/attack/rate-limit'
6
6
 
7
7
  RSpec::configure do |config|
8
8
 
9
- config.treat_symbols_as_metadata_keys_with_true_values = true
10
9
  config.formatter = :documentation
11
10
  config.tty = true
11
+ config.color = true
12
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-attack-rate-limit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Byck
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-23 00:00:00.000000000 Z
11
+ date: 2014-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack