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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +6 -6
- data/lib/rack/attack/rate-limit.rb +33 -9
- data/lib/rack/attack/rate-limit/version.rb +1 -1
- data/spec/rack/attack/rate-limit_spec.rb +79 -37
- data/spec/spec_helper.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d10bdcfab72d5a2a4b398d240d1944189da6318
|
4
|
+
data.tar.gz: 86f9d556c90a96e7d5a5f2e57231a8c24b602203
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 037cb6dee3e51d86c6f8b29f16fa6eeb64c304debabb1f8381e45bdacb239bce77b03fa11aae869bede31ca8e8955c113fc09af09d047376f315b07564761e2a
|
7
|
+
data.tar.gz: 97a2e8aeb2734b1c3681bd196c72b82be999641ec6a7def2ff0f76f3573fb6ea87146bf69a518cd257532a1902d8ac0b7a29b380c1bad89a99a06af03522a635
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -26,7 +26,7 @@ bundle
|
|
26
26
|
|
27
27
|
## Usage
|
28
28
|
|
29
|
-
Rack::Attack::RateLimit expects
|
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
|
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
|
-
|
54
|
-
headers['X-RateLimit-
|
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(
|
66
|
-
|
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(
|
75
|
-
rate_limit_limit(
|
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
|
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].
|
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,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(:
|
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 =
|
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
|
14
|
-
|
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
|
34
|
+
get '/', {}, "#{Rack::Attack::RateLimit::RACK_ATTACK_KEY}" => rack_attack_throttle_data
|
43
35
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
55
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2014-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|