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 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