rack-attack 6.2.2 → 6.3.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 +5 -0
- data/bin/setup +8 -0
- data/lib/rack/attack.rb +43 -105
- data/lib/rack/attack/cache.rb +11 -0
- data/lib/rack/attack/configuration.rb +107 -0
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +6 -22
- data/lib/rack/attack/store_proxy/redis_proxy.rb +14 -1
- data/lib/rack/attack/throttle.rb +28 -12
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/throttling_spec.rb +19 -1
- data/spec/integration/offline_spec.rb +34 -1
- data/spec/rack_attack_spec.rb +22 -0
- data/spec/rack_attack_throttle_spec.rb +0 -4
- data/spec/spec_helper.rb +0 -5
- metadata +35 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cba47e380843d184fd3df1af08b252aca3fc2411de7ccbd62a9b7da6ee933b72
|
4
|
+
data.tar.gz: 0dc5300a553830ca7e1cfa84de814fd743e608b9ec0140fd6b1145c421085ba7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71fd5eace9c851dab06317f3f4f4f28a2cbc20dd20c663228fbd073a052b4f30c4de87a06109ce9cf6b7395fc547b0c99b6f4e579285a699be40a93a9511452f
|
7
|
+
data.tar.gz: 5139f0be932f94273dba2d8d59ccbcb0e14ca92656b68d126099a124f04160c2c426e72f330b9e3c5dd8ed560f2fb292aa68dde2c32f2238d36c8c7e95422d01
|
data/README.md
CHANGED
@@ -342,6 +342,11 @@ end
|
|
342
342
|
While Rack::Attack's primary focus is minimizing harm from abusive clients, it
|
343
343
|
can also be used to return rate limit data that's helpful for well-behaved clients.
|
344
344
|
|
345
|
+
If you want to return to user how many seconds to wait until he can start sending requests again, this can be done through enabling `Retry-After` header:
|
346
|
+
```ruby
|
347
|
+
Rack::Attack.throttled_response_retry_after_header = true
|
348
|
+
```
|
349
|
+
|
345
350
|
Here's an example response that includes conventional `RateLimit-*` headers:
|
346
351
|
|
347
352
|
```ruby
|
data/bin/setup
ADDED
data/lib/rack/attack.rb
CHANGED
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'rack'
|
4
4
|
require 'forwardable'
|
5
|
+
require 'rack/attack/cache'
|
6
|
+
require 'rack/attack/configuration'
|
5
7
|
require 'rack/attack/path_normalizer'
|
6
8
|
require 'rack/attack/request'
|
7
|
-
require "ipaddr"
|
8
9
|
|
9
10
|
require 'rack/attack/railtie' if defined?(::Rails)
|
10
11
|
|
@@ -13,8 +14,8 @@ module Rack
|
|
13
14
|
class Error < StandardError; end
|
14
15
|
class MisconfiguredStoreError < Error; end
|
15
16
|
class MissingStoreError < Error; end
|
17
|
+
class IncompatibleStoreError < Error; end
|
16
18
|
|
17
|
-
autoload :Cache, 'rack/attack/cache'
|
18
19
|
autoload :Check, 'rack/attack/check'
|
19
20
|
autoload :Throttle, 'rack/attack/throttle'
|
20
21
|
autoload :Safelist, 'rack/attack/safelist'
|
@@ -31,82 +32,8 @@ module Rack
|
|
31
32
|
autoload :Allow2Ban, 'rack/attack/allow2ban'
|
32
33
|
|
33
34
|
class << self
|
34
|
-
attr_accessor :enabled, :notifier
|
35
|
-
|
36
|
-
|
37
|
-
def safelist(name = nil, &block)
|
38
|
-
safelist = Safelist.new(name, &block)
|
39
|
-
|
40
|
-
if name
|
41
|
-
safelists[name] = safelist
|
42
|
-
else
|
43
|
-
anonymous_safelists << safelist
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def blocklist(name = nil, &block)
|
48
|
-
blocklist = Blocklist.new(name, &block)
|
49
|
-
|
50
|
-
if name
|
51
|
-
blocklists[name] = blocklist
|
52
|
-
else
|
53
|
-
anonymous_blocklists << blocklist
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def blocklist_ip(ip_address)
|
58
|
-
anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
59
|
-
end
|
60
|
-
|
61
|
-
def safelist_ip(ip_address)
|
62
|
-
anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
63
|
-
end
|
64
|
-
|
65
|
-
def throttle(name, options, &block)
|
66
|
-
throttles[name] = Throttle.new(name, options, &block)
|
67
|
-
end
|
68
|
-
|
69
|
-
def track(name, options = {}, &block)
|
70
|
-
tracks[name] = Track.new(name, options, &block)
|
71
|
-
end
|
72
|
-
|
73
|
-
def safelists
|
74
|
-
@safelists ||= {}
|
75
|
-
end
|
76
|
-
|
77
|
-
def blocklists
|
78
|
-
@blocklists ||= {}
|
79
|
-
end
|
80
|
-
|
81
|
-
def throttles
|
82
|
-
@throttles ||= {}
|
83
|
-
end
|
84
|
-
|
85
|
-
def tracks
|
86
|
-
@tracks ||= {}
|
87
|
-
end
|
88
|
-
|
89
|
-
def safelisted?(request)
|
90
|
-
anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
|
91
|
-
safelists.any? { |_name, safelist| safelist.matched_by?(request) }
|
92
|
-
end
|
93
|
-
|
94
|
-
def blocklisted?(request)
|
95
|
-
anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
|
96
|
-
blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
|
97
|
-
end
|
98
|
-
|
99
|
-
def throttled?(request)
|
100
|
-
throttles.any? do |_name, throttle|
|
101
|
-
throttle.matched_by?(request)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def tracked?(request)
|
106
|
-
tracks.each_value do |track|
|
107
|
-
track.matched_by?(request)
|
108
|
-
end
|
109
|
-
end
|
35
|
+
attr_accessor :enabled, :notifier
|
36
|
+
attr_reader :configuration
|
110
37
|
|
111
38
|
def instrument(request)
|
112
39
|
if notifier
|
@@ -122,34 +49,48 @@ module Rack
|
|
122
49
|
@cache ||= Cache.new
|
123
50
|
end
|
124
51
|
|
125
|
-
def clear_configuration
|
126
|
-
@safelists = {}
|
127
|
-
@blocklists = {}
|
128
|
-
@throttles = {}
|
129
|
-
@tracks = {}
|
130
|
-
self.anonymous_blocklists = []
|
131
|
-
self.anonymous_safelists = []
|
132
|
-
end
|
133
|
-
|
134
52
|
def clear!
|
135
53
|
warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
|
136
|
-
clear_configuration
|
137
|
-
end
|
54
|
+
@configuration.clear_configuration
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset!
|
58
|
+
cache.reset!
|
59
|
+
end
|
60
|
+
|
61
|
+
extend Forwardable
|
62
|
+
def_delegators(
|
63
|
+
:@configuration,
|
64
|
+
:safelist,
|
65
|
+
:blocklist,
|
66
|
+
:blocklist_ip,
|
67
|
+
:safelist_ip,
|
68
|
+
:throttle,
|
69
|
+
:track,
|
70
|
+
:blocklisted_response,
|
71
|
+
:blocklisted_response=,
|
72
|
+
:throttled_response,
|
73
|
+
:throttled_response=,
|
74
|
+
:throttled_response_retry_after_header,
|
75
|
+
:throttled_response_retry_after_header=,
|
76
|
+
:clear_configuration,
|
77
|
+
:safelists,
|
78
|
+
:blocklists,
|
79
|
+
:throttles,
|
80
|
+
:tracks
|
81
|
+
)
|
138
82
|
end
|
139
83
|
|
140
84
|
# Set defaults
|
141
85
|
@enabled = true
|
142
|
-
@anonymous_blocklists = []
|
143
|
-
@anonymous_safelists = []
|
144
86
|
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
145
|
-
@
|
146
|
-
|
147
|
-
|
148
|
-
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
|
149
|
-
end
|
87
|
+
@configuration = Configuration.new
|
88
|
+
|
89
|
+
attr_reader :configuration
|
150
90
|
|
151
91
|
def initialize(app)
|
152
92
|
@app = app
|
93
|
+
@configuration = self.class.configuration
|
153
94
|
end
|
154
95
|
|
155
96
|
def call(env)
|
@@ -159,19 +100,16 @@ module Rack
|
|
159
100
|
env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
|
160
101
|
request = Rack::Attack::Request.new(env)
|
161
102
|
|
162
|
-
if safelisted?(request)
|
103
|
+
if configuration.safelisted?(request)
|
163
104
|
@app.call(env)
|
164
|
-
elsif blocklisted?(request)
|
165
|
-
|
166
|
-
elsif throttled?(request)
|
167
|
-
|
105
|
+
elsif configuration.blocklisted?(request)
|
106
|
+
configuration.blocklisted_response.call(env)
|
107
|
+
elsif configuration.throttled?(request)
|
108
|
+
configuration.throttled_response.call(env)
|
168
109
|
else
|
169
|
-
tracked?(request)
|
110
|
+
configuration.tracked?(request)
|
170
111
|
@app.call(env)
|
171
112
|
end
|
172
113
|
end
|
173
|
-
|
174
|
-
extend Forwardable
|
175
|
-
def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
|
176
114
|
end
|
177
115
|
end
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -41,6 +41,17 @@ module Rack
|
|
41
41
|
store.delete("#{prefix}:#{unprefixed_key}")
|
42
42
|
end
|
43
43
|
|
44
|
+
def reset!
|
45
|
+
if store.respond_to?(:delete_matched)
|
46
|
+
store.delete_matched("#{prefix}*")
|
47
|
+
else
|
48
|
+
raise(
|
49
|
+
Rack::Attack::IncompatibleStoreError,
|
50
|
+
"Configured store #{store.class.name} doesn't respond to #delete_matched method"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
44
55
|
private
|
45
56
|
|
46
57
|
def key_and_expiry(unprefixed_key, period)
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Attack
|
7
|
+
class Configuration
|
8
|
+
DEFAULT_BLOCKLISTED_RESPONSE = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
|
9
|
+
|
10
|
+
DEFAULT_THROTTLED_RESPONSE = lambda do |env|
|
11
|
+
if Rack::Attack.configuration.throttled_response_retry_after_header
|
12
|
+
match_data = env['rack.attack.match_data']
|
13
|
+
now = match_data[:epoch_time]
|
14
|
+
retry_after = match_data[:period] - (now % match_data[:period])
|
15
|
+
|
16
|
+
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
|
17
|
+
else
|
18
|
+
[429, { 'Content-Type' => 'text/plain' }, ["Retry later\n"]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :safelists, :blocklists, :throttles, :anonymous_blocklists, :anonymous_safelists
|
23
|
+
attr_accessor :blocklisted_response, :throttled_response, :throttled_response_retry_after_header
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
set_defaults
|
27
|
+
end
|
28
|
+
|
29
|
+
def safelist(name = nil, &block)
|
30
|
+
safelist = Safelist.new(name, &block)
|
31
|
+
|
32
|
+
if name
|
33
|
+
@safelists[name] = safelist
|
34
|
+
else
|
35
|
+
@anonymous_safelists << safelist
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def blocklist(name = nil, &block)
|
40
|
+
blocklist = Blocklist.new(name, &block)
|
41
|
+
|
42
|
+
if name
|
43
|
+
@blocklists[name] = blocklist
|
44
|
+
else
|
45
|
+
@anonymous_blocklists << blocklist
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def blocklist_ip(ip_address)
|
50
|
+
@anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def safelist_ip(ip_address)
|
54
|
+
@anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def throttle(name, options, &block)
|
58
|
+
@throttles[name] = Throttle.new(name, options, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def track(name, options = {}, &block)
|
62
|
+
@tracks[name] = Track.new(name, options, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def safelisted?(request)
|
66
|
+
@anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
|
67
|
+
@safelists.any? { |_name, safelist| safelist.matched_by?(request) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def blocklisted?(request)
|
71
|
+
@anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
|
72
|
+
@blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def throttled?(request)
|
76
|
+
@throttles.any? do |_name, throttle|
|
77
|
+
throttle.matched_by?(request)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def tracked?(request)
|
82
|
+
@tracks.each_value do |track|
|
83
|
+
track.matched_by?(request)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def clear_configuration
|
88
|
+
set_defaults
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def set_defaults
|
94
|
+
@safelists = {}
|
95
|
+
@blocklists = {}
|
96
|
+
@throttles = {}
|
97
|
+
@tracks = {}
|
98
|
+
@anonymous_blocklists = []
|
99
|
+
@anonymous_safelists = []
|
100
|
+
@throttled_response_retry_after_header = false
|
101
|
+
|
102
|
+
@blocklisted_response = DEFAULT_BLOCKLISTED_RESPONSE
|
103
|
+
@throttled_response = DEFAULT_THROTTLED_RESPONSE
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -15,33 +15,17 @@ module Rack
|
|
15
15
|
#
|
16
16
|
# So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
|
17
17
|
# the counter. After that we continue using the original RedisCacheStore#increment.
|
18
|
-
|
19
|
-
|
20
|
-
write(name, amount, options)
|
18
|
+
if options[:expires_in] && !read(name)
|
19
|
+
write(name, amount, options)
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
21
|
+
amount
|
22
|
+
else
|
23
|
+
super
|
26
24
|
end
|
27
25
|
end
|
28
26
|
|
29
|
-
def read(*_args)
|
30
|
-
rescuing { super }
|
31
|
-
end
|
32
|
-
|
33
27
|
def write(name, value, options = {})
|
34
|
-
|
35
|
-
super(name, value, options.merge!(raw: true))
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def rescuing
|
42
|
-
yield
|
43
|
-
rescue Redis::BaseError
|
44
|
-
nil
|
28
|
+
super(name, value, options.merge!(raw: true))
|
45
29
|
end
|
46
30
|
end
|
47
31
|
end
|
@@ -43,11 +43,24 @@ module Rack
|
|
43
43
|
rescuing { del(key) }
|
44
44
|
end
|
45
45
|
|
46
|
+
def delete_matched(matcher, _options = nil)
|
47
|
+
cursor = "0"
|
48
|
+
|
49
|
+
rescuing do
|
50
|
+
# Fetch keys in batches using SCAN to avoid blocking the Redis server.
|
51
|
+
loop do
|
52
|
+
cursor, keys = scan(cursor, match: matcher, count: 1000)
|
53
|
+
del(*keys) unless keys.empty?
|
54
|
+
break if cursor == "0"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
46
59
|
private
|
47
60
|
|
48
61
|
def rescuing
|
49
62
|
yield
|
50
|
-
rescue Redis::
|
63
|
+
rescue Redis::BaseConnectionError
|
51
64
|
nil
|
52
65
|
end
|
53
66
|
end
|
data/lib/rack/attack/throttle.rb
CHANGED
@@ -23,34 +23,50 @@ module Rack
|
|
23
23
|
|
24
24
|
def matched_by?(request)
|
25
25
|
discriminator = block.call(request)
|
26
|
+
|
26
27
|
return false unless discriminator
|
27
28
|
|
28
|
-
current_period
|
29
|
-
current_limit
|
30
|
-
|
31
|
-
count = cache.count(key, current_period)
|
32
|
-
epoch_time = cache.last_epoch_time
|
29
|
+
current_period = period_for(request)
|
30
|
+
current_limit = limit_for(request)
|
31
|
+
count = cache.count("#{name}:#{discriminator}", current_period)
|
33
32
|
|
34
33
|
data = {
|
35
34
|
discriminator: discriminator,
|
36
35
|
count: count,
|
37
36
|
period: current_period,
|
38
37
|
limit: current_limit,
|
39
|
-
epoch_time:
|
38
|
+
epoch_time: cache.last_epoch_time
|
40
39
|
}
|
41
40
|
|
42
|
-
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
43
|
-
|
44
41
|
(count > current_limit).tap do |throttled|
|
42
|
+
annotate_request_with_throttle_data(request, data)
|
45
43
|
if throttled
|
46
|
-
request
|
47
|
-
request.env['rack.attack.match_discriminator'] = discriminator
|
48
|
-
request.env['rack.attack.match_type'] = type
|
49
|
-
request.env['rack.attack.match_data'] = data
|
44
|
+
annotate_request_with_matched_data(request, data)
|
50
45
|
Rack::Attack.instrument(request)
|
51
46
|
end
|
52
47
|
end
|
53
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def period_for(request)
|
53
|
+
period.respond_to?(:call) ? period.call(request) : period
|
54
|
+
end
|
55
|
+
|
56
|
+
def limit_for(request)
|
57
|
+
limit.respond_to?(:call) ? limit.call(request) : limit
|
58
|
+
end
|
59
|
+
|
60
|
+
def annotate_request_with_throttle_data(request, data)
|
61
|
+
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
62
|
+
end
|
63
|
+
|
64
|
+
def annotate_request_with_matched_data(request, data)
|
65
|
+
request.env['rack.attack.matched'] = name
|
66
|
+
request.env['rack.attack.match_discriminator'] = data[:discriminator]
|
67
|
+
request.env['rack.attack.match_type'] = type
|
68
|
+
request.env['rack.attack.match_data'] = data
|
69
|
+
end
|
54
70
|
end
|
55
71
|
end
|
56
72
|
end
|
data/lib/rack/attack/version.rb
CHANGED
@@ -20,7 +20,7 @@ describe "#throttle" do
|
|
20
20
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
21
21
|
|
22
22
|
assert_equal 429, last_response.status
|
23
|
-
|
23
|
+
assert_nil last_response.headers["Retry-After"]
|
24
24
|
assert_equal "Retry later\n", last_response.body
|
25
25
|
|
26
26
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -34,6 +34,24 @@ describe "#throttle" do
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
it "returns correct Retry-After header if enabled" do
|
38
|
+
Rack::Attack.throttled_response_retry_after_header = true
|
39
|
+
|
40
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
41
|
+
request.ip
|
42
|
+
end
|
43
|
+
|
44
|
+
Timecop.freeze(Time.at(0)) do
|
45
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
46
|
+
assert_equal 200, last_response.status
|
47
|
+
end
|
48
|
+
|
49
|
+
Timecop.freeze(Time.at(25)) do
|
50
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
51
|
+
assert_equal "35", last_response.headers["Retry-After"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
37
55
|
it "supports limit to be dynamic" do
|
38
56
|
# Could be used to have different rate limits for authorized
|
39
57
|
# vs general requests
|
@@ -13,7 +13,11 @@ OfflineExamples = Minitest::SharedExamples.new do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'should count' do
|
16
|
-
@cache.
|
16
|
+
@cache.count('cache-test-key', 1)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should delete' do
|
20
|
+
@cache.delete('cache-test-key')
|
17
21
|
end
|
18
22
|
end
|
19
23
|
|
@@ -29,6 +33,18 @@ if defined?(::ActiveSupport::Cache::RedisStore)
|
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
36
|
+
if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
|
37
|
+
describe 'when Redis is offline' do
|
38
|
+
include OfflineExamples
|
39
|
+
|
40
|
+
before do
|
41
|
+
@cache = Rack::Attack::Cache.new
|
42
|
+
# Use presumably unused port for Redis client
|
43
|
+
@cache.store = ActiveSupport::Cache::RedisCacheStore.new(host: '127.0.0.1', port: 3333)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
32
48
|
if defined?(::Dalli)
|
33
49
|
describe 'when Memcached is offline' do
|
34
50
|
include OfflineExamples
|
@@ -46,6 +62,23 @@ if defined?(::Dalli)
|
|
46
62
|
end
|
47
63
|
end
|
48
64
|
|
65
|
+
if defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore)
|
66
|
+
describe 'when Memcached is offline' do
|
67
|
+
include OfflineExamples
|
68
|
+
|
69
|
+
before do
|
70
|
+
Dalli.logger.level = Logger::FATAL
|
71
|
+
|
72
|
+
@cache = Rack::Attack::Cache.new
|
73
|
+
@cache.store = ActiveSupport::Cache::MemCacheStore.new('127.0.0.1:22122')
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
Dalli.logger.level = Logger::INFO
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
49
82
|
if defined?(Redis)
|
50
83
|
describe 'when Redis is offline' do
|
51
84
|
include OfflineExamples
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -99,4 +99,26 @@ describe 'Rack::Attack' do
|
|
99
99
|
end
|
100
100
|
end
|
101
101
|
end
|
102
|
+
|
103
|
+
describe 'reset!' do
|
104
|
+
it 'raises an error when is not supported by cache store' do
|
105
|
+
Rack::Attack.cache.store = Class.new
|
106
|
+
assert_raises(Rack::Attack::IncompatibleStoreError) do
|
107
|
+
Rack::Attack.reset!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if defined?(Redis)
|
112
|
+
it 'should delete rack attack keys' do
|
113
|
+
redis = Redis.new
|
114
|
+
redis.set('key', 'value')
|
115
|
+
redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
|
116
|
+
Rack::Attack.cache.store = redis
|
117
|
+
Rack::Attack.reset!
|
118
|
+
|
119
|
+
_(redis.get('key')).must_equal 'value'
|
120
|
+
_(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
102
124
|
end
|
@@ -57,10 +57,6 @@ describe 'Rack::Attack.throttle' do
|
|
57
57
|
|
58
58
|
_(last_request.env['rack.attack.match_discriminator']).must_equal('1.2.3.4')
|
59
59
|
end
|
60
|
-
|
61
|
-
it 'should set a Retry-After header' do
|
62
|
-
_(last_response.headers['Retry-After']).must_equal @period.to_s
|
63
|
-
end
|
64
60
|
end
|
65
61
|
end
|
66
62
|
|
data/spec/spec_helper.rb
CHANGED
@@ -30,16 +30,11 @@ class MiniTest::Spec
|
|
30
30
|
|
31
31
|
before do
|
32
32
|
Rails.cache = nil
|
33
|
-
@_original_throttled_response = Rack::Attack.throttled_response
|
34
|
-
@_original_blocklisted_response = Rack::Attack.blocklisted_response
|
35
33
|
end
|
36
34
|
|
37
35
|
after do
|
38
36
|
Rack::Attack.clear_configuration
|
39
37
|
Rack::Attack.instance_variable_set(:@cache, nil)
|
40
|
-
|
41
|
-
Rack::Attack.throttled_response = @_original_throttled_response
|
42
|
-
Rack::Attack.blocklisted_response = @_original_blocklisted_response
|
43
38
|
end
|
44
39
|
|
45
40
|
def app
|
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: 6.
|
4
|
+
version: 6.3.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:
|
11
|
+
date: 2020-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -126,14 +126,14 @@ dependencies:
|
|
126
126
|
requirements:
|
127
127
|
- - '='
|
128
128
|
- !ruby/object:Gem::Version
|
129
|
-
version: 0.
|
129
|
+
version: 0.78.0
|
130
130
|
type: :development
|
131
131
|
prerelease: false
|
132
132
|
version_requirements: !ruby/object:Gem::Requirement
|
133
133
|
requirements:
|
134
134
|
- - '='
|
135
135
|
- !ruby/object:Gem::Version
|
136
|
-
version: 0.
|
136
|
+
version: 0.78.0
|
137
137
|
- !ruby/object:Gem::Dependency
|
138
138
|
name: rubocop-performance
|
139
139
|
requirement: !ruby/object:Gem::Requirement
|
@@ -204,11 +204,13 @@ extra_rdoc_files: []
|
|
204
204
|
files:
|
205
205
|
- README.md
|
206
206
|
- Rakefile
|
207
|
+
- bin/setup
|
207
208
|
- lib/rack/attack.rb
|
208
209
|
- lib/rack/attack/allow2ban.rb
|
209
210
|
- lib/rack/attack/blocklist.rb
|
210
211
|
- lib/rack/attack/cache.rb
|
211
212
|
- lib/rack/attack/check.rb
|
213
|
+
- lib/rack/attack/configuration.rb
|
212
214
|
- lib/rack/attack/fail2ban.rb
|
213
215
|
- lib/rack/attack/path_normalizer.rb
|
214
216
|
- lib/rack/attack/railtie.rb
|
@@ -289,50 +291,50 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
289
291
|
- !ruby/object:Gem::Version
|
290
292
|
version: '0'
|
291
293
|
requirements: []
|
292
|
-
rubygems_version: 3.1.
|
294
|
+
rubygems_version: 3.1.2
|
293
295
|
signing_key:
|
294
296
|
specification_version: 4
|
295
297
|
summary: Block & throttle abusive requests
|
296
298
|
test_files:
|
297
|
-
- spec/rack_attack_spec.rb
|
298
|
-
- spec/fail2ban_spec.rb
|
299
|
-
- spec/allow2ban_spec.rb
|
300
|
-
- spec/support/cache_store_helper.rb
|
301
|
-
- spec/rack_attack_instrumentation_spec.rb
|
302
|
-
- spec/rack_attack_throttle_spec.rb
|
303
299
|
- spec/integration/offline_spec.rb
|
304
|
-
- spec/
|
305
|
-
- spec/acceptance/
|
306
|
-
- spec/acceptance/allow2ban_spec.rb
|
300
|
+
- spec/rack_attack_path_normalizer_spec.rb
|
301
|
+
- spec/acceptance/safelisting_subnet_spec.rb
|
307
302
|
- spec/acceptance/rails_middleware_spec.rb
|
308
|
-
- spec/acceptance/throttling_spec.rb
|
309
303
|
- spec/acceptance/track_throttle_spec.rb
|
304
|
+
- spec/acceptance/cache_store_config_for_fail2ban_spec.rb
|
305
|
+
- spec/acceptance/cache_store_config_with_rails_spec.rb
|
306
|
+
- spec/acceptance/cache_store_config_for_allow2ban_spec.rb
|
307
|
+
- spec/acceptance/safelisting_ip_spec.rb
|
308
|
+
- spec/acceptance/track_spec.rb
|
310
309
|
- spec/acceptance/blocking_subnet_spec.rb
|
311
310
|
- spec/acceptance/blocking_ip_spec.rb
|
312
|
-
- spec/acceptance/
|
313
|
-
- spec/acceptance/
|
314
|
-
- spec/acceptance/
|
311
|
+
- spec/acceptance/allow2ban_spec.rb
|
312
|
+
- spec/acceptance/throttling_spec.rb
|
313
|
+
- spec/acceptance/blocking_spec.rb
|
314
|
+
- spec/acceptance/customizing_throttled_response_spec.rb
|
315
315
|
- spec/acceptance/extending_request_object_spec.rb
|
316
316
|
- spec/acceptance/safelisting_spec.rb
|
317
|
-
- spec/acceptance/customizing_throttled_response_spec.rb
|
318
|
-
- spec/acceptance/safelisting_ip_spec.rb
|
319
|
-
- spec/acceptance/cache_store_config_for_allow2ban_spec.rb
|
320
|
-
- spec/acceptance/customizing_blocked_response_spec.rb
|
321
317
|
- spec/acceptance/cache_store_config_for_throttle_spec.rb
|
322
|
-
- spec/acceptance/
|
323
|
-
- spec/acceptance/stores/
|
324
|
-
- spec/acceptance/stores/
|
318
|
+
- spec/acceptance/fail2ban_spec.rb
|
319
|
+
- spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb
|
320
|
+
- spec/acceptance/stores/active_support_redis_cache_store_spec.rb
|
325
321
|
- spec/acceptance/stores/active_support_memory_store_spec.rb
|
322
|
+
- spec/acceptance/stores/active_support_redis_store_spec.rb
|
323
|
+
- spec/acceptance/stores/active_support_mem_cache_store_spec.rb
|
324
|
+
- spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb
|
326
325
|
- spec/acceptance/stores/connection_pool_dalli_client_spec.rb
|
327
|
-
- spec/acceptance/stores/active_support_redis_cache_store_spec.rb
|
328
326
|
- spec/acceptance/stores/active_support_dalli_store_spec.rb
|
329
|
-
- spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb
|
330
|
-
- spec/acceptance/stores/active_support_mem_cache_store_spec.rb
|
331
|
-
- spec/acceptance/stores/dalli_client_spec.rb
|
332
327
|
- spec/acceptance/stores/redis_store_spec.rb
|
333
|
-
- spec/acceptance/stores/
|
334
|
-
- spec/acceptance/
|
335
|
-
- spec/
|
328
|
+
- spec/acceptance/stores/dalli_client_spec.rb
|
329
|
+
- spec/acceptance/stores/redis_spec.rb
|
330
|
+
- spec/acceptance/customizing_blocked_response_spec.rb
|
331
|
+
- spec/spec_helper.rb
|
332
|
+
- spec/allow2ban_spec.rb
|
333
|
+
- spec/rack_attack_instrumentation_spec.rb
|
334
|
+
- spec/rack_attack_dalli_proxy_spec.rb
|
335
|
+
- spec/rack_attack_spec.rb
|
336
|
+
- spec/rack_attack_throttle_spec.rb
|
336
337
|
- spec/rack_attack_request_spec.rb
|
338
|
+
- spec/fail2ban_spec.rb
|
337
339
|
- spec/rack_attack_track_spec.rb
|
338
|
-
- spec/
|
340
|
+
- spec/support/cache_store_helper.rb
|