rack-attack 5.4.2 → 6.2.2

Sign up to get free protection for your applications and to get access to all the features.
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe Rack::Attack::StoreProxy::DalliProxy do
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ # ActiveSupport::Subscribers added in ~> 4.0.2.0
6
+ if ActiveSupport::VERSION::MAJOR > 3
7
+ require_relative 'spec_helper'
8
+ require 'active_support/subscriber'
9
+ class CustomSubscriber < ActiveSupport::Subscriber
10
+ @notification_count = 0
11
+
12
+ class << self
13
+ attr_accessor :notification_count
14
+ end
15
+
16
+ def throttle(_event)
17
+ self.class.notification_count += 1
18
+ end
19
+ end
20
+
21
+ describe 'Rack::Attack.instrument' do
22
+ before do
23
+ @period = 60 # Use a long period; failures due to cache key rotation less likely
24
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
25
+ Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
26
+ end
27
+
28
+ describe "with throttling" do
29
+ before do
30
+ ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do
31
+ CustomSubscriber.attach_to("rack_attack")
32
+ 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
33
+ end
34
+ end
35
+
36
+ it 'should instrument without error' do
37
+ _(last_response.status).must_equal 429
38
+ assert_equal 1, CustomSubscriber.notification_count
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe Rack::Attack::PathNormalizer do
4
6
  subject { Rack::Attack::PathNormalizer }
5
7
 
6
8
  it 'should have a normalize_path method' do
7
- subject.normalize_path('/foo').must_equal '/foo'
9
+ _(subject.normalize_path('/foo')).must_equal '/foo'
8
10
  end
9
11
 
10
12
  describe 'FallbackNormalizer' do
11
13
  subject { Rack::Attack::FallbackPathNormalizer }
12
14
 
13
15
  it '#normalize_path does not change the path' do
14
- subject.normalize_path('').must_equal ''
16
+ _(subject.normalize_path('')).must_equal ''
15
17
  end
16
18
  end
17
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack' do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack' do
@@ -10,7 +12,7 @@ describe 'Rack::Attack' do
10
12
 
11
13
  it 'blocks requests with trailing slash' do
12
14
  get '/foo/'
13
- last_response.status.must_equal 403
15
+ _(last_response.status).must_equal 403
14
16
  end
15
17
  end
16
18
 
@@ -20,28 +22,21 @@ describe 'Rack::Attack' do
20
22
  Rack::Attack.blocklist("ip #{@bad_ip}") { |req| req.ip == @bad_ip }
21
23
  end
22
24
 
23
- it('has a blocklist') {
24
- Rack::Attack.blocklists.key?("ip #{@bad_ip}").must_equal true
25
- }
26
-
27
- it('has a blacklist with a deprication warning') {
28
- _, stderror = capture_io do
29
- Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true
30
- end
31
- assert_match "[DEPRECATION] 'Rack::Attack.blacklists' is deprecated. Please use 'blocklists' instead.", stderror
32
- }
25
+ it 'has a blocklist' do
26
+ _(Rack::Attack.blocklists.key?("ip #{@bad_ip}")).must_equal true
27
+ end
33
28
 
34
29
  describe "a bad request" do
35
30
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
36
31
 
37
32
  it "should return a blocklist response" do
38
- last_response.status.must_equal 403
39
- last_response.body.must_equal "Forbidden\n"
33
+ _(last_response.status).must_equal 403
34
+ _(last_response.body).must_equal "Forbidden\n"
40
35
  end
41
36
 
42
37
  it "should tag the env" do
43
- last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
44
- last_request.env['rack.attack.match_type'].must_equal :blocklist
38
+ _(last_request.env['rack.attack.matched']).must_equal "ip #{@bad_ip}"
39
+ _(last_request.env['rack.attack.match_type']).must_equal :blocklist
45
40
  end
46
41
 
47
42
  it_allows_ok_requests
@@ -55,43 +50,52 @@ describe 'Rack::Attack' do
55
50
 
56
51
  it('has a safelist') { Rack::Attack.safelists.key?("good ua") }
