rack-attack 5.3.2 → 5.4.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/README.md +3 -3
- data/Rakefile +3 -2
- data/lib/rack/attack.rb +1 -0
- data/lib/rack/attack/cache.rb +4 -3
- data/lib/rack/attack/store_proxy.rb +1 -1
- data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +1 -22
- data/lib/rack/attack/throttle.rb +4 -1
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/stores/redis_spec.rb +42 -0
- data/spec/rack_attack_throttle_spec.rb +4 -4
- data/spec/spec_helper.rb +0 -1
- metadata +35 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 123608043cbaa1604ab2d7b06c056010decd274dd4a5b1fe8f2175cec766fa4d
|
4
|
+
data.tar.gz: 2bd3ca91293545b76221608f144c1a43a458fbd9e6f2e8ea2647bf561d8ea9a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '06388ad68edc65019740c9ee77347f817b0769e782d0f8564ac5ac6b9c7fa819fe2157a10d89b570c78c14ef5a86fe080fac30aba8ebdfed281f2073785f5685'
|
7
|
+
data.tar.gz: a15cc2cef9eebda52d662fe3eeee3f187d5b575c089a1fa2cf5e0179012499f6440889349a47108c4017f8b73dd18985655dc933baf8c031e08d73ea23d1dbba
|
data/README.md
CHANGED
@@ -303,13 +303,13 @@ Here's an example response that includes conventional `X-RateLimit-*` headers:
|
|
303
303
|
|
304
304
|
```ruby
|
305
305
|
Rack::Attack.throttled_response = lambda do |env|
|
306
|
-
now = Time.now
|
307
306
|
match_data = env['rack.attack.match_data']
|
307
|
+
now = match_data[:epoch_time]
|
308
308
|
|
309
309
|
headers = {
|
310
310
|
'X-RateLimit-Limit' => match_data[:limit].to_s,
|
311
311
|
'X-RateLimit-Remaining' => '0',
|
312
|
-
'X-RateLimit-Reset' => (now + (match_data[:period] - now
|
312
|
+
'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
|
313
313
|
}
|
314
314
|
|
315
315
|
[ 429, headers, ["Throttled\n"]]
|
@@ -320,7 +320,7 @@ end
|
|
320
320
|
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
|
321
321
|
|
322
322
|
```ruby
|
323
|
-
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
|
323
|
+
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l, :epoch_time => t }
|
324
324
|
```
|
325
325
|
|
326
326
|
## Logging & Instrumentation
|
data/Rakefile
CHANGED
data/lib/rack/attack.rb
CHANGED
@@ -17,6 +17,7 @@ class Rack::Attack
|
|
17
17
|
autoload :StoreProxy, 'rack/attack/store_proxy'
|
18
18
|
autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
|
19
19
|
autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
|
20
|
+
autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
|
20
21
|
autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
|
21
22
|
autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
|
22
23
|
autoload :Fail2Ban, 'rack/attack/fail2ban'
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -2,6 +2,7 @@ module Rack
|
|
2
2
|
class Attack
|
3
3
|
class Cache
|
4
4
|
attr_accessor :prefix
|
5
|
+
attr_reader :last_epoch_time
|
5
6
|
|
6
7
|
def initialize
|
7
8
|
self.store = ::Rails.cache if defined?(::Rails.cache)
|
@@ -41,10 +42,10 @@ module Rack
|
|
41
42
|
private
|
42
43
|
|
43
44
|
def key_and_expiry(unprefixed_key, period)
|
44
|
-
|
45
|
+
@last_epoch_time = Time.now.to_i
|
45
46
|
# Add 1 to expires_in to avoid timing error: https://git.io/i1PHXA
|
46
|
-
expires_in = (period - (
|
47
|
-
["#{prefix}:#{(
|
47
|
+
expires_in = (period - (@last_epoch_time % period) + 1).to_i
|
48
|
+
["#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
|
48
49
|
end
|
49
50
|
|
50
51
|
def do_count(key, expires_in)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Rack
|
2
2
|
class Attack
|
3
3
|
module StoreProxy
|
4
|
-
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisCacheStoreProxy].freeze
|
4
|
+
PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisProxy, RedisCacheStoreProxy].freeze
|
5
5
|
|
6
6
|
ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore']).freeze
|
7
7
|
ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Attack
|
7
|
+
module StoreProxy
|
8
|
+
class RedisProxy < SimpleDelegator
|
9
|
+
def initialize(*args)
|
10
|
+
if Gem::Version.new(Redis::VERSION) < Gem::Version.new("3")
|
11
|
+
warn 'RackAttack requires Redis gem >= 3.0.0.'
|
12
|
+
end
|
13
|
+
|
14
|
+
super(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.handle?(store)
|
18
|
+
defined?(::Redis) && store.is_a?(::Redis)
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(key)
|
22
|
+
get(key)
|
23
|
+
rescue Redis::BaseError
|
24
|
+
end
|
25
|
+
|
26
|
+
def write(key, value, options = {})
|
27
|
+
if (expires_in = options[:expires_in])
|
28
|
+
setex(key, expires_in, value)
|
29
|
+
else
|
30
|
+
set(key, value)
|
31
|
+
end
|
32
|
+
rescue Redis::BaseError
|
33
|
+
end
|
34
|
+
|
35
|
+
def increment(key, amount, options = {})
|
36
|
+
count = nil
|
37
|
+
|
38
|
+
pipelined do
|
39
|
+
count = incrby(key, amount)
|
40
|
+
expire(key, options[:expires_in]) if options[:expires_in]
|
41
|
+
end
|
42
|
+
|
43
|
+
count.value if count
|
44
|
+
rescue Redis::BaseError
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key, _options = {})
|
48
|
+
del(key)
|
49
|
+
rescue Redis::BaseError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -3,15 +3,11 @@ require 'delegate'
|
|
3
3
|
module Rack
|
4
4
|
class Attack
|
5
5
|
module StoreProxy
|
6
|
-
class RedisStoreProxy <
|
6
|
+
class RedisStoreProxy < RedisProxy
|
7
7
|
def self.handle?(store)
|
8
8
|
defined?(::Redis::Store) && store.is_a?(::Redis::Store)
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize(store)
|
12
|
-
super(store)
|
13
|
-
end
|
14
|
-
|
15
11
|
def read(key)
|
16
12
|
get(key, raw: true)
|
17
13
|
rescue Redis::BaseError
|
@@ -25,23 +21,6 @@ module Rack
|
|
25
21
|
end
|
26
22
|
rescue Redis::BaseError
|
27
23
|
end
|
28
|
-
|
29
|
-
def increment(key, amount, options = {})
|
30
|
-
count = nil
|
31
|
-
|
32
|
-
pipelined do
|
33
|
-
count = incrby(key, amount)
|
34
|
-
expire(key, options[:expires_in]) if options[:expires_in]
|
35
|
-
end
|
36
|
-
|
37
|
-
count.value if count
|
38
|
-
rescue Redis::BaseError
|
39
|
-
end
|
40
|
-
|
41
|
-
def delete(key, _options = {})
|
42
|
-
del(key)
|
43
|
-
rescue Redis::BaseError
|
44
|
-
end
|
45
24
|
end
|
46
25
|
end
|
47
26
|
end
|
data/lib/rack/attack/throttle.rb
CHANGED
@@ -26,12 +26,15 @@ module Rack
|
|
26
26
|
current_limit = limit.respond_to?(:call) ? limit.call(request) : limit
|
27
27
|
key = "#{name}:#{discriminator}"
|
28
28
|
count = cache.count(key, current_period)
|
29
|
+
epoch_time = cache.last_epoch_time
|
29
30
|
|
30
31
|
data = {
|
31
32
|
:count => count,
|
32
33
|
:period => current_period,
|
33
|
-
:limit => current_limit
|
34
|
+
:limit => current_limit,
|
35
|
+
:epoch_time => epoch_time
|
34
36
|
}
|
37
|
+
|
35
38
|
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
36
39
|
|
37
40
|
(count > current_limit).tap do |throttled|
|
data/lib/rack/attack/version.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
if defined?(::Redis)
|
6
|
+
require_relative "../../support/cache_store_helper"
|
7
|
+
require "timecop"
|
8
|
+
|
9
|
+
describe "Plain redis as a cache backend" do
|
10
|
+
before do
|
11
|
+
Rack::Attack.cache.store = Redis.new
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
Rack::Attack.cache.store.flushdb
|
16
|
+
end
|
17
|
+
|
18
|
+
it_works_for_cache_backed_features
|
19
|
+
|
20
|
+
it "doesn't leak keys" do
|
21
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
|
22
|
+
request.ip
|
23
|
+
end
|
24
|
+
|
25
|
+
key = nil
|
26
|
+
|
27
|
+
# Freeze time during these statement to be sure that the key used by rack attack is the same
|
28
|
+
# we pre-calculate in local variable `key`
|
29
|
+
Timecop.freeze do
|
30
|
+
key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
|
31
|
+
|
32
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
33
|
+
end
|
34
|
+
|
35
|
+
assert Rack::Attack.cache.store.get(key)
|
36
|
+
|
37
|
+
sleep 2.1
|
38
|
+
|
39
|
+
assert_nil Rack::Attack.cache.store.get(key)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -20,7 +20,7 @@ describe 'Rack::Attack.throttle' do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'should populate throttle data' do
|
23
|
-
data = { :count => 1, :limit => 1, :period => @period }
|
23
|
+
data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
|
24
24
|
last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
|
25
25
|
end
|
26
26
|
end
|
@@ -37,7 +37,7 @@ describe 'Rack::Attack.throttle' do
|
|
37
37
|
it 'should tag the env' do
|
38
38
|
last_request.env['rack.attack.matched'].must_equal 'ip/sec'
|
39
39
|
last_request.env['rack.attack.match_type'].must_equal :throttle
|
40
|
-
last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period)
|
40
|
+
last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i)
|
41
41
|
last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
|
42
42
|
end
|
43
43
|
|
@@ -65,7 +65,7 @@ describe 'Rack::Attack.throttle with limit as proc' do
|
|
65
65
|
end
|
66
66
|
|
67
67
|
it 'should populate throttle data' do
|
68
|
-
data = { :count => 1, :limit => 1, :period => @period }
|
68
|
+
data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
|
69
69
|
last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
|
70
70
|
end
|
71
71
|
end
|
@@ -89,7 +89,7 @@ describe 'Rack::Attack.throttle with period as proc' do
|
|
89
89
|
end
|
90
90
|
|
91
91
|
it 'should populate throttle data' do
|
92
|
-
data = { :count => 1, :limit => 1, :period => @period }
|
92
|
+
data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
|
93
93
|
last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
|
94
94
|
end
|
95
95
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-07-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
19
|
+
version: '1.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
29
|
+
version: '1.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: appraisal
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +44,20 @@ dependencies:
|
|
38
44
|
- - "~>"
|
39
45
|
- !ruby/object:Gem::Version
|
40
46
|
version: '2.2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.16'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.16'
|
41
61
|
- !ruby/object:Gem::Dependency
|
42
62
|
name: minitest
|
43
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -140,30 +160,30 @@ dependencies:
|
|
140
160
|
name: actionpack
|
141
161
|
requirement: !ruby/object:Gem::Requirement
|
142
162
|
requirements:
|
143
|
-
- - "
|
163
|
+
- - "~>"
|
144
164
|
- !ruby/object:Gem::Version
|
145
|
-
version:
|
165
|
+
version: '5.2'
|
146
166
|
type: :development
|
147
167
|
prerelease: false
|
148
168
|
version_requirements: !ruby/object:Gem::Requirement
|
149
169
|
requirements:
|
150
|
-
- - "
|
170
|
+
- - "~>"
|
151
171
|
- !ruby/object:Gem::Version
|
152
|
-
version:
|
172
|
+
version: '5.2'
|
153
173
|
- !ruby/object:Gem::Dependency
|
154
174
|
name: activesupport
|
155
175
|
requirement: !ruby/object:Gem::Requirement
|
156
176
|
requirements:
|
157
|
-
- - "
|
177
|
+
- - "~>"
|
158
178
|
- !ruby/object:Gem::Version
|
159
|
-
version:
|
179
|
+
version: '5.2'
|
160
180
|
type: :development
|
161
181
|
prerelease: false
|
162
182
|
version_requirements: !ruby/object:Gem::Requirement
|
163
183
|
requirements:
|
164
|
-
- - "
|
184
|
+
- - "~>"
|
165
185
|
- !ruby/object:Gem::Version
|
166
|
-
version:
|
186
|
+
version: '5.2'
|
167
187
|
description: A rack middleware for throttling and blocking abusive requests
|
168
188
|
email: aaron@ktheory.com
|
169
189
|
executables: []
|
@@ -185,6 +205,7 @@ files:
|
|
185
205
|
- lib/rack/attack/store_proxy/dalli_proxy.rb
|
186
206
|
- lib/rack/attack/store_proxy/mem_cache_proxy.rb
|
187
207
|
- lib/rack/attack/store_proxy/redis_cache_store_proxy.rb
|
208
|
+
- lib/rack/attack/store_proxy/redis_proxy.rb
|
188
209
|
- lib/rack/attack/store_proxy/redis_store_proxy.rb
|
189
210
|
- lib/rack/attack/throttle.rb
|
190
211
|
- lib/rack/attack/track.rb
|
@@ -212,6 +233,7 @@ files:
|
|
212
233
|
- spec/acceptance/stores/active_support_redis_store_spec.rb
|
213
234
|
- spec/acceptance/stores/connection_pool_dalli_client_spec.rb
|
214
235
|
- spec/acceptance/stores/dalli_client_spec.rb
|
236
|
+
- spec/acceptance/stores/redis_spec.rb
|
215
237
|
- spec/acceptance/stores/redis_store_spec.rb
|
216
238
|
- spec/acceptance/throttling_spec.rb
|
217
239
|
- spec/acceptance/track_spec.rb
|
@@ -284,6 +306,7 @@ test_files:
|
|
284
306
|
- spec/acceptance/stores/active_support_dalli_store_spec.rb
|
285
307
|
- spec/acceptance/stores/redis_store_spec.rb
|
286
308
|
- spec/acceptance/stores/dalli_client_spec.rb
|
309
|
+
- spec/acceptance/stores/redis_spec.rb
|
287
310
|
- spec/acceptance/customizing_blocked_response_spec.rb
|
288
311
|
- spec/spec_helper.rb
|
289
312
|
- spec/allow2ban_spec.rb
|