rack-defense 0.2.0 → 0.2.1
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 +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
|
[](https://travis-ci.org/Sinbadsoft/rack-defense)
|
7
|
+
[](https://hakiri.io/github/Sinbadsoft/rack-defense/master)
|
7
8
|
[](https://codeclimate.com/github/Sinbadsoft/rack-defense)
|
8
9
|
[](https://gemnasium.com/Sinbadsoft/rack-defense)
|
9
10
|
[](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
|