rack-attack 5.4.2 → 6.2.2

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +78 -27
  3. data/Rakefile +3 -1
  4. data/lib/rack/attack.rb +138 -149
  5. data/lib/rack/attack/allow2ban.rb +2 -0
  6. data/lib/rack/attack/blocklist.rb +3 -1
  7. data/lib/rack/attack/cache.rb +9 -4
  8. data/lib/rack/attack/check.rb +5 -2
  9. data/lib/rack/attack/fail2ban.rb +2 -0
  10. data/lib/rack/attack/path_normalizer.rb +22 -18
  11. data/lib/rack/attack/railtie.rb +13 -0
  12. data/lib/rack/attack/request.rb +2 -0
  13. data/lib/rack/attack/safelist.rb +3 -1
  14. data/lib/rack/attack/store_proxy.rb +12 -14
  15. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
  16. data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
  17. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
  18. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +22 -8
  19. data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -14
  20. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
  21. data/lib/rack/attack/throttle.rb +12 -8
  22. data/lib/rack/attack/track.rb +9 -6
  23. data/lib/rack/attack/version.rb +3 -1
  24. data/spec/acceptance/allow2ban_spec.rb +2 -0
  25. data/spec/acceptance/blocking_ip_spec.rb +4 -2
  26. data/spec/acceptance/blocking_spec.rb +45 -3
  27. data/spec/acceptance/blocking_subnet_spec.rb +4 -2
  28. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +8 -12
  29. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +8 -12
  30. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
  31. data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
  32. data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
  33. data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
  34. data/spec/acceptance/extending_request_object_spec.rb +2 -0
  35. data/spec/acceptance/fail2ban_spec.rb +2 -0
  36. data/spec/acceptance/rails_middleware_spec.rb +35 -0
  37. data/spec/acceptance/safelisting_ip_spec.rb +4 -2
  38. data/spec/acceptance/safelisting_spec.rb +57 -3
  39. data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
  40. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +2 -0
  41. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
  42. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +2 -0
  43. data/spec/acceptance/stores/active_support_memory_store_spec.rb +2 -0
  44. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +9 -1
  45. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +8 -1
  46. data/spec/acceptance/stores/active_support_redis_store_spec.rb +3 -1
  47. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -3
  48. data/spec/acceptance/stores/dalli_client_spec.rb +2 -0
  49. data/spec/acceptance/stores/redis_store_spec.rb +2 -0
  50. data/spec/acceptance/throttling_spec.rb +7 -5
  51. data/spec/acceptance/track_spec.rb +5 -3
  52. data/spec/acceptance/track_throttle_spec.rb +5 -3
  53. data/spec/allow2ban_spec.rb +20 -15
  54. data/spec/fail2ban_spec.rb +20 -17
  55. data/spec/integration/offline_spec.rb +15 -1
  56. data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
  57. data/spec/rack_attack_instrumentation_spec.rb +42 -0
  58. data/spec/rack_attack_path_normalizer_spec.rb +4 -2
  59. data/spec/rack_attack_request_spec.rb +2 -0
  60. data/spec/rack_attack_spec.rb +38 -34
  61. data/spec/rack_attack_throttle_spec.rb +50 -19
  62. data/spec/rack_attack_track_spec.rb +12 -7
  63. data/spec/spec_helper.rb +12 -8
  64. data/spec/support/cache_store_helper.rb +2 -0
  65. metadata +71 -56
  66. data/bin/setup +0 -8
  67. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +0 -50
@@ -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, @block = name, block
9
+ def initialize(name, options, &block)
10
+ @name = name
11
+ @block = block
9
12
  MANDATORY_OPTIONS.each do |opt|
10
- raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt]
13
+ raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
11
14
  end
12
- @limit = options[: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
- :count => count,
33
- :period => current_period,
34
- :limit => current_limit,
35
- :epoch_time => epoch_time
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
@@ -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
- if options[:limit] && options[:period]
10
- @filter = Throttle.new(name, options, block)
11
- else
12
- @filter = Check.new(name, options, block)
13
- end
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)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Attack
3
- VERSION = '5.4.2'
5
+ VERSION = '6.2.2'
4
6
  end
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "timecop"
3
5
 
