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
@@ -4,11 +4,14 @@ require_relative "../../spec_helper"
4
4
 
5
5
  if defined?(::ConnectionPool) && defined?(::Dalli)
6
6
  require_relative "../../support/cache_store_helper"
7
- require "timecop"
8
7
 
9
8
  describe "ActiveSupport::Cache::MemCacheStore (pooled) as a cache backend" do
10
9
  before do
11
- Rack::Attack.cache.store = ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
10
+ Rack::Attack.cache.store = if ActiveSupport.gem_version >= Gem::Version.new("7.2.0")
11
+ ActiveSupport::Cache::MemCacheStore.new(pool: true)
12
+ else
13
+ ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
14
+ end
12
15
  end
13
16
 
14
17
  after do
@@ -4,7 +4,6 @@ require_relative "../../spec_helper"
4
4
 
5
5
  if defined?(::Dalli)
6
6
  require_relative "../../support/cache_store_helper"
7
- require "timecop"
8
7
 
9
8
  describe "ActiveSupport::Cache::MemCacheStore as a cache backend" do
10
9
  before do
@@ -3,8 +3,6 @@
3
3
  require_relative "../../spec_helper"
4
4
  require_relative "../../support/cache_store_helper"
5
5
 
6
- require "timecop"
7
-
8
6
  describe "ActiveSupport::Cache::MemoryStore as a cache backend" do
9
7
  before do
10
8
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@@ -10,11 +10,14 @@ should_run =
10
10
 
11
11
  if should_run
12
12
  require_relative "../../support/cache_store_helper"
13
- require "timecop"
14
13
 
15
14
  describe "ActiveSupport::Cache::RedisCacheStore (pooled) as a cache backend" do
16
15
  before do
17
- Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(pool_size: 2)
16
+ Rack::Attack.cache.store = if ActiveSupport.gem_version >= Gem::Version.new("7.2.0")
17
+ ActiveSupport::Cache::RedisCacheStore.new(pool: true)
18
+ else
19
+ ActiveSupport::Cache::RedisCacheStore.new(pool_size: 2)
20
+ end
18
21
  end
19
22
 
20
23
  after do
@@ -9,7 +9,6 @@ should_run =
9
9
 
10
10
  if should_run
11
11
  require_relative "../../support/cache_store_helper"
12
- require "timecop"
13
12
 
14
13
  describe "ActiveSupport::Cache::RedisCacheStore as a cache backend" do
15
14
  before do
@@ -6,7 +6,6 @@ if defined?(::Dalli) && defined?(::ConnectionPool)
6
6
  require_relative "../../support/cache_store_helper"
7
7
  require "connection_pool"
8
8
  require "dalli"
9
- require "timecop"
10
9
 
11
10
  describe "ConnectionPool with Dalli::Client as a cache backend" do
12
11
  before do
@@ -5,7 +5,6 @@ require_relative "../../spec_helper"
5
5
  if defined?(::Dalli)
6
6
  require_relative "../../support/cache_store_helper"
7
7
  require "dalli"
8
- require "timecop"
9
8
 
10
9
  describe "Dalli::Client as a cache backend" do
11
10
  before do
@@ -4,7 +4,6 @@ require_relative "../../spec_helper"
4
4
 
5
5
  if defined?(::Redis)
6
6
  require_relative "../../support/cache_store_helper"
7
- require "timecop"
8
7
 
9
8
  describe "Plain redis as a cache backend" do
10
9
  before do
@@ -4,9 +4,7 @@ require_relative "../../spec_helper"
4
4
  require_relative "../../support/cache_store_helper"
5
5
 
6
6
  if defined?(::Redis::Store)
7
- require "timecop"
8
-
9
- describe "ActiveSupport::Cache::RedisStore as a cache backend" do
7
+ describe "Redis::Store as a cache backend" do
10
8
  before do
11
9
  Rack::Attack.cache.store = ::Redis::Store.new
12
10
  end
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
4
4
  require "timecop"
5
5
 
6
6
  describe "#throttle" do
7
+ let(:notifications) { [] }
8
+
7
9
  before do
8
10
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
11
  end
@@ -138,42 +140,31 @@ describe "#throttle" do
138
140
  request.ip
139
141
  end
140
142
 
141
- notification_matched = nil
142
- notification_type = nil
143
- notification_data = nil
144
- notification_discriminator = nil
145
-
146
143
  ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |_name, _start, _finish, _id, payload|
