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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +78 -27
  3. data/Rakefile +3 -1
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +137 -148
  6. data/lib/rack/attack/allow2ban.rb +2 -0
  7. data/lib/rack/attack/blocklist.rb +3 -1
  8. data/lib/rack/attack/cache.rb +9 -4
  9. data/lib/rack/attack/check.rb +5 -2
  10. data/lib/rack/attack/fail2ban.rb +2 -0
  11. data/lib/rack/attack/path_normalizer.rb +22 -18
  12. data/lib/rack/attack/railtie.rb +21 -0
  13. data/lib/rack/attack/request.rb +2 -0
  14. data/lib/rack/attack/safelist.rb +3 -1
  15. data/lib/rack/attack/store_proxy.rb +12 -24
  16. data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +39 -0
  17. data/lib/rack/attack/store_proxy/dalli_proxy.rb +27 -13
  18. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +21 -0
  19. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +23 -9
  20. data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -10
  21. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -5
  22. data/lib/rack/attack/throttle.rb +12 -8
  23. data/lib/rack/attack/track.rb +9 -6
  24. data/lib/rack/attack/version.rb +3 -1
  25. data/spec/acceptance/allow2ban_spec.rb +2 -0
  26. data/spec/acceptance/blocking_ip_spec.rb +4 -2
  27. data/spec/acceptance/blocking_spec.rb +45 -3
  28. data/spec/acceptance/blocking_subnet_spec.rb +4 -2
  29. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +50 -39
  30. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +38 -29
  31. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +2 -0
  32. data/spec/acceptance/cache_store_config_with_rails_spec.rb +2 -0
  33. data/spec/acceptance/customizing_blocked_response_spec.rb +2 -0
  34. data/spec/acceptance/customizing_throttled_response_spec.rb +2 -0
  35. data/spec/acceptance/extending_request_object_spec.rb +2 -0
  36. data/spec/acceptance/fail2ban_spec.rb +2 -0
  37. data/spec/acceptance/rails_middleware_spec.rb +41 -0
  38. data/spec/acceptance/safelisting_ip_spec.rb +4 -2
  39. data/spec/acceptance/safelisting_spec.rb +57 -3
  40. data/spec/acceptance/safelisting_subnet_spec.rb +4 -2
  41. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +3 -23
  42. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +20 -0
  43. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +4 -24
  44. data/spec/acceptance/stores/active_support_memory_store_spec.rb +3 -23
  45. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +10 -24
  46. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +9 -25
  47. data/spec/acceptance/stores/active_support_redis_store_spec.rb +4 -24
  48. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +5 -23
  49. data/spec/acceptance/stores/dalli_client_spec.rb +3 -23
  50. data/spec/acceptance/stores/redis_spec.rb +1 -23
  51. data/spec/acceptance/stores/redis_store_spec.rb +3 -23
  52. data/spec/acceptance/throttling_spec.rb +7 -5
  53. data/spec/acceptance/track_spec.rb +5 -3
  54. data/spec/acceptance/track_throttle_spec.rb +5 -3
  55. data/spec/allow2ban_spec.rb +20 -15
  56. data/spec/fail2ban_spec.rb +20 -17
  57. data/spec/integration/offline_spec.rb +3 -1
  58. data/spec/rack_attack_dalli_proxy_spec.rb +2 -0
  59. data/spec/rack_attack_instrumentation_spec.rb +42 -0
  60. data/spec/rack_attack_path_normalizer_spec.rb +4 -2
  61. data/spec/rack_attack_request_spec.rb +2 -0
  62. data/spec/rack_attack_spec.rb +38 -34
  63. data/spec/rack_attack_throttle_spec.rb +50 -19
  64. data/spec/rack_attack_track_spec.rb +12 -7
  65. data/spec/spec_helper.rb +10 -8
  66. data/spec/support/cache_store_helper.rb +27 -1
  67. metadata +48 -28
  68. 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
@@ -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.0'
5
+ VERSION = '6.2.0'
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,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
- basic_store_class = Class.new do
20
- def write(key, value)
21
- end
22
+ raised_exception = nil
22
23
 
23
- def increment(key, count, options = {})
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
- Rack::Attack.cache.store = basic_store_class.new
30
+ Object.stub_const(:FakeStore, fake_store_class) do
31
+ Rack::Attack.cache.store = FakeStore.new
28
32
 