@@ -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("rack.attack") do |_name, _start, _finish, _id, request|
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("rack.attack") do |_name, _start, _finish, _id, request|
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("rack.attack") do |_name, _start, _finish, _id, request|
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
@@ -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 fail2ban" 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 fail2ban" 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 fail2ban" 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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Cache store config when throttling without Rails" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "minitest/stub_const"
3
5
  require "ostruct"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Customizing block responses" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Customizing throttled response" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Extending the request object" do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
  require "timecop"
3
5
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../spec_helper"
4
+
5
+ if defined?(Rails)
6
+ describe "Middleware for Rails" do
7
+ before do
8
+ @app = Class.new(Rails::Application) do
9
+ config.eager_load = false
10
+ config.logger = Logger.new(nil) # avoid creating the log/ directory automatically
11
+ config.cache_store = :null_store # avoid creating tmp/ directory for cache
12
+ end
13
+ end
14
+
15
+ if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("5.1")
16
+ it "is used by default" do
17
+ @app.initialize!
18
+ assert_equal 1, @app.middleware.count(Rack::Attack)
19
+ end
20
+
21
+ it "is not added when it was explicitly deleted" do
22
+ @app.config.middleware.delete(Rack::Attack)
23
+ @app.initialize!
24
+ refute @app.middleware.include?(Rack::Attack)
25
+ end
26
+ end
27
+
28
+ if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
29
+ it "is not used by default" do
30
+ @app.initialize!
31
+ assert_equal 0, @app.middleware.count(Rack::Attack)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Safelist an IP" do
@@ -36,8 +38,8 @@ describe "Safelist an IP" do
36
38
  it "notifies when the request is safe" do
37
39
  notification_type = nil
38
40
 
39
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
- notification_type = request.env["rack.attack.match_type"]
41
+ ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
+ notification_type = payload[:request].env["rack.attack.match_type"]
41
43
  end
42
44
 
43
45
  get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -1,6 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "#safelist" do
6
+ before do
7
+ Rack::Attack.blocklist do |request|
8
+ request.ip == "1.2.3.4"
9
+ end
10
+
11
+ Rack::Attack.safelist do |request|
12
+ request.path == "/safe_space"
13
+ end
14
+ end
15
+
16
+ it "forbids request if blocklist condition is true and safelist is false" do
17
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
18
+
19
+ assert_equal 403, last_response.status
20
+ end
21
+
22
+ it "succeeds if blocklist condition is false and safelist is false" do
23
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
24
+
25
+ assert_equal 200, last_response.status
26
+ end
27
+
28
+ it "succeeds request if blocklist condition is false and safelist is true" do
29
+ get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
30
+
31
+ assert_equal 200, last_response.status
32
+ end
33
+
34
+ it "succeeds request if both blocklist and safelist conditions are true" do
35
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
36
+
37
+ assert_equal 200, last_response.status
38
+ end
39
+
40
+ it "notifies when the request is safe" do
41
+ notification_matched = nil
42
+ notification_type = nil
43
+
44
+ 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"]
47
+ end
48
+
49
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
50
+
51
+ assert_equal 200, last_response.status
52
+ assert_nil notification_matched
53
+ assert_equal :safelist, notification_type
54
+ end
55
+ end
56
+
57
+ describe "#safelist with name" do
4
58
  before do
5
59
  Rack::Attack.blocklist("block 1.2.3.4") do |request|
6
60
  request.ip == "1.2.3.4"
@@ -39,9 +93,9 @@ describe "#safelist" do
39
93
  notification_matched = nil
40
94
  notification_type = nil
41
95
 
42
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
43
- notification_matched = request.env["rack.attack.matched"]
44
- notification_type = request.env["rack.attack.match_type"]
96
+ 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"]
45
99
  end
46
100
 
47
101
  get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../spec_helper"
2
4
 
3
5
  describe "Safelisting an IP subnet" do
@@ -36,8 +38,8 @@ describe "Safelisting an IP subnet" do
36
38
  it "notifies when the request is safe" do
37
39
  notification_type = nil
38
40
 
39
- ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
40
- notification_type = request.env["rack.attack.match_type"]
41
+ ActiveSupport::Notifications.subscribe("safelist.rack_attack") do |_name, _start, _finish, _id, payload|
42
+ notification_type = payload[:request].env["rack.attack.match_type"]
41
43
  end
42
44
 
43
45
  get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"