147
- notification_matched = payload[:request].env["rack.attack.matched"]
148
- notification_type = payload[:request].env["rack.attack.match_type"]
149
- notification_data = payload[:request].env['rack.attack.match_data']
150
- notification_discriminator = payload[:request].env['rack.attack.match_discriminator']
144
+ notifications.push(payload)
151
145
  end
152
146
 
153
147
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
154
148
 
155
149
  assert_equal 200, last_response.status
156
- assert_nil notification_matched
157
- assert_nil notification_type
158
- assert_nil notification_data
159
- assert_nil notification_discriminator
150
+ assert notifications.empty?
160
151
 
161
152
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
162
153
 
163
154
  assert_equal 200, last_response.status
164
- assert_nil notification_matched
165
- assert_nil notification_type
166
- assert_nil notification_data
167
- assert_nil notification_discriminator
155
+ assert notifications.empty?
168
156
 
169
157
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
170
158
 
171
159
  assert_equal 429, last_response.status
172
- assert_equal "by ip", notification_matched
173
- assert_equal :throttle, notification_type
174
- assert_equal 60, notification_data[:period]
175
- assert_equal 1, notification_data[:limit]
176
- assert_equal 2, notification_data[:count]
177
- assert_equal "1.2.3.4", notification_discriminator
160
+
161
+ assert_equal 1, notifications.size
162
+ notification = notifications.pop
163
+ assert_equal "by ip", notification[:request].env["rack.attack.matched"]
164
+ assert_equal :throttle, notification[:request].env["rack.attack.match_type"]
165
+ assert_equal 60, notification[:request].env["rack.attack.match_data"][:period]
166
+ assert_equal 1, notification[:request].env["rack.attack.match_data"][:limit]
167
+ assert_equal 2, notification[:request].env["rack.attack.match_data"][:count]
168
+ assert_equal "1.2.3.4", notification[:request].env["rack.attack.match_discriminator"]
178
169
  end
179
170
  end
@@ -3,27 +3,26 @@
3
3
  require_relative "../spec_helper"
4
4
 
5
5
  describe "#track" do
6
+ let(:notifications) { [] }
7
+
6
8
  it "notifies when track block returns true" do
7
9
  Rack::Attack.track("ip 1.2.3.4") do |request|
8
10
  request.ip == "1.2.3.4"
9
11
  end
10
12
 
11
- notification_matched = nil
12
- notification_type = nil
13
-
14
13
  ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
15
- notification_matched = payload[:request].env["rack.attack.matched"]
16
- notification_type = payload[:request].env["rack.attack.match_type"]
14
+ notifications.push(payload)
17
15
  end
18
16
 
19
17
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
20
18
 
21
- assert_nil notification_matched
22
- assert_nil notification_type
19
+ assert notifications.empty?
23
20
 
24
21
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
25
22
 
26
- assert_equal "ip 1.2.3.4", notification_matched
27
- assert_equal :track, notification_type
23
+ assert_equal 1, notifications.size
24
+ notification = notifications.pop
25
+ assert_equal "ip 1.2.3.4", notification[:request].env["rack.attack.matched"]
26
+ assert_equal :track, notification[:request].env["rack.attack.match_type"]
28
27
  end
29
28
  end
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
4
4
  require "timecop"
5
5
 
6
6
  describe "#track with throttle-ish options" do
7
+ let(:notifications) { [] }
8
+
7
9
  it "notifies when throttle goes over the limit without actually throttling requests" do
8
10
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
11
 
@@ -11,43 +13,35 @@ describe "#track with throttle-ish options" do
11
13
  request.ip
12
14
  end
13
15
 
14
- notification_matched = nil
15
- notification_type = nil
16
-
17
16
  ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
18
- notification_matched = payload[:request].env["rack.attack.matched"]
19
- notification_type = payload[:request].env["rack.attack.match_type"]
17
+ notifications.push(payload)
20
18
  end
21
19
 
22
20
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
23
21
 
24
- assert_nil notification_matched
25
- assert_nil notification_type
22
+ assert notifications.empty?
26
23
 
27
24
  assert_equal 200, last_response.status
28
25
 
29
26
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
30
27
 
31
- assert_nil notification_matched
32
- assert_nil notification_type
28
+ assert notifications.empty?
33
29
 
34
30
  assert_equal 200, last_response.status
35
31
 
36
32
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
37
33
 
38
- assert_equal "by ip", notification_matched
39
- assert_equal :track, notification_type
34
+ assert_equal 1, notifications.size
35
+ notification = notifications.pop
36
+ assert_equal "by ip", notification[:request].env["rack.attack.matched"]
37
+ assert_equal :track, notification[:request].env["rack.attack.match_type"]
40
38
 
