rack-attack 6.0.0 → 6.3.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.
- checksums.yaml +4 -4
- data/README.md +19 -5
- data/lib/rack/attack.rb +97 -146
- data/lib/rack/attack/cache.rb +15 -1
- data/lib/rack/attack/check.rb +2 -1
- data/lib/rack/attack/configuration.rb +107 -0
- data/lib/rack/attack/path_normalizer.rb +20 -18
- data/lib/rack/attack/railtie.rb +13 -0
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +3 -1
- data/lib/rack/attack/store_proxy/redis_proxy.rb +16 -7
- data/lib/rack/attack/throttle.rb +32 -14
- data/lib/rack/attack/track.rb +6 -5
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/rails_middleware_spec.rb +35 -0
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +1 -3
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +7 -1
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +6 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +3 -3
- data/spec/acceptance/throttling_spec.rb +19 -1
- data/spec/allow2ban_spec.rb +17 -14
- data/spec/fail2ban_spec.rb +17 -16
- data/spec/integration/offline_spec.rb +46 -1
- data/spec/rack_attack_instrumentation_spec.rb +1 -1
- data/spec/rack_attack_path_normalizer_spec.rb +2 -2
- data/spec/rack_attack_spec.rb +58 -13
- data/spec/rack_attack_throttle_spec.rb +43 -18
- data/spec/rack_attack_track_spec.rb +8 -5
- data/spec/spec_helper.rb +7 -9
- metadata +31 -21
data/spec/fail2ban_spec.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -13,7 +13,11 @@ OfflineExamples = Minitest::SharedExamples.new do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'should count' do
|
16
|
-
@cache.
|
16
|
+
@cache.count('cache-test-key', 1)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should delete' do
|
20
|
+
@cache.delete('cache-test-key')
|
17
21
|
end
|
18
22
|
end
|
19
23
|
|
@@ -29,6 +33,18 @@ if defined?(::ActiveSupport::Cache::RedisStore)
|
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
36
|
+
if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
|
37
|
+
describe 'when Redis is offline' do
|
38
|
+
include OfflineExamples
|
39
|
+
|
40
|
+
before do
|
41
|
+
@cache = Rack::Attack::Cache.new
|
42
|
+
# Use presumably unused port for Redis client
|
43
|
+
@cache.store = ActiveSupport::Cache::RedisCacheStore.new(host: '127.0.0.1', port: 3333)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
32
48
|
if defined?(::Dalli)
|
33
49
|
describe 'when Memcached is offline' do
|
34
50
|
include OfflineExamples
|
@@ -45,3 +61,32 @@ if defined?(::Dalli)
|
|
45
61
|
end
|
46
62
|
end
|
47
63
|
end
|
64
|
+
|
65
|
+
if defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore)
|
66
|
+
describe 'when Memcached is offline' do
|
67
|
+
include OfflineExamples
|
68
|
+
|
69
|
+
before do
|
70
|
+
Dalli.logger.level = Logger::FATAL
|
71
|
+
|
72
|
+
@cache = Rack::Attack::Cache.new
|
73
|
+
@cache.store = ActiveSupport::Cache::MemCacheStore.new('127.0.0.1:22122')
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
Dalli.logger.level = Logger::INFO
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if defined?(Redis)
|
83
|
+
describe 'when Redis is offline' do
|
84
|
+
include OfflineExamples
|
85
|
+
|
86
|
+
before do
|
87
|
+
@cache = Rack::Attack::Cache.new
|
88
|
+
# Use presumably unused port for Redis client
|
89
|
+
@cache.store = Redis.new(host: '127.0.0.1', port: 3333)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
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
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -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
|
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,70 @@ 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
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'reset!' do
|
104
|
+
it 'raises an error when is not supported by cache store' do
|
105
|
+
Rack::Attack.cache.store = Class.new
|
106
|
+
assert_raises(Rack::Attack::IncompatibleStoreError) do
|
107
|
+
Rack::Attack.reset!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if defined?(Redis)
|
112
|
+
it 'should delete rack attack keys' do
|
113
|
+
redis = Redis.new
|
114
|
+
redis.set('key', 'value')
|
115
|
+
redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
|
116
|
+
Rack::Attack.cache.store = redis
|
117
|
+
Rack::Attack.reset!
|
118
|
+
|
119
|
+
_(redis.get('key')).must_equal 'value'
|
120
|
+
_(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
76
121
|
end
|
77
122
|
end
|
78
123
|
end
|
@@ -18,12 +18,19 @@ describe 'Rack::Attack.throttle' do
|
|
18
18
|
|
19
19
|
it 'should set the counter for one request' do
|
20
20
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
21
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
21
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'should populate throttle data' do
|
25
|
-
data = {
|
26
|
-
|
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
|
27
34
|
end
|
28
35
|
end
|
29
36
|
|
@@ -33,18 +40,22 @@ describe 'Rack::Attack.throttle' do
|
|
33
40
|
end
|
34
41
|
|
35
42
|
it 'should block the last request' do
|
36
|
-
last_response.status.must_equal 429
|
43
|
+
_(last_response.status).must_equal 429
|
37
44
|
end
|
38
45
|
|
39
46
|
it 'should tag the env' do
|
40
|
-
last_request.env['rack.attack.matched'].must_equal 'ip/sec'
|
41
|
-
last_request.env['rack.attack.match_type'].must_equal :throttle
|
42
|
-
|
43
|
-
last_request.env['rack.attack.
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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')
|
48
59
|
end
|
49
60
|
end
|
50
61
|
end
|
@@ -63,12 +74,19 @@ describe 'Rack::Attack.throttle with limit as proc' do
|
|
63
74
|
|
64
75
|
it 'should set the counter for one request' do
|
65
76
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
66
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
77
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
67
78
|
end
|
68
79
|
|
69
80
|
it 'should populate throttle data' do
|
70
|
-
data = {
|
71
|
-
|
81
|
+
data = {
|
82
|
+
count: 1,
|
83
|
+
limit: 1,
|
84
|
+
period: @period,
|
85
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
86
|
+
discriminator: "1.2.3.4"
|
87
|
+
}
|
88
|
+
|
89
|
+
_(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
|
72
90
|
end
|
73
91
|
end
|
74
92
|
end
|
@@ -87,12 +105,19 @@ describe 'Rack::Attack.throttle with period as proc' do
|
|
87
105
|
|
88
106
|
it 'should set the counter for one request' do
|
89
107
|
key = "rack::attack:#{Time.now.to_i / @period}:ip/sec:1.2.3.4"
|
90
|
-
Rack::Attack.cache.store.read(key).must_equal 1
|
108
|
+
_(Rack::Attack.cache.store.read(key)).must_equal 1
|
91
109
|
end
|
92
110
|
|
93
111
|
it 'should populate throttle data' do
|
94
|
-
data = {
|
95
|
-
|
112
|
+
data = {
|
113
|
+
count: 1,
|
114
|
+
limit: 1,
|
115
|
+
period: @period,
|
116
|
+
epoch_time: Rack::Attack.cache.last_epoch_time.to_i,
|
117
|
+
discriminator: "1.2.3.4"
|
118
|
+
}
|
119
|
+
|
120
|
+
_(last_request.env['rack.attack.throttle_data']['ip/sec']).must_equal data
|
96
121
|
end
|
97
122
|
end
|
98
123
|
end
|
@@ -25,8 +25,9 @@ describe 'Rack::Attack.track' do
|
|
25
25
|
|
26
26
|
it "should tag the env" do
|
27
27
|
get '/'
|
28
|
-
|
29
|
-
last_request.env['rack.attack.
|
28
|
+
|
29
|
+
_(last_request.env['rack.attack.matched']).must_equal 'everything'
|
30
|
+
_(last_request.env['rack.attack.match_type']).must_equal :track
|
30
31
|
end
|
31
32
|
|
32
33
|
describe "with a notification subscriber and two tracks" do
|
@@ -43,21 +44,23 @@ describe 'Rack::Attack.track' do
|
|
43
44
|
end
|
44
45
|
|
45
46
|
it "should notify twice" do
|
46
|
-
Counter.check.must_equal 2
|
47
|
+
_(Counter.check).must_equal 2
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
51
|
describe "without limit and period options" do
|
51
52
|
it "should assign the track filter to a Check instance" do
|
52
53
|
track = Rack::Attack.track("homepage") { |req| req.path == "/" }
|
53
|
-
|
54
|
+
|
55
|
+
_(track.filter.class).must_equal Rack::Attack::Check
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
57
59
|
describe "with limit and period options" do
|
58
60
|
it "should assign the track filter to a Throttle instance" do
|
59
61
|
track = Rack::Attack.track("homepage", limit: 10, period: 10) { |req| req.path == "/" }
|
60
|
-
|
62
|
+
|
63
|
+
_(track.filter.class).must_equal Rack::Attack::Throttle
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|