rack-defense 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -2
- data/lib/rack/defense/throttle_counter.rb +4 -5
- data/lib/rack/defense/version.rb +1 -1
- data/spec/defense_ban_spec.rb +2 -2
- data/spec/defense_callbacks_spec.rb +0 -1
- data/spec/defense_throttle_expire_keys_spec.rb +39 -0
- data/spec/defense_throttle_spec.rb +18 -16
- data/spec/throttle_counter_spec.rb +41 -14
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7bada9c179abe1e025f6c215e373df5cb17927c
|
4
|
+
data.tar.gz: 04be29b5ea39a050be5d75fe6e03b1c4383a9681
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3cc00287d01a0a63ae7b9864a27e7266dcb023cf1fe3fc2eef5b9bced4ed203606f0b477f846f5431b5136ee76b934a259e99a39eacebe6f1424386a4d9e71a6
|
7
|
+
data.tar.gz: f957e905c4ed10a0228550d082bc7919bf035081e4775d952151aff170ca6124c289d40ab9db2df97a9d09bbf3340dd3b934ee34d82798fd016a48d3b84d6e82
|
data/README.md
CHANGED
@@ -4,6 +4,7 @@ Rack::Defense
|
|
4
4
|
A Rack middleware for throttling and filtering requests.
|
5
5
|
|
6
6
|
[![Build Status](https://travis-ci.org/Sinbadsoft/rack-defense.svg)](https://travis-ci.org/Sinbadsoft/rack-defense)
|
7
|
+
[![Security](https://hakiri.io/github/Sinbadsoft/rack-defense/master.svg)](https://hakiri.io/github/Sinbadsoft/rack-defense/master)
|
7
8
|
[![Code Climate](https://codeclimate.com/github/Sinbadsoft/rack-defense/badges/gpa.svg)](https://codeclimate.com/github/Sinbadsoft/rack-defense)
|
8
9
|
[![Dependency Status](https://gemnasium.com/Sinbadsoft/rack-defense.svg)](https://gemnasium.com/Sinbadsoft/rack-defense)
|
9
10
|
[![Gem Version](https://badge.fury.io/rb/rack-defense.svg)](http://badge.fury.io/rb/rack-defense)
|
@@ -111,7 +112,7 @@ end
|
|
111
112
|
|
112
113
|
## Filtering
|
113
114
|
|
114
|
-
Rack::Defense can reject requests based on arbitrary properties of the request. Matching requests are filtered.
|
115
|
+
Rack::Defense can reject requests based on arbitrary properties of the request. Matching requests are filtered out.
|
115
116
|
|
116
117
|
### Examples
|
117
118
|
|
@@ -172,7 +173,7 @@ as values is passed.
|
|
172
173
|
```ruby
|
173
174
|
Rack::Defense.setup do |config|
|
174
175
|
config.after_throttle do |req, rules|
|
175
|
-
logger.info rules.map { |e| "rule name: #{e[0]} - rule throttle key: #{e[1]}" }.join ', '
|
176
|
+
logger.info rules.map { |e| "[Throttled] rule name: #{e[0]} - rule throttle key: #{e[1]}" }.join ', '
|
176
177
|
end
|
177
178
|
end
|
178
179
|
```
|
@@ -23,11 +23,10 @@ module Rack
|
|
23
23
|
SCRIPT = <<-LUA_SCRIPT
|
24
24
|
local key = KEYS[1]
|
25
25
|
local timestamp, max_requests, time_period = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
26
|
+
local throttle = (redis.call('rpush', key, timestamp) > max_requests) and
|
27
|
+
(timestamp - time_period) <= tonumber(redis.call('lpop', key))
|
28
|
+
redis.call('pexpire', key, time_period)
|
29
|
+
return throttle
|
31
30
|
LUA_SCRIPT
|
32
31
|
|
33
32
|
private_constant :SCRIPT
|
data/lib/rack/defense/version.rb
CHANGED
data/spec/defense_ban_spec.rb
CHANGED
@@ -8,7 +8,7 @@ describe 'Rack::Defense::ban' do
|
|
8
8
|
Rack::Defense.setup do |config|
|
9
9
|
# allow only given ips on path
|
10
10
|
config.ban('allow_only_ip_list') do |req|
|
11
|
-
req.path == '/protected' &&
|
11
|
+
req.path == '/protected' && !%w(192.168.0.1 127.0.0.1).include?(req.ip)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -27,7 +27,7 @@ describe 'Rack::Defense::ban' do
|
|
27
27
|
|
28
28
|
def check_request(verb, path, ip)
|
29
29
|
send verb, path, {}, 'REMOTE_ADDR' => ip
|
30
|
-
expected_status = path == '/protected' &&
|
30
|
+
expected_status = path == '/protected' && !%w(192.168.0.1 127.0.0.1).include?(ip) ?
|
31
31
|
status_banned : status_ok
|
32
32
|
assert_equal expected_status, last_response.status
|
33
33
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rack::Defense::throttle_expire_keys' do
|
4
|
+
def window
|
5
|
+
10 * 1000 # in milliseconds
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
Rack::Defense.setup do |config|
|
10
|
+
# allow 1 requests per #window per ip
|
11
|
+
config.throttle('rule', 3, window) { |req| req.ip if req.path == '/path' }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'expire throttle key' do
|
16
|
+
ip = '192.168.169.244'
|
17
|
+
throttle_key = "#{Rack::Defense::ThrottleCounter::KEY_PREFIX}:rule:#{ip}"
|
18
|
+
redis = Rack::Defense.config.store
|
19
|
+
start = Time.now.to_i
|
20
|
+
3.times do
|
21
|
+
get '/path', {}, 'REMOTE_ADDR' => ip
|
22
|
+
assert status_ok, last_response.status
|
23
|
+
end
|
24
|
+
|
25
|
+
get '/path', {}, 'REMOTE_ADDR' => ip
|
26
|
+
elapsed = Time.now.to_i - start
|
27
|
+
if elapsed < window
|
28
|
+
assert status_throttled, last_response.status
|
29
|
+
assert redis.exists throttle_key
|
30
|
+
else
|
31
|
+
puts "Warning: test too slow elapsed:#{elapsed}s expected < #{window}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Since Redis 2.6 the expire error is from 0 to 1 milliseconds. See http://redis.io/commands/expire
|
35
|
+
sleep (window / 1000) + 0.002
|
36
|
+
|
37
|
+
refute redis.exists throttle_key
|
38
|
+
end
|
39
|
+
end
|
@@ -1,28 +1,30 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
2
|
|
3
3
|
describe 'Rack::Defense::throttle' do
|
4
|
-
|
4
|
+
def window
|
5
|
+
60 * 1000 # in milliseconds
|
6
|
+
end
|
5
7
|
|
6
8
|
before do
|
7
9
|
@start_time = Time.utc(2015, 10, 30, 21, 0, 0)
|
8
10
|
|
9
11
|
#
|
10
|
-
# configure the Rack::Defense middleware with
|
12
|
+
# configure the Rack::Defense middleware with throttling
|
11
13
|
# strategies.
|
12
14
|
#
|
13
15
|
Rack::Defense.setup do |config|
|
14
|
-
# allow only 3 post requests on path '/login' per
|
15
|
-
config.throttle('login', 3,
|
16
|
+
# allow only 3 post requests on path '/login' per #window per ip
|
17
|
+
config.throttle('login', 3, window) do |req|
|
16
18
|
req.ip if req.path == '/login' && req.post?
|
17
19
|
end
|
18
20
|
|
19
|
-
# allow only
|
20
|
-
config.throttle('res', 30,
|
21
|
+
# allow only 30 get requests on path '/search' per #window per ip
|
22
|
+
config.throttle('res', 30, window) do |req|
|
21
23
|
req.ip if req.path == '/search' && req.get?
|
22
24
|
end
|
23
25
|
|
24
|
-
# allow only 5 get requests on path /api/* per
|
25
|
-
config.throttle('api', 5,
|
26
|
+
# allow only 5 get requests on path /api/* per #window per authorization token
|
27
|
+
config.throttle('api', 5, window) do |req|
|
26
28
|
req.env['HTTP_AUTHORIZATION'] if %r{^/api/} =~ req.path
|
27
29
|
end
|
28
30
|
end
|
@@ -35,27 +37,27 @@ describe 'Rack::Defense::throttle' do
|
|
35
37
|
end
|
36
38
|
it 'ban get requests higher than acceptable rate' do
|
37
39
|
10.times do |period|
|
38
|
-
50.times { |offset| check_get_request(offset + period*
|
40
|
+
50.times { |offset| check_get_request(offset + period*window) }
|
39
41
|
end
|
40
42
|
end
|
41
43
|
it 'ban post requests higher than acceptable rate' do
|
42
44
|
10.times do |period|
|
43
|
-
7.times { |offset| check_post_request(offset + period*
|
45
|
+
7.times { |offset| check_post_request(offset + period*window) }
|
44
46
|
end
|
45
47
|
end
|
46
|
-
it 'not have side effects between
|
48
|
+
it 'not have side effects between different throttle rules with mixed requests' do
|
47
49
|
10.times do |period|
|
48
50
|
50.times do |offset|
|
49
|
-
check_get_request(offset + period*
|
50
|
-
check_post_request(offset + period*
|
51
|
+
check_get_request(offset + period*window)
|
52
|
+
check_post_request(offset + period*window)
|
51
53
|
end
|
52
54
|
end
|
53
55
|
end
|
54
56
|
it 'not have side effects between request filtered by the same rule but with different keys' do
|
55
57
|
10.times do |period|
|
56
58
|
50.times do |offset|
|
57
|
-
check_get_request(offset + period*
|
58
|
-
check_get_request(offset + period*
|
59
|
+
check_get_request(offset + period*window, ip='192.168.0.1')
|
60
|
+
check_get_request(offset + period*window, ip='192.168.0.2')
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
@@ -111,7 +113,7 @@ describe 'Rack::Defense::throttle' do
|
|
111
113
|
def check_request(verb, path, time_offset, max_requests, ip, headers={})
|
112
114
|
Timecop.freeze(@start_time + time_offset) do
|
113
115
|
send verb, path, {}, headers.merge('REMOTE_ADDR' => ip)
|
114
|
-
expected_status = (time_offset %
|
116
|
+
expected_status = (time_offset % window) >= max_requests ? status_throttled : status_ok
|
115
117
|
assert_equal expected_status, last_response.status, "offset #{time_offset}"
|
116
118
|
end
|
117
119
|
end
|
@@ -2,41 +2,68 @@ require_relative 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Rack::Defense::ThrottleCounter do
|
4
4
|
before do
|
5
|
-
@counter = Rack::Defense::ThrottleCounter.new('upload_photo', 5, 10, Redis.current)
|
6
5
|
@key = '192.168.0.1'
|
7
6
|
end
|
8
|
-
|
9
7
|
describe '.throttle?' do
|
8
|
+
window = 60 * 1000
|
9
|
+
before { @counter = Rack::Defense::ThrottleCounter.new('upload_photo', 5, window, Redis.current) }
|
10
10
|
it 'allow request number max_requests if after period' do
|
11
11
|
do_max_requests_minus_one
|
12
|
-
refute @counter.throttle? @key,
|
12
|
+
refute @counter.throttle? @key, window + 1
|
13
13
|
end
|
14
14
|
it 'block request number max_requests if in period' do
|
15
15
|
do_max_requests_minus_one
|
16
|
-
assert @counter.throttle? @key,
|
16
|
+
assert @counter.throttle? @key, window
|
17
17
|
end
|
18
18
|
it 'allow consecutive valid periods' do
|
19
|
-
(0..20).each { |i| do_max_requests_minus_one(
|
19
|
+
(0..20).each { |i| do_max_requests_minus_one((window + 1) * i) }
|
20
20
|
end
|
21
21
|
it 'block consecutive invalid requests' do
|
22
22
|
do_max_requests_minus_one
|
23
|
-
(0..20).each { |i| assert @counter.throttle?(@key,
|
23
|
+
(0..20).each { |i| assert @counter.throttle?(@key, window + i) }
|
24
24
|
end
|
25
25
|
it 'use a sliding window and not reset count after each full period' do
|
26
|
-
[5,
|
27
|
-
[
|
26
|
+
[5, 4, 3, 2, 1].map { |e| window - e }.each { |t| refute @counter.throttle?(@key, t), "timestamp #{t}" }
|
27
|
+
[1, 2, 3, 4].map { |e| window + e }.each { |t| assert @counter.throttle?(@key, t), "timestamp #{t}"}
|
28
28
|
end
|
29
29
|
it 'should unblock after blocking requests' do
|
30
30
|
do_max_requests_minus_one
|
31
|
-
assert @counter.throttle? @key,
|
32
|
-
assert @counter.throttle? @key,
|
33
|
-
refute @counter.throttle? @key,
|
31
|
+
assert @counter.throttle? @key, window
|
32
|
+
assert @counter.throttle? @key, window + 1
|
33
|
+
refute @counter.throttle? @key, window + 6
|
34
34
|
end
|
35
35
|
it 'should include throttled(blocked) request into the request count' do
|
36
36
|
[0, 1, 2, 3, 4].each { |t| refute @counter.throttle?(@key, t), "timestamp #{t}" }
|
37
|
-
assert @counter.throttle? @key,
|
38
|
-
[16, 17, 18, 19].each { |t| refute @counter.throttle?(@key, t), "timestamp #{t}" }
|
39
|
-
assert @counter.throttle? @key, 20
|
37
|
+
assert @counter.throttle? @key, window
|
38
|
+
[16, 17, 18, 19].map { |e| window + e }.each { |t| refute @counter.throttle?(@key, t), "timestamp #{t}" }
|
39
|
+
assert @counter.throttle? @key, window + 20
|
40
|
+
end
|
41
|
+
end
|
42
|
+
describe 'expire keys' do
|
43
|
+
before do
|
44
|
+
@redis = Redis.current
|
45
|
+
@counter = Rack::Defense::ThrottleCounter.new('rule_name', 3, 10 * 1000, @redis)
|
46
|
+
@throttle_key = "#{Rack::Defense::ThrottleCounter::KEY_PREFIX}:rule_name:#{@key}"
|
47
|
+
end
|
48
|
+
it 'expire throttle key' do
|
49
|
+
start = Time.now.to_i
|
50
|
+
|
51
|
+
3.times do
|
52
|
+
refute @counter.throttle? @key
|
53
|
+
end
|
54
|
+
|
55
|
+
elapsed = Time.now.to_i - start
|
56
|
+
if elapsed < 10
|
57
|
+
assert @counter.throttle? @key
|
58
|
+
assert @redis.exists @throttle_key
|
59
|
+
else
|
60
|
+
puts "Warning: test too slow elapsed:#{elapsed}s expected < #{10}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Since Redis 2.6 the expire error is from 0 to 1 milliseconds. See http://redis.io/commands/expire
|
64
|
+
sleep 10 + 0.002
|
65
|
+
|
66
|
+
refute @redis.exists @throttle_key
|
40
67
|
end
|
41
68
|
end
|
42
69
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-defense
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chaker Nakhli
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -109,6 +109,7 @@ files:
|
|
109
109
|
- spec/defense_ban_spec.rb
|
110
110
|
- spec/defense_callbacks_spec.rb
|
111
111
|
- spec/defense_config_spec.rb
|
112
|
+
- spec/defense_throttle_expire_keys_spec.rb
|
112
113
|
- spec/defense_throttle_spec.rb
|
113
114
|
- spec/spec_helper.rb
|
114
115
|
- spec/throttle_counter_spec.rb
|
@@ -141,6 +142,7 @@ test_files:
|
|
141
142
|
- spec/defense_ban_spec.rb
|
142
143
|
- spec/defense_callbacks_spec.rb
|
143
144
|
- spec/defense_config_spec.rb
|
145
|
+
- spec/defense_throttle_expire_keys_spec.rb
|
144
146
|
- spec/defense_throttle_spec.rb
|
145
147
|
- spec/spec_helper.rb
|
146
148
|
- spec/throttle_counter_spec.rb
|