rack-attack 6.1.0 → 6.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,17 +15,33 @@ module Rack
15
15
  #
16
16
  # So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
17
17
  # the counter. After that we continue using the original RedisCacheStore#increment.
18
- if options[:expires_in] && !read(name)
19
- write(name, amount, options)
18
+ rescuing do
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
 
29
+ def read(*_args)
30
+ rescuing { super }
31
+ end
32
+
27
33
  def write(name, value, options = {})
28
- super(name, value, options.merge!(raw: true))
34
+ rescuing do
35
+ super(name, value, options.merge!(raw: true))
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def rescuing
42
+ yield
43
+ rescue Redis::BaseError
44
+ nil
29
45
  end
30
46
  end
31
47
  end
@@ -7,11 +7,12 @@ module Rack
7
7
 
8
8
  attr_reader :name, :limit, :period, :block, :type
9
9
  def initialize(name, options, &block)
10
- @name, @block = name, block
10
+ @name = name
11
+ @block = block
11
12
  MANDATORY_OPTIONS.each do |opt|
12
13
  raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
13
14
  end
14
- @limit = options[:limit]
15
+ @limit = options[:limit]
15
16
  @period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i
16
17
  @type = options.fetch(:type, :throttle)
17
18
  end
@@ -8,11 +8,12 @@ module Rack
8
8
  def initialize(name, options = {}, &block)
9
9
  options[:type] = :track
10
10
 
11
- if options[:limit] && options[:period]
12
- @filter = Throttle.new(name, options, &block)
13
- else
14
- @filter = Check.new(name, options, &block)
15
- 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
16
17
  end
17
18
 
18
19
  def matched_by?(request)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class Attack
5
- VERSION = '6.1.0'
5
+ VERSION = '6.2.0'
6
6
  end
7
7
  end
@@ -0,0 +1,41 @@
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 added explicitly" do
22
+ @app.config.middleware.use(Rack::Attack)
23
+ @app.initialize!
24
+ assert_equal 1, @app.middleware.count(Rack::Attack)
25
+ end
26
+
27
+ it "is not added when it was explicitly deleted" do
28
+ @app.config.middleware.delete(Rack::Attack)
29
+ @app.initialize!
30
+ refute @app.middleware.include?(Rack::Attack)
31
+ end
32
+ end
33
+
34
+ if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
35
+ it "is not used by default" do
36
+ @app.initialize!
37
+ assert_equal 0, @app.middleware.count(Rack::Attack)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -15,8 +15,6 @@ if defined?(::ConnectionPool) && defined?(::Dalli)
15
15
  Rack::Attack.cache.store.clear
16
16
  end
17
17
 
18
- it_works_for_cache_backed_features(fetch_from_store: ->(key) {
19
- Rack::Attack.cache.store.read(key)
20
- })
18
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
21
19
  end
22
20
  end
@@ -2,7 +2,13 @@
2
2
 
3
3
  require_relative "../../spec_helper"
4
4
 
5
- if defined?(::ConnectionPool) && defined?(::Redis) && Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") && defined?(::ActiveSupport::Cache::RedisCacheStore)
5
+ should_run =
6
+ defined?(::ConnectionPool) &&
7
+ defined?(::Redis) &&
8
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
9
+ defined?(::ActiveSupport::Cache::RedisCacheStore)
10
+
11
+ if should_run
6
12
  require_relative "../../support/cache_store_helper"
7
13
  require "timecop"
8
14
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  require_relative "../../spec_helper"
4
4
 
5
- if defined?(::Redis) && Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") && defined?(::ActiveSupport::Cache::RedisCacheStore)
5
+ should_run =
6
+ defined?(::Redis) &&
7
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("4") &&
8
+ defined?(::ActiveSupport::Cache::RedisCacheStore)
9
+
10
+ if should_run
6
11
  require_relative "../../support/cache_store_helper"
7
12
  require "timecop"
8
13
 
@@ -17,8 +17,8 @@ if defined?(::Dalli) && defined?(::ConnectionPool)
17
17
  Rack::Attack.cache.store.with { |client| client.flush_all }