57
52
 
58
- it('has a whitelist with a deprication warning') {
59
- _, stderror = capture_io do
60
- Rack::Attack.whitelists.key?("good ua")
61
- end
62
- assert_match "[DEPRECATION] 'Rack::Attack.whitelists' is deprecated. Please use 'safelists' instead.", stderror
63
- }
64
-
65
53
  describe "with a request match both safelist & blocklist" do
66
54
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
67
55
 
68
56
  it "should allow safelists before blocklists" do
69
- last_response.status.must_equal 200
57
+ _(last_response.status).must_equal 200
70
58
  end
71
59
 
72
60
  it "should tag the env" do
73
- last_request.env['rack.attack.matched'].must_equal 'good ua'
74
- last_request.env['rack.attack.match_type'].must_equal :safelist
61
+ _(last_request.env['rack.attack.matched']).must_equal 'good ua'
62
+ _(last_request.env['rack.attack.match_type']).must_equal :safelist
75
63
  end
76
64
  end
77
65
  end
78
66
 
79
67
  describe '#blocklisted_response' do
80
68
  it 'should exist' do
81
- Rack::Attack.blocklisted_response.must_respond_to :call
82
- end
83
-
84
- it 'should give a deprication warning for blacklisted_response' do
85
- _, stderror = capture_io do
86
- Rack::Attack.blacklisted_response
87
- end
88
- assert_match "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead.", stderror
69
+ _(Rack::Attack.blocklisted_response).must_respond_to :call
89
70
  end
90
71
  end
91
72
 
92
73
  describe '#throttled_response' do
93
74
  it 'should exist' do
94
- Rack::Attack.throttled_response.must_respond_to :call
75
+ _(Rack::Attack.throttled_response).must_respond_to :call
76
+ end
77
+ end
78
+ end
79
+
80
+ describe 'enabled' do
81
+ it 'should be enabled by default' do
82
+ _(Rack::Attack.enabled).must_equal true
83
+ end
84
+
85
+ it 'should directly pass request when disabled' do
86
+ bad_ip = '1.2.3.4'
87
+ Rack::Attack.blocklist("ip #{bad_ip}") { |req| req.ip == bad_ip }
88
+
89
+ get '/', {}, 'REMOTE_ADDR' => bad_ip
90
+ _(last_response.status).must_equal 403
91
+
92
+ prev_enabled = Rack::Attack.enabled
93
+ begin
94
+ Rack::Attack.enabled = false
95
+ get '/', {}, 'REMOTE_ADDR' => bad_ip
96
+ _(last_response.status).must_equal 200
97
+ ensure
98
+ Rack::Attack.enabled = prev_enabled
95
99
  end
96
100
  end
97
101
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack.throttle' do
4
6
  before do
5
7
  @period = 60 # Use a long period; failures due to cache key rotation less likely
6
8
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
- Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
9
+ Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
8
10
  end
9
11
 
10
12
  it('should have a throttle') { Rack::Attack.throttles.key?('ip/sec') }
@@ -16,12 +18,19 @@ describe 'Rack::Attack.throttle' do
16
18
 
17
19
  it 'should set the counter for one request' do
18
20
  key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
19
- Rack::Attack.cache.store.read(key).must_equal 1
21
+ _(Rack::Attack.cache.store.read(key)).must_equal 1
20
22
  end
21
23
 
22
24
  it 'should populate throttle data' do