41
39
  assert_equal 200, last_response.status
42
40
 
43
41
  Timecop.travel(60) do
44
- notification_matched = nil
45
- notification_type = nil
46
-
47
42
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
48
43
 
49
- assert_nil notification_matched
50
- assert_nil notification_type
44
+ assert notifications.empty?
51
45
 
52
46
  assert_equal 200, last_response.status
53
47
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ describe Rack::Attack::Configuration do
6
+ subject { Rack::Attack::Configuration.new }
7
+
8
+ describe 'attributes' do
9
+ it 'exposes the safelists attribute' do
10
+ _(subject.safelists).must_equal({})
11
+ end
12
+
13
+ it 'exposes the blocklists attribute' do
14
+ _(subject.blocklists).must_equal({})
15
+ end
16
+
17
+ it 'exposes the throttles attribute' do
18
+ _(subject.throttles).must_equal({})
19
+ end
20
+
21
+ it 'exposes the tracks attribute' do
22
+ _(subject.tracks).must_equal({})
23
+ end
24
+
25
+ it 'exposes the anonymous_blocklists attribute' do
26
+ _(subject.anonymous_blocklists).must_equal([])
27
+ end
28
+
29
+ it 'exposes the anonymous_safelists attribute' do
30
+ _(subject.anonymous_safelists).must_equal([])
31
+ end
32
+ end
33
+ end
@@ -21,18 +21,6 @@ OfflineExamples = Minitest::SharedExamples.new do
21
21
  end
22
22
  end
23
23
 
24
- if defined?(::ActiveSupport::Cache::RedisStore)
25
- describe 'when Redis is offline' do
26
- include OfflineExamples
27
-
28
- before do
29
- @cache = Rack::Attack::Cache.new
30
- # Use presumably unused port for Redis client
31
- @cache.store = ActiveSupport::Cache::RedisStore.new(host: '127.0.0.1', port: 3333)
32
- end
33
- end
34
- end
35
-
36
24
  if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
37
25
  describe 'when Redis is offline' do
38
26
  include OfflineExamples
@@ -2,42 +2,38 @@
2
2
 
3
3
  require_relative "spec_helper"
4
4
  require 'active_support'
5
+ require 'active_support/subscriber'
5
6
 
6
- # ActiveSupport::Subscribers added in ~> 4.0.2.0
7
- if ActiveSupport::VERSION::MAJOR > 3
8
- require_relative 'spec_helper'
9
- require 'active_support/subscriber'
10
- class CustomSubscriber < ActiveSupport::Subscriber
11
- @notification_count = 0
7
+ class CustomSubscriber < ActiveSupport::Subscriber
8
+ @notification_count = 0
12
9
 
13
- class << self
14
- attr_accessor :notification_count
15
- end
10
+ class << self
11
+ attr_accessor :notification_count
12
+ end
16
13
 
17
- def throttle(_event)
18
- self.class.notification_count += 1
19
- end
14
+ def throttle(_event)
15
+ self.class.notification_count += 1
20
16
  end
17
+ end
21
18
 
22
- describe 'Rack::Attack.instrument' do
23
- before do
24
- @period = 60 # Use a long period; failures due to cache key rotation less likely
25
- Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
26
- Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
27
- end
19
+ describe 'Rack::Attack.instrument' do
20
+ before do
21
+ @period = 60 # Use a long period; failures due to cache key rotation less likely
22
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
23
+ Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
24
+ end
28
25
 
29
- describe "with throttling" do
30
- before do
31
- ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do
32
- CustomSubscriber.attach_to("rack_attack")
33
- 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
34
- end
26
+ describe "with throttling" do
27
+ before do
28
+ ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do
29
+ CustomSubscriber.attach_to("rack_attack")
30
+ 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
35
31
  end
32
+ end
36
33
 
37
- it 'should instrument without error' do
38
- _(last_response.status).must_equal 429
39
- assert_equal 1, CustomSubscriber.notification_count
40
- end
34
+ it 'should instrument without error' do
35
+ _(last_response.status).must_equal 429
36
+ assert_equal 1, CustomSubscriber.notification_count
41
37
  end
42
38
  end
43
39
  end
@@ -5,10 +5,8 @@ require_relative 'spec_helper'
5
5
  describe 'Rack::Attack' do
6
6
  describe 'helpers' do
7
7
  before do
8
- class Rack::Attack::Request
9
- def remote_ip
10
- ip
11
- end
8
+ Rack::Attack::Request.define_method :remote_ip do
9
+ ip
12
10
  end
