rack-attack 6.7.0 → 6.8.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -6
  3. data/lib/rack/attack/base_proxy.rb +1 -0
  4. data/lib/rack/attack/cache.rb +1 -1
  5. data/lib/rack/attack/configuration.rb +7 -3
  6. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +16 -10
  7. data/lib/rack/attack/store_proxy/redis_proxy.rb +2 -1
  8. data/lib/rack/attack/throttle.rb +2 -1
  9. data/lib/rack/attack/version.rb +1 -1
  10. data/lib/rack/attack.rb +3 -1
  11. data/spec/acceptance/blocking_ip_spec.rb +13 -8
  12. data/spec/acceptance/blocking_spec.rb +16 -18
  13. data/spec/acceptance/blocking_subnet_spec.rb +7 -8
  14. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +5 -3
  15. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +7 -5
  16. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +5 -3
  17. data/spec/acceptance/cache_store_config_with_rails_spec.rb +6 -4
  18. data/spec/acceptance/extending_request_object_spec.rb +3 -7
  19. data/spec/acceptance/fail2ban_spec.rb +42 -0
  20. data/spec/acceptance/safelisting_ip_spec.rb +12 -4
  21. data/spec/acceptance/safelisting_spec.rb +14 -14
  22. data/spec/acceptance/safelisting_subnet_spec.rb +6 -4
  23. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +5 -2
  24. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +0 -1
  25. data/spec/acceptance/stores/active_support_memory_store_spec.rb +0 -2
  26. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +5 -2
  27. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +0 -1
  28. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +0 -1
  29. data/spec/acceptance/stores/dalli_client_spec.rb +0 -1
  30. data/spec/acceptance/stores/redis_spec.rb +0 -1
  31. data/spec/acceptance/stores/redis_store_spec.rb +1 -3
  32. data/spec/acceptance/throttling_spec.rb +14 -23
  33. data/spec/acceptance/track_spec.rb +8 -9
  34. data/spec/acceptance/track_throttle_spec.rb +10 -16
  35. data/spec/configuration_spec.rb +33 -0
  36. data/spec/integration/offline_spec.rb +0 -12
  37. data/spec/rack_attack_instrumentation_spec.rb +24 -28
  38. data/spec/rack_attack_request_spec.rb +2 -4
  39. data/spec/rack_attack_reset_spec.rb +90 -0
  40. data/spec/rack_attack_spec.rb +0 -22
  41. data/spec/rack_attack_throttle_spec.rb +49 -28
  42. data/spec/rack_attack_track_spec.rb +4 -17
  43. data/spec/spec_helper.rb +4 -3
  44. data/spec/support/cache_store_helper.rb +31 -25
  45. data/spec/support/freeze_time_helper.rb +9 -0
  46. metadata +41 -15
  47. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +0 -39
  48. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +0 -25
  49. data/spec/acceptance/stores/active_support_redis_store_spec.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ea46a4cc03f800f7d841d6e2fafb387f47514ba4eb9e89c2a0f84bbeeaa6fea
4
- data.tar.gz: 355b0da550c98fd1c79c9aaef19bb2f651bdf941cf0f7feaec1e75c50a3d56f1
3
+ metadata.gz: cca196b4f54fee6e576f7afd081dcc0311c25f2a9705a498bf5c319ec27a0d79
4
+ data.tar.gz: c758c6d6c9a10eac5ae1b20b1d501c3f9ded73d1c2dd77777a57a1727addf0fa
5
5
  SHA512:
6
- metadata.gz: 8b9965c215ce6fced981d2863496b8060282fd35b9134d867772adc848612c97cd37227d1a16bf20def74bd88d96c07744bd23ac4344e3cb173065c7ad3f47b4
7
- data.tar.gz: 9f4cc8f537454a521a154f1dfa8c102831f901c89c485b769156c91107a193391f7cddeab9f0fbdf195ca49da24bb16478a7c090cc2b23c69ff20aa9d666f5fa
6
+ metadata.gz: 4f8a0c70cb9744d842786b4960d9ec82f971c54a2fa4c036c44919be5695310cb192f22fa5910edd187f1655e537e894f092db0b16bda41744ecac2cde9e1bb3
7
+ data.tar.gz: 6e999c3c981c90130aac0bffddd69feb09ff480cd1b791dea33f580080a2cf7ffc4e4949c93ec2e4dfe61dd1cdd957291e7893c0ca2b879b2092ac6984a45169
data/README.md CHANGED
@@ -11,7 +11,6 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
11
11
 