23
- data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
24
- last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
25
+ data = {
26
+ count: 1,
27
+ limit: 1,
28
+ period: @period,
29
+ epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
30
+ discriminator: "1.2.3.4"
31
+ }
32
+
33
+ _(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
25
34
  end
26
35
  end
27
36
 
@@ -31,18 +40,26 @@ describe 'Rack::Attack.throttle' do
31
40
  end
32
41
 
33
42
  it 'should block the last request' do
34
- last_response.status.must_equal 429
43
+ _(last_response.status).must_equal 429
35
44
  end
36
45
 
37
46
  it 'should tag the env' do
38
- last_request.env['rack.attack.matched'].must_equal 'ip/sec'
39
- last_request.env['rack.attack.match_type'].must_equal :throttle
40
- last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i)
41
- last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
47
+ _(last_request.env['rack.attack.matched']).must_equal 'ip/sec'
48
+ _(last_request.env['rack.attack.match_type']).must_equal :throttle
49
+
50
+ _(last_request.env['rack.attack.match_data']).must_equal(
51
+ count: 2,
52
+ limit: 1,
53
+ period: @period,
54
+ epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
55
+ discriminator: "1.2.3.4"
56
+ )
57
+
58
+ _(last_request.env['rack.attack.match_discriminator']).must_equal('1.2.3.4')
42
59
  end
43
60
 
44
61
  it 'should set a Retry-After header' do
45
- last_response.headers['Retry-After'].must_equal @period.to_s
62
+ _(last_response.headers['Retry-After']).must_equal @period.to_s
46
63
  end
47
64
  end
48
65
  end
@@ -51,7 +68,7 @@ describe 'Rack::Attack.throttle with limit as proc' do
51
68
  before do
52
69
  @period = 60 # Use a long period; failures due to cache key rotation less likely
53
70
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
54
- Rack::Attack.throttle('ip/sec', :limit => lambda { |_req| 1 }, :period => @period) { |req| req.ip }
71
+ Rack::Attack.throttle('ip/sec', limit: lambda { |_req| 1 }, period: @period) { |req| req.ip }
55
72
  end
56
73
 
57
74
  it_allows_ok_requests
@@ -61,12 +78,19 @@ describe 'Rack::Attack.throttle with limit as proc' do
61
78
 
62
79
  it 'should set the counter for one request' do
63
80
  key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
64
- Rack::Attack.cache.store.read(key).must_equal 1
81
+ _(Rack::Attack.cache.store.read(key)).must_equal 1
65
82
  end
66
83
 
67
84
  it 'should populate throttle data' do
68
- data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
69
- last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
85
+ data = {
86
+ count: 1,
87
+ limit: 1,
88
+ period: @period,
89
+ epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
90
+ discriminator: "1.2.3.4"
91
+ }
92
+
93
+ _(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
70
94
  end
71
95
  end
72
96
  end
@@ -75,7 +99,7 @@ describe 'Rack::Attack.throttle with period as proc' do
75
99
  before do
76
100
  @period = 60 # Use a long period; failures due to cache key rotation less likely
77
101
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
78
- Rack::Attack.throttle('ip/sec', :limit => lambda { |_req| 1 }, :period => lambda { |_req| @period }) { |req| req.ip }
102
+ Rack::Attack.throttle('ip/sec', limit: lambda { |_req| 1 }, period: lambda { |_req| @period }) { |req| req.ip }
79
103
  end
80
104
 
81
105
  it_allows_ok_requests
@@ -85,12 +109,19 @@ describe 'Rack::Attack.throttle with period as proc' do
85
109
 
86
110
  it 'should set the counter for one request' do
87
111
  key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
88
- Rack::Attack.cache.store.read(key).must_equal 1
112
+ _(Rack::Attack.cache.store.read(key)).must_equal 1
89
113
  end
90
114
 
91
115
  it 'should populate throttle data' do
92
- data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
93
- last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
116
+ data = {
117
+ count: 1,
118
+ limit: 1,
119
+ period: @period,
120
+ epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
121
+ discriminator: "1.2.3.4"
122
+ }
123
+
124
+ _(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
94
125
  end
95
126
  end
96
127
  end
@@ -99,7 +130,7 @@ describe 'Rack::Attack.throttle with block retuning nil' do
99
130
  before do
100
131
  @period = 60
101
132
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
102
- Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |_| nil }
133
+ Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |_| nil }
103
134
  end
104
135
 
105
136
  it_allows_ok_requests
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'spec_helper'
2
4
 
3
5
  describe 'Rack::Attack.track' do