13
11
 
14
12
  Rack::Attack.safelist('valid IP') do |req|
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ describe "Rack::Attack.reset!" do
6
+ it "raises an error when is not supported by cache store" do
7
+ Rack::Attack.cache.store = Class.new
8
+ assert_raises(Rack::Attack::IncompatibleStoreError) do
9
+ Rack::Attack.reset!
10
+ end
11
+ end
12
+
13
+ if defined?(Redis)
14
+ it "should delete rack attack keys" do
15
+ redis = Redis.new
16
+ redis.set("key", "value")
17
+ redis.set("#{Rack::Attack.cache.prefix}::key", "value")
18
+ Rack::Attack.cache.store = redis
19
+ Rack::Attack.reset!
20
+
21
+ _(redis.get("key")).must_equal "value"
22
+ _(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
23
+ end
24
+ end
25
+
26
+ if defined?(Redis::Store)
27
+ it "should delete rack attack keys" do
28
+ redis_store = Redis::Store.new
29
+ redis_store.set("key", "value")
30
+ redis_store.set("#{Rack::Attack.cache.prefix}::key", "value")
31
+ Rack::Attack.cache.store = redis_store
32
+ Rack::Attack.reset!
33
+
34
+ _(redis_store.get("key")).must_equal "value"
35
+ _(redis_store.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
36
+ end
37
+ end
38
+
39
+ if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore)
40
+ it "should delete rack attack keys" do
41
+ redis_cache_store = ActiveSupport::Cache::RedisCacheStore.new
42
+ redis_cache_store.write("key", "value")
43
+ redis_cache_store.write("#{Rack::Attack.cache.prefix}::key", "value")
44
+ Rack::Attack.cache.store = redis_cache_store
45
+ Rack::Attack.reset!
46
+
47
+ _(redis_cache_store.read("key")).must_equal "value"
48
+ _(redis_cache_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
49
+ end
50
+
51
+ describe "with a namespaced cache" do
52
+ it "should delete rack attack keys" do
53
+ redis_cache_store = ActiveSupport::Cache::RedisCacheStore.new(namespace: "ns")
54
+ redis_cache_store.write("key", "value")
55
+ redis_cache_store.write("#{Rack::Attack.cache.prefix}::key", "value")
56
+ Rack::Attack.cache.store = redis_cache_store
57
+ Rack::Attack.reset!
58
+
59
+ _(redis_cache_store.read("key")).must_equal "value"
60
+ _(redis_cache_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
61
+ end
62
+ end
63
+ end
64
+
65
+ if defined?(ActiveSupport::Cache::MemoryStore)
66
+ it "should delete rack attack keys" do
67
+ memory_store = ActiveSupport::Cache::MemoryStore.new
68
+ memory_store.write("key", "value")
69
+ memory_store.write("#{Rack::Attack.cache.prefix}::key", "value")
70
+ Rack::Attack.cache.store = memory_store
71
+ Rack::Attack.reset!
72
+
73
+ _(memory_store.read("key")).must_equal "value"
74
+ _(memory_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
75
+ end
76
+
77
+ describe "with a namespaced cache" do
78
+ it "should delete rack attack keys" do
79
+ memory_store = ActiveSupport::Cache::MemoryStore.new(namespace: "ns")
80
+ memory_store.write("key", "value")
81
+ memory_store.write("#{Rack::Attack.cache.prefix}::key", "value")
82
+ Rack::Attack.cache.store = memory_store
83
+ Rack::Attack.reset!
84
+
85
+ _(memory_store.read("key")).must_equal "value"
86
+ _(memory_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
87
+ end
88
+ end
89
+ end
90
+ end
@@ -103,26 +103,4 @@ describe 'Rack::Attack' do
103
103
  end
104
104
  end
105
105
  end
106
-
107
- describe 'reset!' do
108
- it 'raises an error when is not supported by cache store' do
109
- Rack::Attack.cache.store = Class.new
110
- assert_raises(Rack::Attack::IncompatibleStoreError) do
111
- Rack::Attack.reset!
112
- end
113
- end
114
-
115
- if defined?(Redis)
116
- it 'should delete rack attack keys' do
117
- redis = Redis.new
118
- redis.set('key', 'value')
119
- redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
120
- Rack::Attack.cache.store = redis
121
- Rack::Attack.reset!
122
-
123
- _(redis.get('key')).must_equal 'value'
124
- _(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
125
- end
126
- end
127
- end
128
106
  end