rack-attack 5.4.0 → 6.2.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 +78 -27
- data/Rakefile +3 -1
- data/bin/setup +8 -0
- data/lib/rack/attack.rb +137 -148
- data/lib/rack/attack/allow2ban.rb +2 -0
- data/lib/rack/attack/blocklist.rb +3 -1
- data/lib/rack/attack/cache.rb +9 -4
- data/lib/rack/attack/check.rb +5 -2
- data/lib/rack/attack/fail2ban.rb +2 -0
- data/lib/rack/attack/path_normalizer.rb +22 -18
- data/lib/rack/attack/railtie.rb +21 -0
- data/lib/rack/attack/request.rb +2 -0
- data/lib/rack/attack/safelist.rb +3 -1
- data/lib/rack/attack/store_proxy.rb +12 -24
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +21 -0
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
- data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
- data/lib/rack/attack/throttle.rb +12 -8
- data/lib/rack/attack/track.rb +9 -6
- data/lib/rack/attack/version.rb +3 -1
- data/spec/acceptance/allow2ban_spec.rb +2 -0
- data/spec/acceptance/blocking_ip_spec.rb +4 -2
- data/spec/acceptance/blocking_spec.rb +45 -3
- data/spec/acceptance/blocking_subnet_spec.rb +4 -2
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
- data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
- data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
- data/spec/acceptance/extending_request_object_spec.rb +2 -0
- data/spec/acceptance/fail2ban_spec.rb +2 -0
- data/spec/acceptance/rails_middleware_spec.rb +41 -0
- data/spec/acceptance/safelisting_ip_spec.rb +4 -2
- data/spec/acceptance/safelisting_spec.rb +57 -3
- data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +3 -23
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +20 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +4 -24
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +3 -23
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +10 -24
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +9 -25
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +4 -24
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -23
- data/spec/acceptance/stores/dalli_client_spec.rb +3 -23
- data/spec/acceptance/stores/redis_spec.rb +1 -23
- data/spec/acceptance/stores/redis_store_spec.rb +3 -23
- data/spec/acceptance/throttling_spec.rb +7 -5
- data/spec/acceptance/track_spec.rb +5 -3
- data/spec/acceptance/track_throttle_spec.rb +5 -3
- data/spec/allow2ban_spec.rb +20 -15
- data/spec/fail2ban_spec.rb +20 -17
- data/spec/integration/offline_spec.rb +3 -1
- data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
- data/spec/rack_attack_instrumentation_spec.rb +42 -0
- data/spec/rack_attack_path_normalizer_spec.rb +4 -2
- data/spec/rack_attack_request_spec.rb +2 -0
- data/spec/rack_attack_spec.rb +38 -34
- data/spec/rack_attack_throttle_spec.rb +50 -19
- data/spec/rack_attack_track_spec.rb +12 -7
- data/spec/spec_helper.rb +10 -8
- data/spec/support/cache_store_helper.rb +27 -1
- metadata +48 -28
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
data/lib/rack/attack/cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Cache
|
@@ -27,7 +29,7 @@ module Rack
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def write(unprefixed_key, value, expires_in)
|
30
|
-
store.write("#{prefix}:#{unprefixed_key}", value, :
|
32
|
+
store.write("#{prefix}:#{unprefixed_key}", value, expires_in: expires_in)
|
31
33
|
end
|
32
34
|
|
33
35
|
def reset_count(unprefixed_key, period)
|
@@ -52,13 +54,13 @@ module Rack
|
|
52
54
|
enforce_store_presence!
|
53
55
|
enforce_store_method_presence!(:increment)
|
54
56
|
|
55
|
-
result = store.increment(key, 1, :
|
57
|
+
result = store.increment(key, 1, expires_in: expires_in)
|
56
58
|
|
57
59
|
# NB: Some stores return nil when incrementing uninitialized values
|
58
60
|
if result.nil?
|
59
61
|
enforce_store_method_presence!(:write)
|
60
62
|
|
61
|
-
store.write(key, 1, :
|
63
|
+
store.write(key, 1, expires_in: expires_in)
|
62
64
|
end
|
63
65
|
result || 1
|
64
66
|
end
|
@@ -71,7 +73,10 @@ module Rack
|
|
71
73
|
|
72
74
|
def enforce_store_method_presence!(method_name)
|
73
75
|
if !store.respond_to?(method_name)
|
74
|
-
raise
|
76
|
+
raise(
|
77
|
+
Rack::Attack::MisconfiguredStoreError,
|
78
|
+
"Configured store #{store.class.name} doesn't respond to ##{method_name} method"
|
79
|
+
)
|
75
80
|
end
|
76
81
|
end
|
77
82
|
end
|
data/lib/rack/attack/check.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Check
|
4
6
|
attr_reader :name, :block, :type
|
5
|
-
def initialize(name, options = {}, block)
|
6
|
-
@name
|
7
|
+
def initialize(name, options = {}, &block)
|
8
|
+
@name = name
|
9
|
+
@block = block
|
7
10
|
@type = options.fetch(:type, nil)
|
8
11
|
end
|
9
12
|
|
data/lib/rack/attack/fail2ban.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
|
-
|
2
|
-
# When using Rack::Attack with a Rails app, developers expect the request path
|
3
|
-
# to be normalized. In particular, trailing slashes are stripped.
|
4
|
-
# (See https://git.io/v0rrR for implementation.)
|
5
|
-
#
|
6
|
-
# Look for an ActionDispatch utility class that Rails folks would expect
|
7
|
-
# to normalize request paths. If unavailable, use a fallback class that
|
8
|
-
# doesn't normalize the path (as a non-Rails rack app developer expects).
|
1
|
+
# frozen_string_literal: true
|
9
2
|
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
# When using Rack::Attack with a Rails app, developers expect the request path
|
6
|
+
# to be normalized. In particular, trailing slashes are stripped.
|
7
|
+
# (See https://git.io/v0rrR for implementation.)
|
8
|
+
#
|
9
|
+
# Look for an ActionDispatch utility class that Rails folks would expect
|
10
|
+
# to normalize request paths. If unavailable, use a fallback class that
|
11
|
+
# doesn't normalize the path (as a non-Rails rack app developer expects).
|
12
|
+
|
13
|
+
module FallbackPathNormalizer
|
14
|
+
def self.normalize_path(path)
|
15
|
+
path
|
16
|
+
end
|
13
17
|
end
|
14
|
-
end
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
|
20
|
+
# For Rails apps
|
21
|
+
::ActionDispatch::Journey::Router::Utils
|
22
|
+
else
|
23
|
+
FallbackPathNormalizer
|
24
|
+
end
|
25
|
+
end
|
22
26
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Attack
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'rack.attack.middleware', after: :load_config_initializers, before: :build_middleware_stack do |app|
|
7
|
+
if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.1")
|
8
|
+
middlewares = app.config.middleware
|
9
|
+
operations = middlewares.send(:operations) + middlewares.send(:delete_operations)
|
10
|
+
|
11
|
+
use_middleware = operations.none? do |operation|
|
12
|
+
middleware = operation[1]
|
13
|
+
middleware.include?(Rack::Attack)
|
14
|
+
end
|
15
|
+
|
16
|
+
middlewares.use(Rack::Attack) if use_middleware
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/rack/attack/request.rb
CHANGED
data/lib/rack/attack/safelist.rb
CHANGED
@@ -1,32 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
module StoreProxy
|
4
|
-
PROXIES = [
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
PROXIES = [
|
7
|
+
DalliProxy,
|
8
|
+
MemCacheStoreProxy,
|
9
|
+
RedisStoreProxy,
|
10
|
+
RedisProxy,
|
11
|
+
RedisCacheStoreProxy,
|
12
|
+
ActiveSupportRedisStoreProxy
|
13
|
+
].freeze
|
8
14
|
|
9
15
|
def self.build(store)
|
10
|
-
|
11
|
-
klass
|
12
|
-
klass ? klass.new(client) : client
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.unwrap_active_support_stores(store)
|
16
|
-
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
17
|
-
# so use the raw Redis::Store instead.
|
18
|
-
# We also want to use the underlying Dalli client instead of ::ActiveSupport::Cache::MemCacheStore,
|
19
|
-
# and the MemCache client if using Rails 3.x
|
20
|
-
|
21
|
-
if store.instance_variable_defined?(:@data)
|
22
|
-
client = store.instance_variable_get(:@data)
|
23
|
-
end
|
24
|
-
|
25
|
-
if ACTIVE_SUPPORT_WRAPPER_CLASSES.include?(store.class.to_s) && ACTIVE_SUPPORT_CLIENTS.include?(client.class.to_s)
|
26
|
-
client
|
27
|
-
else
|
28
|
-
store
|
29
|
-
end
|
16
|
+
klass = PROXIES.find { |proxy| proxy.handle?(store) }
|
17
|
+
klass ? klass.new(store) : store
|
30
18
|
end
|
31
19
|
end
|
32
20
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Attack
|
7
|
+
module StoreProxy
|
8
|
+
class ActiveSupportRedisStoreProxy < SimpleDelegator
|
9
|
+
def self.handle?(store)
|
10
|
+
defined?(::Redis) &&
|
11
|
+
defined?(::ActiveSupport::Cache::RedisStore) &&
|
12
|
+
store.is_a?(::ActiveSupport::Cache::RedisStore)
|
13
|
+
end
|
14
|
+
|
15
|
+
def increment(name, amount = 1, options = {})
|
16
|
+
# #increment ignores options[:expires_in].
|
17
|
+
#
|
18
|
+
# So in order to workaround this we use #write (which sets expiration) to initialize
|
19
|
+
# the counter. After that we continue using the original #increment.
|
20
|
+
if options[:expires_in] && !read(name)
|
21
|
+
write(name, amount, options)
|
22
|
+
|
23
|
+
amount
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(name, options = {})
|
30
|
+
super(name, options.merge!(raw: true))
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(name, value, options = {})
|
34
|
+
super(name, value, options.merge!(raw: true))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -22,31 +24,35 @@ module Rack
|
|
22
24
|
end
|
23
25
|
|
24
26
|
def read(key)
|
25
|
-
|
26
|
-
client
|
27
|
+
rescuing do
|
28
|
+
with do |client|
|
29
|
+
client.get(key)
|
30
|
+
end
|
27
31
|
end
|
28
|
-
rescue Dalli::DalliError
|
29
32
|
end
|
30
33
|
|
31
34
|
def write(key, value, options = {})
|
32
|
-
|
33
|
-
|
35
|
+
rescuing do
|
36
|
+
with do |client|
|
37
|
+
client.set(key, value, options.fetch(:expires_in, 0), raw: true)
|
38
|
+
end
|
34
39
|
end
|
35
|
-
rescue Dalli::DalliError
|
36
40
|
end
|
37
41
|
|
38
42
|
def increment(key, amount, options = {})
|
39
|
-
|
40
|
-
|
43
|
+
rescuing do
|
44
|
+
with do |client|
|
45
|
+
client.incr(key, amount, options.fetch(:expires_in, 0), amount)
|
46
|
+
end
|
41
47
|
end
|
42
|
-
rescue Dalli::DalliError
|
43
48
|
end
|
44
49
|
|
45
50
|
def delete(key)
|
46
|
-
|
47
|
-
client
|
51
|
+
rescuing do
|
52
|
+
with do |client|
|
53
|
+
client.delete(key)
|
54
|
+
end
|
48
55
|
end
|
49
|
-
rescue Dalli::DalliError
|
50
56
|
end
|
51
57
|
|
52
58
|
private
|
@@ -54,10 +60,18 @@ module Rack
|
|
54
60
|
def stub_with_if_missing
|
55
61
|
unless __getobj__.respond_to?(:with)
|
56
62
|
class << self
|
57
|
-
def with
|
63
|
+
def with
|
64
|
+
yield __getobj__
|
65
|
+
end
|
58
66
|
end
|
59
67
|
end
|
60
68
|
end
|
69
|
+
|
70
|
+
def rescuing
|
71
|
+
yield
|
72
|
+
rescue Dalli::DalliError
|
73
|
+
nil
|
74
|
+
end
|
61
75
|
end
|
62
76
|
end
|
63
77
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
class Attack
|
7
|
+
module StoreProxy
|
8
|
+
class MemCacheStoreProxy < SimpleDelegator
|
9
|
+
def self.handle?(store)
|
10
|
+
defined?(::Dalli) &&
|
11
|
+
defined?(::ActiveSupport::Cache::MemCacheStore) &&
|
12
|
+
store.is_a?(::ActiveSupport::Cache::MemCacheStore)
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(name, value, options = {})
|
16
|
+
super(name, value, options.merge!(raw: true))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -5,7 +7,7 @@ module Rack
|
|
5
7
|
module StoreProxy
|
6
8
|
class RedisCacheStoreProxy < SimpleDelegator
|
7
9
|
def self.handle?(store)
|
8
|
-
|
10
|
+
store.class.name == "ActiveSupport::Cache::RedisCacheStore"
|
9
11
|
end
|
10
12
|
|
11
13
|
def increment(name, amount = 1, options = {})
|
@@ -13,21 +15,33 @@ module Rack
|
|
13
15
|
#
|
14
16
|
# So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
|
15
17
|
# the counter. After that we continue using the original RedisCacheStore#increment.
|
16
|
-
|
17
|
-
|
18
|
+
rescuing do
|
19
|
+
if options[:expires_in] && !read(name)
|
20
|
+
write(name, amount, options)
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
amount
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
22
26
|
end
|
23
27
|
end
|
24
28
|
|
25
|
-
def read(
|
26
|
-
super
|
29
|
+
def read(*_args)
|
30
|
+
rescuing { super }
|
27
31
|
end
|
28
32
|
|
29
33
|
def write(name, value, options = {})
|
30
|
-
|
34
|
+
rescuing do
|
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
|
31
45
|
end
|
32
46
|
end
|
33
47
|
end
|
@@ -19,34 +19,40 @@ module Rack
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def read(key)
|
22
|
-
get(key)
|
23
|
-
rescue Redis::BaseError
|
22
|
+
rescuing { get(key) }
|
24
23
|
end
|
25
24
|
|
26
25
|
def write(key, value, options = {})
|
27
26
|
if (expires_in = options[:expires_in])
|
28
|
-
setex(key, expires_in, value)
|
27
|
+
rescuing { setex(key, expires_in, value) }
|
29
28
|
else
|
30
|
-
set(key, value)
|
29
|
+
rescuing { set(key, value) }
|
31
30
|
end
|
32
|
-
rescue Redis::BaseError
|
33
31
|
end
|
34
32
|
|
35
33
|
def increment(key, amount, options = {})
|
36
34
|
count = nil
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
rescuing do
|
37
|
+
pipelined do
|
38
|
+
count = incrby(key, amount)
|
39
|
+
expire(key, options[:expires_in]) if options[:expires_in]
|
40
|
+
end
|
41
41
|
end
|
42
42
|
|
43
43
|
count.value if count
|
44
|
-
rescue Redis::BaseError
|
45
44
|
end
|
46
45
|
|
47
46
|
def delete(key, _options = {})
|
48
|
-
del(key)
|
47
|
+
rescuing { del(key) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def rescuing
|
53
|
+
yield
|
49
54
|
rescue Redis::BaseError
|
55
|
+
nil
|
50
56
|
end
|
51
57
|
end
|
52
58
|
end
|