@@ -23,8 +25,9 @@ describe 'Rack::Attack.track' do
23
25
 
24
26
  it "should tag the env" do
25
27
  get '/'
26
- last_request.env['rack.attack.matched'].must_equal 'everything'
27
- last_request.env['rack.attack.match_type'].must_equal :track
28
+
29
+ _(last_request.env['rack.attack.matched']).must_equal 'everything'
30
+ _(last_request.env['rack.attack.match_type']).must_equal :track
28
31
  end
29
32
 
30
33
  describe "with a notification subscriber and two tracks" do
@@ -33,7 +36,7 @@ describe 'Rack::Attack.track' do
33
36
  # A second track
34
37
  Rack::Attack.track("homepage") { |req| req.path == "/" }
35
38
 
36
- ActiveSupport::Notifications.subscribe("rack.attack") do |*_args|
39
+ ActiveSupport::Notifications.subscribe("track.rack_attack") do |*_args|
37
40
  Counter.incr
38
41
  end
39
42
 
@@ -41,21 +44,23 @@ describe 'Rack::Attack.track' do
41
44
  end
42
45
 
43
46
  it "should notify twice" do
44
- Counter.check.must_equal 2
47
+ _(Counter.check).must_equal 2
45
48
  end
46
49
  end
47
50
 
48
51
  describe "without limit and period options" do
49
52
  it "should assign the track filter to a Check instance" do
50
53
  track = Rack::Attack.track("homepage") { |req| req.path == "/" }
51
- track.filter.class.must_equal Rack::Attack::Check
54
+
55
+ _(track.filter.class).must_equal Rack::Attack::Check
52
56
  end
53
57
  end
54
58
 
55
59
  describe "with limit and period options" do
56
60
  it "should assign the track filter to a Throttle instance" do
57
- track = Rack::Attack.track("homepage", :limit => 10, :period => 10) { |req| req.path == "/" }
58
- track.filter.class.must_equal Rack::Attack::Throttle
61
+ track = Rack::Attack.track("homepage", limit: 10, period: 10) { |req| req.path == "/" }
62
+
63
+ _(track.filter.class).must_equal Rack::Attack::Throttle
59
64
  end
60
65
  end
61
66
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/setup"
2
4
 
3
5
  require "minitest/autorun"
4
6
  require "minitest/pride"
5
7
  require "rack/test"
6
- require 'active_support'
7
- require 'action_dispatch'
8
+ require "rails"
8
9
 
9
10
  require "rack/attack"
10
11
 
@@ -13,10 +14,9 @@ if RUBY_ENGINE == "ruby"
13
14
  end
14
15
 
15
16
  def safe_require(name)
16
- begin
17
- require name
18
- rescue LoadError
19
- end
17
+ require name
18
+ rescue LoadError
19
+ nil
20
20
  end
21
21
 
22
22
  safe_require "connection_pool"
@@ -29,6 +29,7 @@ class MiniTest::Spec
29
29
  include Rack::Test::Methods
30
30
 
31
31
  before do
32
+ Rails.cache = nil
32
33
  @_original_throttled_response = Rack::Attack.throttled_response
33
34
  @_original_blocklisted_response = Rack::Attack.blocklisted_response
34
35
  end
@@ -45,6 +46,8 @@ class MiniTest::Spec
45
46
  Rack::Builder.new do
46
47
  # Use Rack::Lint to test that rack-attack is complying with the rack spec
47
48
  use Rack::Lint
49
+ # Intentionally added twice to test idempotence property
50
+ use Rack::Attack
48
51
  use Rack::Attack
49
52
  use Rack::Lint
50
53
 
@@ -55,8 +58,9 @@ class MiniTest::Spec
55
58
  def self.it_allows_ok_requests
56
59
  it "must allow ok requests" do
57
60
  get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
58
- last_response.status.must_equal 200
59
- last_response.body.must_equal 'Hello World'
61
+
62
+ _(last_response.status).must_equal 200
63
+ _(last_response.body).must_equal 'Hello World'
60
64
  end
61
65
  end
62
66
  end