rack-attack 5.4.2 → 6.0.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 +65 -23
- data/Rakefile +3 -1
- data/lib/rack/attack.rb +46 -70
- data/lib/rack/attack/allow2ban.rb +2 -0
- data/lib/rack/attack/blocklist.rb +3 -1
- data/lib/rack/attack/cache.rb +5 -3
- data/lib/rack/attack/check.rb +3 -1
- data/lib/rack/attack/fail2ban.rb +2 -0
- data/lib/rack/attack/path_normalizer.rb +2 -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 -14
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +37 -0
- data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +2 -4
- 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 +8 -6
- data/lib/rack/attack/track.rb +5 -3
- 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 +8 -12
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +8 -12
- 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/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 +2 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +2 -0
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +2 -0
- data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
- data/spec/acceptance/stores/redis_store_spec.rb +2 -0
- 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 +3 -1
- data/spec/fail2ban_spec.rb +3 -1
- 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 +2 -0
- data/spec/rack_attack_request_spec.rb +2 -0
- data/spec/rack_attack_spec.rb +2 -21
- data/spec/rack_attack_throttle_spec.rb +10 -8
- data/spec/rack_attack_track_spec.rb +4 -2
- data/spec/spec_helper.rb +5 -4
- data/spec/support/cache_store_helper.rb +2 -0
- metadata +21 -14
- data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
data/lib/rack/attack/request.rb
CHANGED
data/lib/rack/attack/safelist.rb
CHANGED
@@ -1,22 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
module StoreProxy
|
4
|
-
PROXIES = [
|
6
|
+
PROXIES = [
|
7
|
+
DalliProxy,
|
8
|
+
MemCacheStoreProxy,
|
9
|
+
RedisStoreProxy,
|
10
|
+
RedisProxy,
|
11
|
+
RedisCacheStoreProxy,
|
12
|
+
ActiveSupportRedisStoreProxy
|
13
|
+
].freeze
|
5
14
|
|
6
15
|
def self.build(store)
|
7
|
-
|
8
|
-
klass
|
9
|
-
klass ? klass.new(client) : client
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.unwrap_active_support_stores(store)
|
13
|
-
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
14
|
-
# so use the raw Redis::Store instead.
|
15
|
-
if store.class.name == 'ActiveSupport::Cache::RedisStore'
|
16
|
-
store.instance_variable_get(:@data)
|
17
|
-
else
|
18
|
-
store
|
19
|
-
end
|
16
|
+
klass = PROXIES.find { |proxy| proxy.handle?(store) }
|
17
|
+
klass ? klass.new(store) : store
|
20
18
|
end
|
21
19
|
end
|
22
20
|
end
|
@@ -0,0 +1,37 @@
|
|
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) && defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
|
11
|
+
end
|
12
|
+
|
13
|
+
def increment(name, amount = 1, options = {})
|
14
|
+
# #increment ignores options[:expires_in].
|
15
|
+
#
|
16
|
+
# So in order to workaround this we use #write (which sets expiration) to initialize
|
17
|
+
# the counter. After that we continue using the original #increment.
|
18
|
+
if options[:expires_in] && !read(name)
|
19
|
+
write(name, amount, options)
|
20
|
+
|
21
|
+
amount
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(name, options = {})
|
28
|
+
super(name, options.merge!(raw: true))
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(name, value, options = {})
|
32
|
+
super(name, value, options.merge!(raw: true))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -22,10 +24,6 @@ module Rack
|
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
25
|
-
def read(name, options = {})
|
26
|
-
super(name, options.merge!(raw: true))
|
27
|
-
end
|
28
|
-
|
29
27
|
def write(name, value, options = {})
|
30
28
|
super(name, value, options.merge!(raw: true))
|
31
29
|
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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
4
|
|
3
5
|
module Rack
|
@@ -9,17 +11,15 @@ module Rack
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def read(key)
|
12
|
-
get(key, raw: true)
|
13
|
-
rescue Redis::BaseError
|
14
|
+
rescuing { get(key, raw: true) }
|
14
15
|
end
|
15
16
|
|
16
17
|
def write(key, value, options = {})
|
17
18
|
if (expires_in = options[:expires_in])
|
18
|
-
setex(key, expires_in, value, raw: true)
|
19
|
+
rescuing { setex(key, expires_in, value, raw: true) }
|
19
20
|
else
|
20
|
-
set(key, value, raw: true)
|
21
|
+
rescuing { set(key, value, raw: true) }
|
21
22
|
end
|
22
|
-
rescue Redis::BaseError
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
data/lib/rack/attack/throttle.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Throttle
|
4
6
|
MANDATORY_OPTIONS = [:limit, :period].freeze
|
5
7
|
|
6
8
|
attr_reader :name, :limit, :period, :block, :type
|
7
|
-
def initialize(name, options, block)
|
9
|
+
def initialize(name, options, &block)
|
8
10
|
@name, @block = name, block
|
9
11
|
MANDATORY_OPTIONS.each do |opt|
|
10
|
-
raise ArgumentError
|
12
|
+
raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
|
11
13
|
end
|
12
14
|
@limit = options[:limit]
|
13
15
|
@period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i
|
@@ -29,10 +31,10 @@ module Rack
|
|
29
31
|
epoch_time = cache.last_epoch_time
|
30
32
|
|
31
33
|
data = {
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
34
|
+
count: count,
|
35
|
+
period: current_period,
|
36
|
+
limit: current_limit,
|
37
|
+
epoch_time: epoch_time
|
36
38
|
}
|
37
39
|
|
38
40
|
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
data/lib/rack/attack/track.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
class Attack
|
3
5
|
class Track
|
4
6
|
attr_reader :filter
|
5
7
|
|
6
|
-
def initialize(name, options = {}, block)
|
8
|
+
def initialize(name, options = {}, &block)
|
7
9
|
options[:type] = :track
|
8
10
|
|
9
11
|
if options[:limit] && options[:period]
|
10
|
-
@filter = Throttle.new(name, options, block)
|
12
|
+
@filter = Throttle.new(name, options, &block)
|
11
13
|
else
|
12
|
-
@filter = Check.new(name, options, block)
|
14
|
+
@filter = Check.new(name, options, &block)
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
data/lib/rack/attack/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Blocking an IP" do
|
@@ -21,9 +23,9 @@ describe "Blocking an IP" do
|
|
21
23
|
notified = false
|
22
24
|
notification_type = nil
|
23
25
|
|
24
|
-
ActiveSupport::Notifications.subscribe("
|
26
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
25
27
|
notified = true
|
26
|
-
notification_type = request.env["rack.attack.match_type"]
|
28
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
27
29
|
end
|
28
30
|
|
29
31
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,6 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "#blocklist" do
|
6
|
+
before do
|
7
|
+
Rack::Attack.blocklist do |request|
|
8
|
+
request.ip == "1.2.3.4"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "forbids request if blocklist condition is true" do
|
13
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
14
|
+
|
15
|
+
assert_equal 403, last_response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
it "succeeds if blocklist condition is false" do
|
19
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
20
|
+
|
21
|
+
assert_equal 200, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
it "notifies when the request is blocked" do
|
25
|
+
notification_matched = nil
|
26
|
+
notification_type = nil
|
27
|
+
|
28
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
|
29
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
30
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
31
|
+
end
|
32
|
+
|
33
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
34
|
+
|
35
|
+
assert_nil notification_matched
|
36
|
+
assert_nil notification_type
|
37
|
+
|
38
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
39
|
+
|
40
|
+
assert_nil notification_matched
|
41
|
+
assert_equal :blocklist, notification_type
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#blocklist with name" do
|
4
46
|
before do
|
5
47
|
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
48
|
request.ip == "1.2.3.4"
|
@@ -23,9 +65,9 @@ describe "#blocklist" do
|
|
23
65
|
notification_matched = nil
|
24
66
|
notification_type = nil
|
25
67
|
|
26
|
-
ActiveSupport::Notifications.subscribe("
|
27
|
-
notification_matched = request.env["rack.attack.matched"]
|
28
|
-
notification_type = request.env["rack.attack.match_type"]
|
68
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
69
|
+
notification_matched = payload[:request].env["rack.attack.matched"]
|
70
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
29
71
|
end
|
30
72
|
|
31
73
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
|
3
5
|
describe "Blocking an IP subnet" do
|
@@ -27,9 +29,9 @@ describe "Blocking an IP subnet" do
|
|
27
29
|
notified = false
|
28
30
|
notification_type = nil
|
29
31
|
|
30
|
-
ActiveSupport::Notifications.subscribe("
|
32
|
+
ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
|
31
33
|
notified = true
|
32
|
-
notification_type = request.env["rack.attack.match_type"]
|
34
|
+
notification_type = payload[:request].env["rack.attack.match_type"]
|
33
35
|
end
|
34
36
|
|
35
37
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
2
4
|
require "minitest/stub_const"
|
3
5
|
|
@@ -20,11 +22,9 @@ describe "Cache store config when using allow2ban" do
|
|
20
22
|
raised_exception = nil
|
21
23
|
|
22
24
|
fake_store_class = Class.new do
|
23
|
-
def write(key, value)
|
24
|
-
end
|
25
|
+
def write(key, value); end
|
25
26
|
|
26
|
-
def increment(key, count, options = {})
|
27
|
-
end
|
27
|
+
def increment(key, count, options = {}); end
|
28
28
|
end
|
29
29
|
|
30
30
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -42,11 +42,9 @@ describe "Cache store config when using allow2ban" do
|
|
42
42
|
raised_exception = nil
|
43
43
|
|
44
44
|
fake_store_class = Class.new do
|
45
|
-
def read(key)
|
46
|
-
end
|
45
|
+
def read(key); end
|
47
46
|
|
48
|
-
def increment(key, count, options = {})
|
49
|
-
end
|
47
|
+
def increment(key, count, options = {}); end
|
50
48
|
end
|
51
49
|
|
52
50
|
Object.stub_const(:FakeStore, fake_store_class) do
|
@@ -64,11 +62,9 @@ describe "Cache store config when using allow2ban" do
|
|
64
62
|
raised_exception = nil
|
65
63
|
|
66
64
|
fake_store_class = Class.new do
|
67
|
-
def read(key)
|
68
|
-
end
|
65
|
+
def read(key); end
|
69
66
|
|
70
|
-
def write(key, value)
|
71
|
-
end
|
67
|
+
def write(key, value); end
|
72
68
|
end
|
73
69
|
|
74
70
|
Object.stub_const(:FakeStore, fake_store_class) do
|