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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0c50757c35cc2da91b683042c9e3b756ec83512c
4
- data.tar.gz: a491f2c431c5ccb04f5fd75b20314f6fc0e7a543
3
+ metadata.gz: e7bada9c179abe1e025f6c215e373df5cb17927c
4
+ data.tar.gz: 04be29b5ea39a050be5d75fe6e03b1c4383a9681
5
5
  SHA512:
6
- metadata.gz: bc05d56c2f0cfa059aee32d9d2a0158ae8f53f74eed2f8abdf69f051dc6cd3d3268ed4f7d260fc55bbd0744dc22c6cb0fd1d5b6228a66363f3ad5c97a7ab3d78
7
- data.tar.gz: d99193f5823229cc5f155bf7b1f5cb1e7eab49af9aa7bf7aa733890a6fdafc35735a92ccab916dcafa2f4be6642235d8c9deac8212b43b148198c30eb545645e
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
- if redis.call('rpush', key, timestamp) <= max_requests then
27
- return false
28
- else
29
- return (timestamp - tonumber(redis.call('lpop', key))) <= time_period
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
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Defense
3
- VERSION = '0.2.0'
3
+ VERSION = '0.2.1'
4
4
  end
5
5
  end
@@ -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' && !['192.168.0.1', '127.0.0.1'].include?(req.ip)
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' && !['192.168.0.1', '127.0.0.1'].include?(ip) ?
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
@@ -50,7 +50,6 @@ describe 'Rack::Defense::callbacks' do
50
50
  end
51
51
 
52
52
  def check_callback_data(trace, matching_request_count, rule_data, req_path)
53
- puts
54
53
  assert_equal matching_request_count, trace.length
55
54
  data = trace[-1]
56
55
  # check callback data
@@ -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
- PERIOD = 60 * 1000 # in milliseconds
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 two throttling
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 PERIOD per ip
15
- config.throttle('login', 3, PERIOD) do |req|
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 50 get requests on path '/search' per PERIOD per ip
20
- config.throttle('res', 30, PERIOD) do |req|
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 PERIOD per authorization token
25
- config.throttle('api', 5, PERIOD) do |req|
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*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*PERIOD) }
45
+ 7.times { |offset| check_post_request(offset + period*window) }
44
46
  end
45
47
  end
46
- it 'not have side effects between differrent throttle rules with mixed requests' do
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*PERIOD)
50
- check_post_request(offset + period*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*PERIOD, ip='192.168.0.1')
58
- check_get_request(offset + period*PERIOD, ip='192.168.0.2')
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 % PERIOD) >= max_requests ? status_throttled : status_ok
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, 11
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, 10
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(11 * i) }
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, 10 + i) }
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, 6, 7, 8, 9].each { |t| refute @counter.throttle?(@key, t), "timestamp #{t}" }
27
- [12, 13, 14, 15].each { |t| assert @counter.throttle?(@key, t), "timestamp #{t}"}
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, 10
32
- assert @counter.throttle? @key, 11
33
- refute @counter.throttle? @key, 16
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, 10
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.0
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-20 00:00:00.000000000 Z
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