18
18
  end
19
19
 
20
- it_works_for_cache_backed_features(fetch_from_store: ->(key) {
21
- Rack::Attack.cache.store.with { |client| client.fetch(key) }
22
- })
20
+ it_works_for_cache_backed_features(
21
+ fetch_from_store: ->(key) { Rack::Attack.cache.store.with { |client| client.fetch(key) } }
22
+ )
23
23
  end
24
24
  end
@@ -20,7 +20,8 @@ describe 'Rack::Attack.Allow2Ban' do
20
20
  describe 'making ok request' do
21
21
  it 'succeeds' do
22
22
  get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
23
- last_response.status.must_equal 200
23
+
24
+ _(last_response.status).must_equal 200
24
25
  end
25
26
  end
26
27
 
@@ -29,17 +30,18 @@ describe 'Rack::Attack.Allow2Ban' do
29
30
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
30
31
 
31
32
  it 'succeeds' do
32
- last_response.status.must_equal 200
33
+ _(last_response.status).must_equal 200
33
34
  end
34
35
 
35
36
  it 'increases fail count' do
36
37
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
37
- @cache.store.read(key).must_equal 1
38
+
39
+ _(@cache.store.read(key)).must_equal 1
38
40
  end
39
41
 
40
42
  it 'is not banned' do
41
43
  key = "rack::attack:allow2ban:1.2.3.4"
42
- @cache.store.read(key).must_be_nil
44
+ _(@cache.store.read(key)).must_be_nil
43
45
  end
44
46
  end
45
47
 
@@ -51,17 +53,17 @@ describe 'Rack::Attack.Allow2Ban' do
51
53
  end
52
54
 
53
55
  it 'succeeds' do
54
- last_response.status.must_equal 200
56
+ _(last_response.status).must_equal 200
55
57
  end
56
58
 
57
59
  it 'increases fail count' do
58
60
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
59
- @cache.store.read(key).must_equal 2
61
+ _(@cache.store.read(key)).must_equal 2
60
62
  end
61
63
 
62
64
  it 'is banned' do
63
65
  key = "rack::attack:allow2ban:ban:1.2.3.4"
64
- @cache.store.read(key).must_equal 1
66
+ _(@cache.store.read(key)).must_equal 1
65
67
  end
66
68
  end
67
69
  end
@@ -77,7 +79,8 @@ describe 'Rack::Attack.Allow2Ban' do
77
79
  describe 'making request for other discriminator' do
78
80
  it 'succeeds' do
79
81
  get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
80
- last_response.status.must_equal 200
82
+
83
+ _(last_response.status).must_equal 200
81
84
  end
82
85
  end
83
86
 
@@ -87,17 +90,17 @@ describe 'Rack::Attack.Allow2Ban' do
87
90
  end
88
91
 
89
92
  it 'fails' do
90
- last_response.status.must_equal 403
93
+ _(last_response.status).must_equal 403
91
94
  end
92
95
 
93
96
  it 'does not increase fail count' do
94
97
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
95
- @cache.store.read(key).must_equal 2
98
+ _(@cache.store.read(key)).must_equal 2
96
99
  end
97
100
 
98
101
  it 'is still banned' do
99
102
  key = "rack::attack:allow2ban:ban:1.2.3.4"
100
- @cache.store.read(key).must_equal 1
103
+ _(@cache.store.read(key)).must_equal 1
101
104
  end
102
105
  end
103
106
 
@@ -107,17 +110,17 @@ describe 'Rack::Attack.Allow2Ban' do
107
110
  end
108
111
 
109
112
  it 'fails' do
110
- last_response.status.must_equal 403
113
+ _(last_response.status).must_equal 403
111
114
  end
112
115
 
113
116
  it 'does not increase fail count' do
114
117
  key = "rack::attack:#{Time.now.to_i / @findtime}:allow2ban:count:1.2.3.4"
115
- @cache.store.read(key).must_equal 2
118
+ _(@cache.store.read(key)).must_equal 2
116
119
  end
117
120
 
118
121
  it 'is still banned' do