12
12
  [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
13
13
  [![build](https://github.com/rack/rack-attack/actions/workflows/build.yml/badge.svg)](https://github.com/rack/rack-attack/actions/workflows/build.yml)
14
- [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
15
14
  [![Join the chat at https://gitter.im/rack-attack/rack-attack](https://badges.gitter.im/rack-attack/rack-attack.svg)](https://gitter.im/rack-attack/rack-attack)
16
15
 
17
16
  ## Table of contents
@@ -56,7 +55,7 @@ Add this line to your application's Gemfile:
56
55
  ```ruby
57
56
  # In your Gemfile
58
57
 
59
- gem 'rack-attack'
58
+ gem "rack-attack", "~> 6.8"
60
59
  ```
61
60
 
62
61
  And then execute:
@@ -291,7 +290,7 @@ Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
291
290
  end
292
291
 
293
292
  # Track it using ActiveSupport::Notification
294
- ActiveSupport::Notifications.subscribe("track.rack_attack") do |name, start, finish, request_id, payload|
293
+ ActiveSupport::Notifications.subscribe("track.rack_attack") do |name, start, finish, instrumenter_id, payload|
295
294
  req = payload[:request]
296
295
  if req.env['rack.attack.matched'] == "special_agent"
297
296
  Rails.logger.info "special_agent: #{req.path}"
@@ -302,7 +301,7 @@ end
302
301
 
303
302
  ### Cache store configuration
304
303
 
305
- Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
304
+ Throttle, track, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
306
305
 
307
306
  ```ruby
308
307
  # This is the default
@@ -383,7 +382,7 @@ To get notified about specific type of events, subscribe to the event name follo
383
382
  E.g. for throttles use:
384
383
 
385
384
  ```ruby
386
- ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, request_id, payload|
385
+ ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |name, start, finish, instrumenter_id, payload|
387
386
  # request object available in payload[:request]
388
387
 
389
388
  # Your code here
@@ -393,7 +392,7 @@ end
393
392
  If you want to subscribe to every `rack_attack` event, use:
394
393
 
395
394
  ```ruby
396
- ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
395
+ ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, instrumenter_id, payload|
397
396
  # request object available in payload[:request]
398
397
 
399
398
  # Your code here
@@ -11,6 +11,7 @@ module Rack
11
11
  end
12
12
 
13
13
  def inherited(klass)
14
+ super
14
15
  proxies << klass
15
16
  end
16
17
 
@@ -55,7 +55,7 @@ module Rack
55
55
 
56
56
  def reset!
57
57
  if store.respond_to?(:delete_matched)
58
- store.delete_matched("#{prefix}*")
58
+ store.delete_matched(/#{prefix}*/)
59
59
  else
60
60
  raise(
61
61
  Rack::Attack::IncompatibleStoreError,
@@ -19,7 +19,7 @@ module Rack
19
19
  end
20
20
  end
21
21
 
22
- attr_reader :safelists, :blocklists, :throttles, :anonymous_blocklists, :anonymous_safelists
22
+ attr_reader :safelists, :blocklists, :throttles, :tracks, :anonymous_blocklists, :anonymous_safelists
23
23
  attr_accessor :blocklisted_responder, :throttled_responder, :throttled_response_retry_after_header
24
24
 
25
25
  attr_reader :blocklisted_response, :throttled_response # Keeping these for backwards compatibility
@@ -61,11 +61,15 @@ module Rack
61
61
  end
62
62
 
63
63
  def blocklist_ip(ip_address)
64
- @anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
64
+ @anonymous_blocklists << Blocklist.new do |request|
65
+ request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip))
66
+ end
65
67
  end
66
68
 
67
69
  def safelist_ip(ip_address)
68
- @anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
70
+ @anonymous_safelists << Safelist.new do |request|
71
+ request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip))
72
+ end
69
73
  end
70
74
 
71
75
  def throttle(name, options, &block)
@@ -10,17 +10,19 @@ module Rack
10
10
  store.class.name == "ActiveSupport::Cache::RedisCacheStore"
11
11
  end
12
12
 
13
- def increment(name, amount = 1, **options)
14
- # RedisCacheStore#increment ignores options[:expires_in].
15
- #
16
- # So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
17
- # the counter. After that we continue using the original RedisCacheStore#increment.
18
- if options[:expires_in] && !read(name)
19
- write(name, amount, options)
13
+ if defined?(::ActiveSupport) && ::ActiveSupport::VERSION::MAJOR < 6
14
+ def increment(name, amount = 1, **options)
15
+ # RedisCacheStore#increment ignores options[:expires_in] in versions prior to 6.
16
+ #
17
+ # So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
18
+ # the counter. After that we continue using the original RedisCacheStore#increment.
19
+ if options[:expires_in] && !read(name)
20
+ write(name, amount, options)
20
21
 
21
- amount
22
- else
23
- super
22
+ amount
23
+ else
24
+ super
25
+ end
24
26
  end
25
27
  end
26
28
 
@@ -31,6 +33,10 @@ module Rack
31
33
  def write(name, value, options = {})
32
34
  super(name, value, options.merge!(raw: true))
33
35
  end
36
+
37
+ def delete_matched(matcher, options = nil)
38
+ super(matcher.source, options)
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -45,11 +45,12 @@ module Rack
45
45
 
46
46
  def delete_matched(matcher, _options = nil)
47
47
  cursor = "0"
48
+ source = matcher.source
48
49
 
49
50
  rescuing do
50
51
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
51
52
  loop do
52
- cursor, keys = scan(cursor, match: matcher, count: 1000)
53
+ cursor, keys = scan(cursor, match: source, count: 1000)
53
54
  del(*keys) unless keys.empty?
54
55
  break if cursor == "0"
55
56
  end
@@ -38,8 +38,9 @@ module Rack
38
38
  epoch_time: cache.last_epoch_time
39
39
  }
40
40
 
41
+ annotate_request_with_throttle_data(request, data)
42
+
41
43
  (count > current_limit).tap do |throttled|
42
- annotate_request_with_throttle_data(request, data)
43
44
  if throttled
44
45
  annotate_request_with_matched_data(request, data)
45
46
  Rack::Attack.instrument(request)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class Attack
5
- VERSION = '6.7.0'
5
+ VERSION = '6.8.0'
6
6
  end
7
7
  end
data/lib/rack/attack.rb CHANGED
@@ -11,15 +11,17 @@ require 'rack/attack/store_proxy/mem_cache_store_proxy'
11
11
  require 'rack/attack/store_proxy/redis_proxy'
12
12
  require 'rack/attack/store_proxy/redis_store_proxy'
13
13
  require 'rack/attack/store_proxy/redis_cache_store_proxy'
14
- require 'rack/attack/store_proxy/active_support_redis_store_proxy'
15
14
 
16
15
  require 'rack/attack/railtie' if defined?(::Rails)
17
16
 
18
17
  module Rack
19
18
  class Attack
20
19
  class Error < StandardError; end
20
+
21
21
  class MisconfiguredStoreError < Error; end
22
+
22
23
  class MissingStoreError < Error; end
24
+
23
25
  class IncompatibleStoreError < Error; end
24
26
 
25
27
  autoload :Check, 'rack/attack/check'
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "Blocking an IP" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist_ip("1.2.3.4")
8
10
  end
@@ -19,22 +21,25 @@ describe "Blocking an IP" do
19
21
  assert_equal 200, last_response.status
20
22
  end
21
23
 
22
- it "notifies when the request is blocked" do
23
- notified = false
24
- notification_type = nil
24
+ it "succeeds if IP is missing" do
25
+ get "/", {}, "REMOTE_ADDR" => ""
26
+
27
+ assert_equal 200, last_response.status
28
+ end
25
29
 
30
+ it "notifies when the request is blocked" do
26
31
  ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
27
- notified = true
28
- notification_type = payload[:request].env["rack.attack.match_type"]
32
+ notifications.push(payload)
29
33
  end
30
34
 
31
35
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
32
36
 
33
- refute notified
37
+ assert notifications.empty?
34
38
 
35
39
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
36
40
 
37
- assert notified
38
- assert_equal :blocklist, notification_type
41
+ assert_equal 1, notifications.size
42
+ notification = notifications.pop
43
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
39
44
  end
40
45
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "#blocklist" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist do |request|
8
10
  request.ip == "1.2.3.4"
@@ -22,27 +24,26 @@ describe "#blocklist" do
22
24
  end
23
25
 
24
26
  it "notifies when the request is blocked" do
25
- notification_matched = nil
26
- notification_type = nil
27
-
28
27
  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"]
28
+ notifications.push(payload)
31
29
  end
32
30
 
33
31
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
34
32
 
35
- assert_nil notification_matched
36
- assert_nil notification_type
33
+ assert notifications.empty?
37
34
 
38
35
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
39
36
 
40
- assert_nil notification_matched
41
- assert_equal :blocklist, notification_type
37
+ assert_equal 1, notifications.size
38
+ notification = notifications.pop
39
+ assert_nil notification[:request].env["rack.attack.matched"]
40
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
42
41
  end
43
42
  end
44
43
 
45
44
  describe "#blocklist with name" do
45
+ let(:notifications) { [] }
46
+
46
47
  before do
47
48
  Rack::Attack.blocklist("block 1.2.3.4") do |request|
48
49
  request.ip == "1.2.3.4"
@@ -62,22 +63,19 @@ describe "#blocklist with name" do
62
63
  end
63
64
 
64
65
  it "notifies when the request is blocked" do
65
- notification_matched = nil
66
- notification_type = nil
67
-
68
66
  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"]
67
+ notifications.push(payload)
71
68
  end
72
69
 
73
70
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
74
71
 
75
- assert_nil notification_matched
76
- assert_nil notification_type
72
+ assert notifications.empty?
77
73
 
78
74
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
79
75
 
80
- assert_equal "block 1.2.3.4", notification_matched
81
- assert_equal :blocklist, notification_type
76
+ assert_equal 1, notifications.size
77
+ notification = notifications.pop
78
+ assert_equal "block 1.2.3.4", notification[:request].env["rack.attack.matched"]
79
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
82
80
  end
83
81
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "Blocking an IP subnet" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist_ip("1.2.3.4/31")
8
10
  end
@@ -26,21 +28,18 @@ describe "Blocking an IP subnet" do
26
28
  end
27
29
 
28
30
  it "notifies when the request is blocked" do
29
- notified = false
30
- notification_type = nil
31
-
32
31
  ActiveSupport::Notifications.subscribe("blocklist.rack_attack") do |_name, _start, _finish, _id, payload|
33
- notified = true
34
- notification_type = payload[:request].env["rack.attack.match_type"]
32
+ notifications.push(payload)
35
33
  end
36
34
 
37
35
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
38
36
 
39
- refute notified
37
+ assert notifications.empty?
40
38
 
41
39
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
42
40
 
43
- assert notified
44
- assert_equal :blocklist, notification_type
41
+ assert_equal 1, notifications.size
42
+ notification = notifications.pop
43
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
45
44
  end
46
45
  end
@@ -12,9 +12,11 @@ describe "Cache store config when using allow2ban" do
12
12
  end
13
13
  end
14
14
 
15
- it "gives semantic error if no store was configured" do
16
- assert_raises(Rack::Attack::MissingStoreError) do
17
- get "/scarce-resource"
15
+ unless defined?(Rails)
16
+ it "gives semantic error if no store was configured" do
17
+ assert_raises(Rack::Attack::MissingStoreError) do
18
+ get "/scarce-resource"
19
+ end
18
20
  end
19
21
  end
20
22
 
@@ -12,9 +12,11 @@ describe "Cache store config when using fail2ban" do
12
12
  end
13
13
  end
14
14
 
15
- it "gives semantic error if no store was configured" do
16
- assert_raises(Rack::Attack::MissingStoreError) do
17
- get "/private-place"
15
+ unless defined?(Rails)
16
+ it "gives semantic error if no store was configured" do
17
+ assert_raises(Rack::Attack::MissingStoreError) do
18
+ get "/private-place"
19
+ end
18
20
  end
19
21
  end
20
22
 
@@ -79,7 +81,7 @@ describe "Cache store config when using fail2ban" do
79
81
  end
80
82
 
81
83
  it "works with any object that responds to #read, #write and #increment" do
82
- FakeStore = Class.new do
84
+ fake_store_class = Class.new do
83
85
  attr_accessor :backend
84
86
 
85
87
  def initialize
@@ -100,7 +102,7 @@ describe "Cache store config when using fail2ban" do
100
102
  end
101
103
  end
102
104
 
103
- Rack::Attack.cache.store = FakeStore.new
105
+ Rack::Attack.cache.store = fake_store_class.new
104
106
 
105
107
  get "/"
106
108
  assert_equal 200, last_response.status
@@ -9,9 +9,11 @@ describe "Cache store config when throttling without Rails" do
9
9
  end
10
10
  end
11
11
 
12
- it "gives semantic error if no store was configured" do
13
- assert_raises(Rack::Attack::MissingStoreError) do
14
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
12
+ unless defined?(Rails)
13
+ it "gives semantic error if no store was configured" do
14
+ assert_raises(Rack::Attack::MissingStoreError) do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
16
+ end
15
17
  end
16
18
  end
17
19
 
@@ -11,10 +11,12 @@ describe "Cache store config with Rails" do
11
11
  end
12
12
  end
13
13
 
14
- it "fails when Rails.cache is not set" do
15
- Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
16
- assert_raises(Rack::Attack::MissingStoreError) do
17
- get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
14
+ unless defined?(Rails)
15
+ it "fails when Rails.cache is not set" do
16
+ Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
17
+ assert_raises(Rack::Attack::MissingStoreError) do
18
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
19
+ end
18
20
  end
19
21
  end
20
22
  end
@@ -4,10 +4,8 @@ require_relative "../spec_helper"
4
4
 
5
5
  describe "Extending the request object" do
6
6
  before do
7
- class Rack::Attack::Request
8
- def authorized?
9
- env["APIKey"] == "private-secret"
10
- end
7
+ Rack::Attack::Request.define_method :authorized? do
8
+ env["APIKey"] == "private-secret"
11
9
  end
12
10
 
13
11
  Rack::Attack.blocklist("unauthorized requests") do |request|
@@ -17,9 +15,7 @@ describe "Extending the request object" do
17
15
 
18
16
  # We don't want the extension to leak to other test cases
19
17
  after do
20
- class Rack::Attack::Request
21
- remove_method :authorized?
22
- end
18
+ Rack::Attack::Request.undef_method :authorized?
23
19
  end
24
20
 
25
21
  it "forbids request if blocklist condition is true" do
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
4
4
  require "timecop"
5
5
 
6
6
  describe "fail2ban" do
7
+ let(:notifications) { [] }
8
+
7
9
  before do
8
10
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
11
 
@@ -75,4 +77,44 @@ describe "fail2ban" do
75
77
  assert_equal 200, last_response.status
76
78
  end
77
79
  end
80
+
81
+ it "notifies when the request is blocked" do
82
+ ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
83
+ notifications.push(payload)
84
+ end
85
+
86
+ get "/"
87
+
88
+ assert_equal 200, last_response.status
89
+ assert notifications.empty?
90
+
91
+ get "/private-place"
92
+
93
+ assert_equal 403, last_response.status
94
+ assert_equal 1, notifications.size
95
+ notification = notifications.pop
96
+ assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
97
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
98
+
99
+ get "/"
100
+
101
+ assert_equal 200, last_response.status
102
+ assert notifications.empty?
103
+
104
+ get "/private-place"
105
+
106
+ assert_equal 403, last_response.status
107
+ assert_equal 1, notifications.size
108
+ notification = notifications.pop
109
+ assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
110
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
111
+
112
+ get "/"
113
+
114
+ assert_equal 403, last_response.status
115
+ assert_equal 1, notifications.size
116
+ notification = notifications.pop
117
+ assert_equal 'fail2ban pentesters', notification[:request].env["rack.attack.matched"]
118
+ assert_equal :blocklist, notification[:request].env["rack.attack.match_type"]
119
+ end
78
120
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "Safelist an IP" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist("admin") do |request|
8
10
  request.path == "/admin"
@@ -17,6 +19,12 @@ describe "Safelist an IP" do
17
19
  assert_equal 403, last_response.status
18
20
  end
19
21
 
22
+ it "forbids request if blocklist condition is true and safelist is false (missing IP)" do
23
+ get "/admin", {}, "REMOTE_ADDR" => ""
24
+
25
+ assert_equal 403, last_response.status
26
+ end
27
+
20
28
  it "succeeds if blocklist condition is false and safelist is false" do
21
29
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
22
30
 
@@ -36,15 +44,15 @@ describe "Safelist an IP" do
36
44
  end
37
45
 
38
46
  it "notifies when the request is safe" do
39
- notification_type = nil
40
-
41
47
  ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
- notification_type = payload[:request].env["rack.attack.match_type"]
48
+ notifications.push(payload)
43
49
  end
44
50
 
45
51
  get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
46
52
 
47
53
  assert_equal 200, last_response.status
48
- assert_equal :safelist, notification_type
54
+ assert_equal 1, notifications.size
55
+ notification = notifications.pop
56
+ assert_equal :safelist, notification[:request].env["rack.attack.match_type"]
49
57
  end
50
58
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "#safelist" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist do |request|
8
10
  request.ip == "1.2.3.4"
@@ -38,23 +40,23 @@ describe "#safelist" do
38
40
  end
39
41
 
40
42
  it "notifies when the request is safe" do
41
- notification_matched = nil
42
- notification_type = nil
43
-
44
43
  ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, payload|
45
- notification_matched = payload[:request].env["rack.attack.matched"]
46
- notification_type = payload[:request].env["rack.attack.match_type"]
44
+ notifications.push(payload)
47
45
  end
48
46
 
49
47
  get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
50
48
 
51
49
  assert_equal 200, last_response.status
52
- assert_nil notification_matched
53
- assert_equal :safelist, notification_type
50
+ assert_equal 1, notifications.size
51
+ notification = notifications.pop
52
+ assert_nil notification[:request].env["rack.attack.matched"]
53
+ assert_equal :safelist, notification[:request].env["rack.attack.match_type"]
54
54
  end
55
55
  end
56
56
 
57
57
  describe "#safelist with name" do
58
+ let(:notifications) { [] }
59
+
58
60
  before do
59
61
  Rack::Attack.blocklist("block 1.2.3.4") do |request|
60
62
  request.ip == "1.2.3.4"
@@ -90,18 +92,16 @@ describe "#safelist with name" do
90
92
  end
91
93
 
92
94
  it "notifies when the request is safe" do
93
- notification_matched = nil
94
- notification_type = nil
95
-
96
95
  ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
97
- notification_matched = payload[:request].env["rack.attack.matched"]
98
- notification_type = payload[:request].env["rack.attack.match_type"]
96
+ notifications.push(payload)
99
97
  end
100
98
 
101
99
  get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
102
100
 
103
101
  assert_equal 200, last_response.status
104
- assert_equal "safe path", notification_matched
105
- assert_equal :safelist, notification_type
102
+ assert_equal 1, notifications.size
103
+ notification = notifications.pop
104
+ assert_equal "safe path", notification[:request].env["rack.attack.matched"]
105
+ assert_equal :safelist, notification[:request].env["rack.attack.match_type"]
106
106
  end
107
107
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "Safelisting an IP subnet" do
6
+ let(:notifications) { [] }
7
+
6
8
  before do
7
9
  Rack::Attack.blocklist("admin") do |request|
8
10
  request.path == "/admin"
@@ -36,15 +38,15 @@ describe "Safelisting an IP subnet" do
36
38
  end
37
39
 
38
40
  it "notifies when the request is safe" do
39
- notification_type = nil
40
-
41
41
  ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
- notification_type = payload[:request].env["rack.attack.match_type"]
42
+ notifications.push(payload)
43
43
  end
44
44
 
45
45
  get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
46
46
 
47
47
  assert_equal 200, last_response.status
48
- assert_equal :safelist, notification_type
48
+ assert_equal 1, notifications.size
49
+ notification = notifications.pop
50
+ assert_equal :safelist, notification[:request].env["rack.attack.match_type"]
49
51
  end
50
52
  end