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
@@ -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,15 +1,18 @@
|
|
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)
|
8
|
-
@name
|
9
|
+
def initialize(name, options, &block)
|
10
|
+
@name = name
|
11
|
+
@block = block
|
9
12
|
MANDATORY_OPTIONS.each do |opt|
|
10
|
-
raise ArgumentError
|
13
|
+
raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
|
11
14
|
end
|
12
|
-
@limit
|
15
|
+
@limit = options[:limit]
|
13
16
|
@period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i
|
14
17
|
@type = options.fetch(:type, :throttle)
|
15
18
|
end
|
@@ -29,10 +32,11 @@ module Rack
|
|
29
32
|
epoch_time = cache.last_epoch_time
|
30
33
|
|
31
34
|
data = {
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
35
|
+
discriminator: discriminator,
|
36
|
+
count: count,
|
37
|
+
period: current_period,
|
38
|
+
limit: current_limit,
|
39
|
+
epoch_time: epoch_time
|
36
40
|
}
|
37
41
|
|
38
42
|
(request.env['rack.attack.throttle_data'] ||= {})[name] = data
|
data/lib/rack/attack/track.rb
CHANGED
@@ -1,16 +1,19 @@
|
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
@filter =
|
12
|
+
if options[:limit] && options[:period]
|
13
|
+
Throttle.new(name, options, &block)
|
14
|
+
else
|
15
|
+
Check.new(name, options, &block)
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
19
|
def matched_by?(request)
|
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,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
4
|
+
require "minitest/stub_const"
|
2
5
|
|
3
6
|
describe "Cache store config when using allow2ban" do
|
4
7
|
before do
|
@@ -16,61 +19,67 @@ describe "Cache store config when using allow2ban" do
|
|
16
19
|
end
|
17
20
|
|
18
21
|
it "gives semantic error if store is missing #read method" do
|
19
|
-
|
20
|
-
def write(key, value)
|
21
|
-
end
|
22
|
+
raised_exception = nil
|
22
23
|
|
23
|
-
|
24
|
-
end
|
24
|
+
fake_store_class = Class.new do
|
25
|
+
def write(key, value); end
|
26
|
+
|
27
|
+
def increment(key, count, options = {}); end
|
25
28
|
end
|
26
29
|
|
27
|
-
|
30
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
31
|
+
Rack::Attack.cache.store = FakeStore.new
|
28
32
|
|
29
|
-
|
30
|
-
|
33
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
34
|
+
get "/scarce-resource"
|
35
|
+
end
|
31
36
|
end
|
32
37
|
|
33
|
-
assert_equal "
|
38
|
+
assert_equal "Configured store FakeStore doesn't respond to #read method", raised_exception.message
|
34
39
|
end
|
35
40
|
|
36
41
|
it "gives semantic error if store is missing #write method" do
|
37
|
-
|
38
|
-
def read(key)
|
39
|
-
end
|
42
|
+
raised_exception = nil
|
40
43
|
|
41
|
-
|
42
|
-
end
|
44
|
+
fake_store_class = Class.new do
|
45
|
+
def read(key); end
|
46
|
+
|
47
|
+
def increment(key, count, options = {}); end
|
43
48
|
end
|
44
49
|
|
45
|
-
|
50
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
51
|
+
Rack::Attack.cache.store = FakeStore.new
|
46
52
|
|
47
|
-
|
48
|
-
|
53
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
54
|
+
get "/scarce-resource"
|
55
|
+
end
|
49
56
|
end
|
50
57
|
|
51
|
-
assert_equal "
|
58
|
+
assert_equal "Configured store FakeStore doesn't respond to #write method", raised_exception.message
|
52
59
|
end
|
53
60
|
|
54
61
|
it "gives semantic error if store is missing #increment method" do
|
55
|
-
|
56
|
-
def read(key)
|
57
|
-
end
|
62
|
+
raised_exception = nil
|
58
63
|
|
59
|
-
|
60
|
-
end
|
64
|
+
fake_store_class = Class.new do
|
65
|
+
def read(key); end
|
66
|
+
|
67
|
+
def write(key, value); end
|
61
68
|
end
|
62
69
|
|
63
|
-
|
70
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
71
|
+
Rack::Attack.cache.store = FakeStore.new
|
64
72
|
|
65
|
-
|
66
|
-
|
73
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
74
|
+
get "/scarce-resource"
|
75
|
+
end
|
67
76
|
end
|
68
77
|
|
69
|
-
assert_equal "
|
78
|
+
assert_equal "Configured store FakeStore doesn't respond to #increment method", raised_exception.message
|
70
79
|
end
|
71
80
|
|
72
81
|
it "works with any object that responds to #read, #write and #increment" do
|
73
|
-
|
82
|
+
fake_store_class = Class.new do
|
74
83
|
attr_accessor :backend
|
75
84
|
|
76
85
|
def initialize
|
@@ -91,21 +100,23 @@ describe "Cache store config when using allow2ban" do
|
|
91
100
|
end
|
92
101
|
end
|
93
102
|
|
94
|
-
|
103
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
104
|
+
Rack::Attack.cache.store = FakeStore.new
|
95
105
|
|
96
|
-
|
97
|
-
|
106
|
+
get "/"
|
107
|
+
assert_equal 200, last_response.status
|
98
108
|
|
99
|
-
|
100
|
-
|
109
|
+
get "/scarce-resource"
|
110
|
+
assert_equal 200, last_response.status
|
101
111
|
|
102
|
-
|
103
|
-
|
112
|
+
get "/scarce-resource"
|
113
|
+
assert_equal 200, last_response.status
|
104
114
|
|
105
|
-
|
106
|
-
|
115
|
+
get "/scarce-resource"
|
116
|
+
assert_equal 403, last_response.status
|
107
117
|
|
108
|
-
|
109
|
-
|
118
|
+
get "/"
|
119
|
+
assert_equal 403, last_response.status
|
120
|
+
end
|
110
121
|
end
|
111
122
|
end
|
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "../spec_helper"
|
4
|
+
require "minitest/stub_const"
|
2
5
|
|
3
6
|
describe "Cache store config when using fail2ban" do
|
4
7
|
before do
|
@@ -16,61 +19,67 @@ describe "Cache store config when using fail2ban" do
|
|
16
19
|
end
|
17
20
|
|
18
21
|
it "gives semantic error if store is missing #read method" do
|
19
|
-
|
20
|
-
def write(key, value)
|
21
|
-
end
|
22
|
+
raised_exception = nil
|
22
23
|
|
23
|
-
|
24
|
-
end
|
24
|
+
fake_store_class = Class.new do
|
25
|
+
def write(key, value); end
|
26
|
+
|
27
|
+
def increment(key, count, options = {}); end
|
25
28
|
end
|
26
29
|
|
27
|
-
|
30
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
31
|
+
Rack::Attack.cache.store = FakeStore.new
|
28
32
|
|
29
|
-
|
30
|
-
|
33
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
34
|
+
get "/private-place"
|
35
|
+
end
|
31
36
|
end
|
32
37
|
|
33
|
-
assert_equal "
|
38
|
+
assert_equal "Configured store FakeStore doesn't respond to #read method", raised_exception.message
|
34
39
|
end
|
35
40
|
|
36
41
|
it "gives semantic error if store is missing #write method" do
|
37
|
-
|
38
|
-
def read(key)
|
39
|
-
end
|
42
|
+
raised_exception = nil
|
40
43
|
|
41
|
-
|
42
|
-
end
|
44
|
+
fake_store_class = Class.new do
|
45
|
+
def read(key); end
|
46
|
+
|
47
|
+
def increment(key, count, options = {}); end
|
43
48
|
end
|
44
49
|
|
45
|
-
|
50
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
51
|
+
Rack::Attack.cache.store = FakeStore.new
|
46
52
|
|
47
|
-
|
48
|
-
|
53
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
54
|
+
get "/private-place"
|
55
|
+
end
|
49
56
|
end
|
50
57
|
|
51
|
-
assert_equal "
|
58
|
+
assert_equal "Configured store FakeStore doesn't respond to #write method", raised_exception.message
|
52
59
|
end
|
53
60
|
|
54
61
|
it "gives semantic error if store is missing #increment method" do
|
55
|
-
|
56
|
-
def read(key)
|
57
|
-
end
|
62
|
+
raised_exception = nil
|
58
63
|
|
59
|
-
|
60
|
-
end
|
64
|
+
fake_store_class = Class.new do
|
65
|
+
def read(key); end
|
66
|
+
|
67
|
+
def write(key, value); end
|
61
68
|
end
|
62
69
|
|
63
|
-
|
70
|
+
Object.stub_const(:FakeStore, fake_store_class) do
|
71
|
+
Rack::Attack.cache.store = FakeStore.new
|
64
72
|
|
65
|
-
|
66
|
-
|
73
|
+
raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
74
|
+
get "/private-place"
|
75
|
+
end
|
67
76
|
end
|
68
77
|
|
69
|
-
assert_equal "
|
78
|
+
assert_equal "Configured store FakeStore doesn't respond to #increment method", raised_exception.message
|
70
79
|
end
|
71
80
|
|
72
81
|
it "works with any object that responds to #read, #write and #increment" do
|
73
|
-
|
82
|
+
FakeStore = Class.new do
|
74
83
|
attr_accessor :backend
|
75
84
|
|
76
85
|
def initialize
|
@@ -91,7 +100,7 @@ describe "Cache store config when using fail2ban" do
|
|
91
100
|
end
|
92
101
|
end
|
93
102
|
|
94
|
-
Rack::Attack.cache.store =
|
103
|
+
Rack::Attack.cache.store = FakeStore.new
|
95
104
|
|
96
105
|
get "/"
|
97
106
|
assert_equal 200, last_response.status
|