119
122
  key = "rack::attack:allow2ban:ban:1.2.3.4"
120
- @cache.store.read(key).must_equal 1
123
+ _(@cache.store.read(key)).must_equal 1
121
124
  end
122
125
  end
123
126
  end
@@ -20,7 +20,7 @@ describe 'Rack::Attack.Fail2Ban' do
20
20
  describe 'making ok request' do
21
21
  it 'succeeds' do
22
22
  get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
23
- last_response.status.must_equal 200
23
+ _(last_response.status).must_equal 200
24
24
  end
25
25
  end
26
26
 
@@ -29,17 +29,17 @@ describe 'Rack::Attack.Fail2Ban' do
29
29
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
30
30
 
31
31
  it 'fails' do
32
- last_response.status.must_equal 403
32
+ _(last_response.status).must_equal 403
33
33
  end
34
34
 
35
35
  it 'increases fail count' do
36
36
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
37
- @cache.store.read(key).must_equal 1
37
+ _(@cache.store.read(key)).must_equal 1
38
38
  end
39
39
 
40
40
  it 'is not banned' do
41
41
  key = "rack::attack:fail2ban:1.2.3.4"
42
- @cache.store.read(key).must_be_nil
42
+ _(@cache.store.read(key)).must_be_nil
43
43
  end
44
44
  end
45
45
 
@@ -51,17 +51,17 @@ describe 'Rack::Attack.Fail2Ban' do
51
51
  end
52
52
 
53
53
  it 'fails' do
54
- last_response.status.must_equal 403
54
+ _(last_response.status).must_equal 403
55
55
  end
56
56
 
57
57
  it 'increases fail count' do
58
58
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
59
- @cache.store.read(key).must_equal 2
59
+ _(@cache.store.read(key)).must_equal 2
60
60
  end
61
61
 
62
62
  it 'is banned' do
63
63
  key = "rack::attack:fail2ban:ban:1.2.3.4"
64
- @cache.store.read(key).must_equal 1
64
+ _(@cache.store.read(key)).must_equal 1
65
65
  end
66
66
  end
67
67
 
@@ -73,7 +73,7 @@ describe 'Rack::Attack.Fail2Ban' do
73
73
  end
74
74
 
75
75
  it 'succeeds' do
76
- last_response.status.must_equal 200
76
+ _(last_response.status).must_equal 200
77
77
  end
78
78
 
79
79
  it 'resets fail count' do
@@ -82,7 +82,7 @@ describe 'Rack::Attack.Fail2Ban' do
82
82
  end
83
83
 
84
84
  it 'IP is not banned' do
85
- Rack::Attack::Fail2Ban.banned?('1.2.3.4').must_equal false
85
+ _(Rack::Attack::Fail2Ban.banned?('1.2.3.4')).must_equal false
86
86
  end
87
87
  end
88
88
  end
@@ -98,7 +98,8 @@ describe 'Rack::Attack.Fail2Ban' do
98
98
  describe 'making request for other discriminator' do
99
99
  it 'succeeds' do
100
100
  get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
101
- last_response.status.must_equal 200
101
+
102
+ _(last_response.status).must_equal 200
102
103
  end
103
104
  end
104
105
 
@@ -108,17 +109,17 @@ describe 'Rack::Attack.Fail2Ban' do
108
109
  end
109
110
 
110
111
  it 'fails' do
111
- last_response.status.must_equal 403
112
+ _(last_response.status).must_equal 403
112
113
  end
113
114
 
114
115
  it 'does not increase fail count' do
115
116
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
116
- @cache.store.read(key).must_equal 2
117
+ _(@cache.store.read(key)).must_equal 2
117
118
  end
118
119
 
119
120
  it 'is still banned' do
120
121
  key = "rack::attack:fail2ban:ban:1.2.3.4"
121
- @cache.store.read(key).must_equal 1
122
+ _(@cache.store.read(key)).must_equal 1
122
123
  end
123
124
  end
124
125
 
@@ -128,17 +129,17 @@ describe 'Rack::Attack.Fail2Ban' do
128
129
  end
129
130
 
130
131
  it 'fails' do
