rack-attack-rate-limit 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|