29
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
30
- get "/scarce-resource"
33
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
34
+ get "/scarce-resource"
35
+ end
31
36
  end
32
37
 
33
- assert_equal "Store needs to respond to #read", raised_exception.message
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
- basic_store_class = Class.new do
38
- def read(key)
39
- end
42
+ raised_exception = nil
40
43
 
41
- def increment(key, count, options = {})
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
- Rack::Attack.cache.store = basic_store_class.new
50
+ Object.stub_const(:FakeStore, fake_store_class) do
51
+ Rack::Attack.cache.store = FakeStore.new
46
52
 
47
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
48
- get "/scarce-resource"
53
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
54
+ get "/scarce-resource"
55
+ end
49
56
  end
50
57
 
51
- assert_equal "Store needs to respond to #write", raised_exception.message
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
- basic_store_class = Class.new do
56
- def read(key)
57
- end
62
+ raised_exception = nil
58
63
 
59
- def write(key, value)
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
- Rack::Attack.cache.store = basic_store_class.new
70
+ Object.stub_const(:FakeStore, fake_store_class) do
71
+ Rack::Attack.cache.store = FakeStore.new
64
72
 
65
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
66
- get "/scarce-resource"
73
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
74
+ get "/scarce-resource"
75
+ end
67
76
  end
68
77
 
69
- assert_equal "Store needs to respond to #increment", raised_exception.message
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
- basic_store_class = Class.new do
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
- Rack::Attack.cache.store = basic_store_class.new
103
+ Object.stub_const(:FakeStore, fake_store_class) do
104
+ Rack::Attack.cache.store = FakeStore.new
95
105
 
96
- get "/"
97
- assert_equal 200, last_response.status
106
+ get "/"
107
+ assert_equal 200, last_response.status
98
108
 
99
- get "/scarce-resource"
100
- assert_equal 200, last_response.status
109
+ get "/scarce-resource"
110
+ assert_equal 200, last_response.status
101
111
 
102
- get "/scarce-resource"
103
- assert_equal 200, last_response.status
112
+ get "/scarce-resource"
113
+ assert_equal 200, last_response.status
104
114
 
105
- get "/scarce-resource"
106
- assert_equal 403, last_response.status
115
+ get "/scarce-resource"
116
+ assert_equal 403, last_response.status
107
117
 
108
- get "/"
109
- assert_equal 403, last_response.status
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
- basic_store_class = Class.new do
20
- def write(key, value)
21
- end
22
+ raised_exception = nil
22
23
 
23
- def increment(key, count, options = {})
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
- Rack::Attack.cache.store = basic_store_class.new
30
+ Object.stub_const(:FakeStore, fake_store_class) do
31
+ Rack::Attack.cache.store = FakeStore.new
28
32
 
29
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
30
- get "/private-place"
33
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
34
+ get "/private-place"
35
+ end
31
36
  end
32
37
 
33
- assert_equal "Store needs to respond to #read", raised_exception.message
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
- basic_store_class = Class.new do
38
- def read(key)
39
- end
42
+ raised_exception = nil
40
43
 
41
- def increment(key, count, options = {})
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
- Rack::Attack.cache.store = basic_store_class.new
50
+ Object.stub_const(:FakeStore, fake_store_class) do
51
+ Rack::Attack.cache.store = FakeStore.new
46
52
 
47
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
48
- get "/private-place"
53
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
54
+ get "/private-place"
55
+ end
49
56
  end
50
57
 
51
- assert_equal "Store needs to respond to #write", raised_exception.message
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
- basic_store_class = Class.new do
56
- def read(key)
57
- end
62
+ raised_exception = nil
58
63
 
59
- def write(key, value)
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
- Rack::Attack.cache.store = basic_store_class.new
70
+ Object.stub_const(:FakeStore, fake_store_class) do
71
+ Rack::Attack.cache.store = FakeStore.new
64
72
 
65
- raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
66
- get "/private-place"
73
+ raised_exception = assert_raises(Rack::Attack::MisconfiguredStoreError) do
74
+ get "/private-place"
75
+ end
67
76
  end
68
77
 
69
- assert_equal "Store needs to respond to #increment", raised_exception.message
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
- basic_store_class = Class.new do
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 = basic_store_class.new
103
+ Rack::Attack.cache.store = FakeStore.new
95
104
 
96
105
  get "/"
97
106
  assert_equal 200, last_response.status