131
- last_response.status.must_equal 403
132
+ _(last_response.status).must_equal 403
132
133
  end
133
134
 
134
135
  it 'does not increase fail count' do
135
136
  key = "rack::attack:#{Time.now.to_i / @findtime}:fail2ban:count:1.2.3.4"
136
- @cache.store.read(key).must_equal 2
137
+ _(@cache.store.read(key)).must_equal 2
137
138
  end
138
139
 
139
140
  it 'is still banned' do
140
141
  key = "rack::attack:fail2ban:ban:1.2.3.4"
141
- @cache.store.read(key).must_equal 1
142
+ _(@cache.store.read(key)).must_equal 1
142
143
  end
143
144
  end
144
145
  end
@@ -34,7 +34,7 @@ if ActiveSupport::VERSION::MAJOR > 3
34
34
  end
35
35
 
36
36
  it 'should instrument without error' do
37
- last_response.status.must_equal 429
37
+ _(last_response.status).must_equal 429
38
38
  assert_equal 1, CustomSubscriber.notification_count
39
39
  end
40
40
  end
@@ -6,14 +6,14 @@ describe Rack::Attack::PathNormalizer do
6
6
  subject { Rack::Attack::PathNormalizer }
7
7
 
8
8
  it 'should have a normalize_path method' do
9
- subject.normalize_path('/foo').must_equal '/foo'
9
+ _(subject.normalize_path('/foo')).must_equal '/foo'
10
10
  end
11
11
 
12
12
  describe 'FallbackNormalizer' do
13
13
  subject { Rack::Attack::FallbackPathNormalizer }
14
14
 
15
15
  it '#normalize_path does not change the path' do
16
- subject.normalize_path('').must_equal ''
16
+ _(subject.normalize_path('')).must_equal ''
17
17
  end
18
18
  end
19
19
  end
@@ -12,7 +12,7 @@ describe 'Rack::Attack' do
12
12
 
13
13
  it 'blocks requests with trailing slash' do
14
14
  get '/foo/'
15
- last_response.status.must_equal 403
15
+ _(last_response.status).must_equal 403
16
16
  end
17
17
  end
18
18
 
@@ -22,21 +22,21 @@ describe 'Rack::Attack' do
22
22
  Rack::Attack.blocklist("ip #{@bad_ip}") { |req| req.ip == @bad_ip }
23
23
  end
24
24
 
25
- it('has a blocklist') {
26
- Rack::Attack.blocklists.key?("ip #{@bad_ip}").must_equal true
27
- }
25
+ it 'has a blocklist' do
26
+ _(Rack::Attack.blocklists.key?("ip #{@bad_ip}")).must_equal true
27
+ end
28
28
 
29
29
  describe "a bad request" do
30
30
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
31
31
 
32
32
  it "should return a blocklist response" do
33
- last_response.status.must_equal 403
34
- last_response.body.must_equal "Forbidden\n"
33
+ _(last_response.status).must_equal 403
34
+ _(last_response.body).must_equal "Forbidden\n"
35
35
  end
36
36
 
37
37
  it "should tag the env" do
38
- last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
39
- 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
40
40
  end
41
41
 
42
42
  it_allows_ok_requests
@@ -54,25 +54,48 @@ describe 'Rack::Attack' do
54
54
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
55
55
 
56
56
  it "should allow safelists before blocklists" do
57
- last_response.status.must_equal 200
57
+ _(last_response.status).must_equal 200
58
58
  end
59
59
 
60
60
  it "should tag the env" do
61
- last_request.env['rack.attack.matched'].must_equal 'good ua'
62
- 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
63
63
  end
64
64
  end
65
65
  end
66
66
 
67
67
  describe '#blocklisted_response' do
68
68
  it 'should exist' do
69
- Rack::Attack.blocklisted_response.must_respond_to :call
69
+ _(Rack::Attack.blocklisted_response).must_respond_to :call
70
70
  end
71
71
  end
72
72
 
73
73
  describe '#throttled_response' do
74
74
  it 'should exist' do
75
- 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
76
99
  end
77
100
  end
78
101
  end