rack-defense 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3ece635cf91bb8348db49468754161d10d7263d8
4
+ data.tar.gz: 9e83f747514c973078fde57e36f370d1429e8188
5
+ SHA512:
6
+ metadata.gz: c2c434565856a67d7dba649a8d17fc140242e086038c4f61fc6f08e05a6215779f8f76f0baf2fcc0a0fd5e9ff2fcacedb0ba9cc5e737ef21fbd417b29792e2fe
7
+ data.tar.gz: 78d36f081d9084174c7922d48977f48cf2e38eabf800ddbcff5d17388eb312175d0c32e63765eef393b0054568a535bd716eee06fb955316d80615b53d511c70
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ Rack::Defense
2
+ =============
3
+
4
+ A Rack middleware for throttling and filtering requests.
5
+
6
+ [![Code Climate](https://codeclimate.com/github/Sinbadsoft/rack-defense/badges/gpa.svg)](https://codeclimate.com/github/Sinbadsoft/rack-defense) [![Build Status](https://travis-ci.org/Sinbadsoft/rack-defense.svg)](https://travis-ci.org/Sinbadsoft/rack-defense)
7
+ [![Dependency Status](https://gemnasium.com/Sinbadsoft/rack-defense.svg)](https://gemnasium.com/Sinbadsoft/rack-defense)
8
+
9
+ Rack::Defense is a Rack middleware that allows you to easily add request rate limiting and request filtering to your Rack based application (Ruby On Rails, Sinatra etc.).
10
+
11
+ * Throttling (aka rate limiting) happens on __sliding window__ using the provided period, request criteria and maximum request number. It uses Redis to track the request rate.
12
+
13
+ * Request filtering allows to reject requests based on provided critera.
14
+
15
+ Rack::Defense has a small footprint and only two dependencies: [rack](https://github.com/rack/rack) and [redis](https://github.com/redis/redis-rb).
16
+
17
+ ## Getting started
18
+
19
+ Install the rack-defense gem; or add it to you Gemfile with bundler:
20
+
21
+ ```ruby
22
+ # In your Gemfile
23
+ gem 'rack-defense'
24
+ ```
25
+ Tell your app to use the Rack::Defense middleware. For Rails 3+ apps:
26
+ ```
27
+ # In config/application.rb
28
+ config.middleware.use Rack::Defense
29
+ ```
30
+
31
+ Or for Rackup files:
32
+ ```
33
+ # In config.ru
34
+ use Rack::Defense
35
+ ```
36
+
37
+ Add a `rack-defense.rb` file to `config/initalizers/`:
38
+ ```ruby
39
+ # In config/initializers/rack-defense.rb
40
+ Rack::Defense.setup do |config|
41
+ # your configuration here
42
+ end
43
+ ```
44
+
45
+ ## Throttling
46
+ The Rack::Defense middleware evaluates the throttling criterias (lambadas) against the incoming request. If the return value is falsy, the request is not throttled. Otherwise, the returned value is used as a key to throttle the request. The returned key could be the request IP, user name, API token or any discriminator to throttle the requests against.
47
+
48
+ ### Examples
49
+
50
+ Throttle POST requests for path `/login` with a maximum rate of 3 request per minute per IP
51
+ ```ruby
52
+ Rack::Defense.setup do |config|
53
+ config.throttle('login', 3, 60 * 1000) do |req|
54
+ req.ip if req.path == '/login' && req.post?
55
+ end
56
+ end
57
+ ```
58
+
59
+ Throttle GET requests for path `/image` with a maximum rate of 50 request per second per API token
60
+ ```ruby
61
+ Rack::Defense.setup do |config|
62
+ config.throttle('api', 50, 1000) do |req|
63
+ req.env['HTTP_AUTHORIZATION'] if %r{^/api/} =~ req.path
64
+ end
65
+ end
66
+ ```
67
+
68
+ ### Redis Configuration
69
+
70
+ Rack::Defense uses Redis to track request rates. By default, the `REDIS_URL` environment variable is used to setup the store. If not set, it falls back to host `127.0.0.1` port `6379`.
71
+ The redis store can be setup with either a connection url:
72
+ ```ruby
73
+ Rack::Defense.setup do |config|
74
+ store = "redis://:p4ssw0rd@10.0.1.1:6380/15"
75
+ end
76
+ ```
77
+ or directly with a connection object:
78
+ ```ruby
79
+ Rack::Defense.setup do |config|
80
+ store = Redis.new(host: "10.0.1.1", port: 6380, db: 15)
81
+ end
82
+ ```
83
+
84
+ ## Filtering
85
+
86
+ Rack::Defense can reject requests based on arbitrary properties of the request. Matching requests are filtered.
87
+
88
+ ### Examples
89
+ Allow only a whitelist of ips for a given path:
90
+ ```ruby
91
+ Rack::Defense.setup do |config|
92
+ config.ban('ip_whitelist') do |req|
93
+ req.path == '/protected' && !['192.168.0.1', '127.0.0.1'].include?(req.ip)
94
+ end
95
+ end
96
+ ```
97
+
98
+ Allow only requests with a known API authorization token:
99
+ ```ruby
100
+ Rack::Defense.setup do |config|
101
+ config.ban('allow_only_ip_list') do |req|
102
+ %r{^/api/} =~ req.path && Redis.current.sismember('apitokens', req.env['HTTP_AUTHORIZATION'])
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## Response configuration
108
+
109
+ By default, Rack::Defense returns `429 Too Many Requests` and `403 Forbidden` respectively for throttled and banned requests. These responses can be fully configured in the setup:
110
+
111
+ ```ruby
112
+ Rack::Defense.setup do |config|
113
+ config.banned_response =
114
+ ->(env) { [404, {'Content-Type' => 'text/plain'}, ["Not Found"]] }
115
+
116
+ config.throttled_response =
117
+ ->(env) { [503, {'Content-Type' => 'text/plain'}, ["Service Unavailable"]] }
118
+ end
119
+ end
120
+ ```
121
+
122
+ ## License
123
+
124
+ Copyright [Sinbadsoft](http://www.sinbadsoft.com).
125
+
126
+ Licensed under the [MIT License](http://opensource.org/licenses/MIT).
127
+
128
+
129
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new(:test) do |t|
4
+ t.pattern = "spec/*_spec.rb"
5
+ end
6
+
7
+ task :default => :test
@@ -0,0 +1,70 @@
1
+ require 'rack'
2
+ require 'redis'
3
+
4
+ class Rack::Defense
5
+ autoload :ThrottleCounter, 'rack/defense/throttle_counter'
6
+
7
+ class Config
8
+ attr_accessor :banned_response
9
+ attr_accessor :throttled_response
10
+ attr_reader :bans
11
+ attr_reader :throttles
12
+
13
+ def initialize
14
+ @throttles, @bans = {}, {}
15
+ self.banned_response = ->(env) { [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
16
+ self.throttled_response = ->(env) { [429, {'Content-Type' => 'text/plain'}, ["Retry later\n"]] }
17
+ end
18
+
19
+ def throttle(name, max_requests, period, &block)
20
+ counter = ThrottleCounter.new(name, max_requests, period, store)
21
+ throttles[name] = lambda do |req|
22
+ key = block.call(req)
23
+ key && counter.throttle?(key)
24
+ end
25
+ end
26
+
27
+ def ban(name, &block)
28
+ bans[name] = block
29
+ end
30
+
31
+ def store=(value)
32
+ @store = value.is_a?(String) ? Redis.new(url: value) : value
33
+ end
34
+
35
+ def store
36
+ # Redis.new uses REDIS_URL environment variable by default as URL.
37
+ # See https://github.com/redis/redis-rb
38
+ @store ||= Redis.new
39
+ end
40
+ end
41
+
42
+ class << self
43
+ attr_accessor :config
44
+
45
+ def setup(&block)
46
+ self.config = Config.new
47
+ yield config
48
+ end
49
+
50
+ def ban?(req)
51
+ config.bans.any? { |name, filter| filter.call(req) }
52
+ end
53
+
54
+ def throttle?(req)
55
+ config.throttles.any? { |name, filter| filter.call(req) }
56
+ end
57
+ end
58
+
59
+ def initialize(app)
60
+ @app = app
61
+ end
62
+
63
+ def call(env)
64
+ klass, config = self.class, self.class.config
65
+ req = ::Rack::Request.new(env)
66
+ return config.banned_response.call(env) if klass.ban?(req)
67
+ return config.throttled_response.call(env) if klass.throttle?(req)
68
+ @app.call(env)
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ module Rack
2
+ class Defense
3
+ class ThrottleCounter
4
+
5
+ KEY_PREFIX = 'rack-defense'
6
+
7
+ attr_accessor :logger
8
+ attr_accessor :name
9
+
10
+ def initialize(name, max_requests, time_period, store)
11
+ @name, @max_requests, @time_period = name.to_s, max_requests.to_i, time_period.to_i
12
+ @store = store
13
+ end
14
+
15
+ def throttle?(key, timestamp=nil)
16
+ timestamp ||= (Time.now.utc.to_f * 1000).to_i
17
+ @store.eval SCRIPT,
18
+ ["#{KEY_PREFIX}:#{@name}:#{key}"],
19
+ [timestamp, @max_requests, @time_period]
20
+ end
21
+
22
+ SCRIPT = <<-LUA_SCRIPT
23
+ local key = KEYS[1]
24
+ local timestamp, max_requests, time_period = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
25
+ if redis.call('rpush', key, timestamp) <= max_requests then
26
+ return false
27
+ else
28
+ return (timestamp - tonumber(redis.call('lpop', key))) <= time_period
29
+ end
30
+ LUA_SCRIPT
31
+
32
+ private_constant :SCRIPT
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class Defense
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'spec_helper'
2
+ describe 'Rack::Defense::ban' do
3
+ before do
4
+ #
5
+ # configure the Rack::Defense middleware with a ban
6
+ # strategy.
7
+ #
8
+ Rack::Defense.setup do |config|
9
+ # allow only given ips on path
10
+ config.ban('allow_only_ip_list') do |req|
11
+ req.path == '/protected' && !['192.168.0.1', '127.0.0.1'].include?(req.ip)
12
+ end
13
+ end
14
+ end
15
+ it 'ban matching requests' do
16
+ check_request(:get, '/protected','192.168.0.2')
17
+ check_request(:post, '/protected','192.168.0.3')
18
+ check_request(:patch, '/protected','192.168.0.2')
19
+ check_request(:delete, '/protected','192.168.0.2')
20
+ end
21
+ it 'allow non matching request' do
22
+ check_request(:get, '/protected','192.168.0.1')
23
+ check_request(:get, '/protected','127.0.0.1')
24
+ check_request(:get, '/protectedx','192.168.0.5')
25
+ check_request(:post, '/allowed','192.168.0.5')
26
+ end
27
+
28
+ def check_request(verb, path, ip)
29
+ send verb, path, {}, 'REMOTE_ADDR' => ip
30
+ expected_status = path == '/protected' && !['192.168.0.1', '127.0.0.1'].include?(ip) ?
31
+ status_banned : status_ok
32
+ assert_equal expected_status, last_response.status
33
+ end
34
+ end
35
+
@@ -0,0 +1,119 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'Rack::Defense::throttle' do
4
+ PERIOD = 60 * 1000 # in milliseconds
5
+
6
+ before do
7
+ @start_time = Time.utc(2015, 10, 30, 21, 0, 0)
8
+
9
+ #
10
+ # configure the Rack::Defense middleware with two throttling
11
+ # strategies.
12
+ #
13
+ 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
+ req.ip if req.path == '/login' && req.post?
17
+ end
18
+
19
+ # allow only 50 get requests on path '/search' per PERIOD per ip
20
+ config.throttle('res', 30, PERIOD) do |req|
21
+ req.ip if req.path == '/search' && req.get?
22
+ end
23
+
24
+ # allow only 5 get requests on path /api/* per PERIOD per authorization token
25
+ config.throttle('api', 5, PERIOD) do |req|
26
+ req.env['HTTP_AUTHORIZATION'] if %r{^/api/} =~ req.path
27
+ end
28
+ end
29
+ end
30
+ it 'allow ok post' do
31
+ check_post_request
32
+ end
33
+ it 'allow ok get' do
34
+ check_get_request
35
+ end
36
+ it 'ban get requests higher than acceptable rate' do
37
+ 10.times do |period|
38
+ 50.times { |offset| check_get_request(offset + period*PERIOD) }
39
+ end
40
+ end
41
+ it 'ban post requests higher than acceptable rate' do
42
+ 10.times do |period|
43
+ 7.times { |offset| check_post_request(offset + period*PERIOD) }
44
+ end
45
+ end
46
+ it 'not have side effects between differrent throttle rules with mixed requests' do
47
+ 10.times do |period|
48
+ 50.times do |offset|
49
+ check_get_request(offset + period*PERIOD)
50
+ check_post_request(offset + period*PERIOD)
51
+ end
52
+ end
53
+ end
54
+ it 'not have side effects between request filtered by the same rule but with different keys' do
55
+ 10.times do |period|
56
+ 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
+ end
60
+ end
61
+ end
62
+ it 'allow unfiltered requests' do
63
+ 50.times do |offset|
64
+ time = @start_time + offset
65
+ Timecop.freeze(time) do
66
+ # the rule matches the '/search' path and not '/searchx'
67
+ get '/searchx', {}, 'REMOTE_ADDR' => '192.168.0.1'
68
+ assert_equal status_ok, last_response.status
69
+
70
+ # the rule matches only get requests and not post
71
+ post '/search', {}, 'REMOTE_ADDR' => '192.168.0.1'
72
+ assert_equal status_ok, last_response.status
73
+ end
74
+ end
75
+ 10.times do |offset|
76
+ time = @start_time + offset
77
+ Timecop.freeze(time) do
78
+ # the rule matches only post and not get
79
+ get '/login', {}, 'REMOTE_ADDR' => '192.168.0.1'
80
+ assert_equal status_ok, last_response.status
81
+ end
82
+ end
83
+ end
84
+ it 'not have side effects between unfiltered and filtered requests' do
85
+ 50.times do |offset|
86
+ time = @start_time + offset
87
+ Timecop.freeze(time) do
88
+ get '/searchx', {}, 'REMOTE_ADDR' => '192.168.0.1'
89
+ assert_equal status_ok, last_response.status
90
+ get '/search', {}, 'REMOTE_ADDR' => '192.168.0.1'
91
+ assert_equal offset < 30 ? status_ok : status_throttled, last_response.status
92
+ end
93
+ end
94
+ end
95
+ it 'should work with key in http header' do
96
+ 10.times do |offset|
97
+ check_request(:get, '/api/action', offset, 5,
98
+ '192.168.0.1',
99
+ 'HTTP_AUTHORIZATION' => 'token api_token_here')
100
+ end
101
+ end
102
+
103
+ def check_get_request(time_offset=0, ip='192.168.0.1', path='/search')
104
+ check_request(:get, path, time_offset, 30, ip)
105
+ end
106
+
107
+ def check_post_request(time_offset=0, ip='192.168.0.1', path='/login')
108
+ check_request(:post, path, time_offset, 3, ip)
109
+ end
110
+
111
+ def check_request(verb, path, time_offset, max_requests, ip, headers={})
112
+ Timecop.freeze(@start_time + time_offset) do
113
+ send verb, path, {}, headers.merge('REMOTE_ADDR' => ip)
114
+ expected_status = (time_offset % PERIOD) >= max_requests ? status_throttled : status_ok
115
+ assert_equal expected_status, last_response.status, "offset #{time_offset}"
116
+ end
117
+ end
118
+ end
119
+
@@ -0,0 +1,26 @@
1
+ require 'minitest/autorun'
2
+ require 'rack/test'
3
+ require 'redis'
4
+ require 'timecop'
5
+ require 'rack/defense'
6
+
7
+ class MiniTest::Spec
8
+ include Rack::Test::Methods
9
+
10
+ def status_ok; 200 end
11
+ def status_throttled; 429 end
12
+ def status_banned; 403 end
13
+
14
+ def app
15
+ Rack::Builder.new {
16
+ use Rack::Defense
17
+ run ->(env) { [200, {}, ['Hello World']] }
18
+ }.to_app
19
+ end
20
+
21
+ before do
22
+ Timecop.safe_mode = true
23
+ keys = Redis.current.keys("#{Rack::Defense::ThrottleCounter::KEY_PREFIX}:*")
24
+ Redis.current.del *keys if keys.any?
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Rack::Defense::ThrottleCounter do
4
+ before do
5
+ @counter = Rack::Defense::ThrottleCounter.new('upload_photo', 5, 10, Redis.current)
6
+ @key = '192.168.0.1'
7
+ end
8
+
9
+ describe '.throttle?' do
10
+ it 'allow request number max_requests if after period' do
11
+ do_max_requests_minus_one
12
+ refute @counter.throttle? @key, 11
13
+ end
14
+ it 'block request number max_requests if in period' do
15
+ do_max_requests_minus_one
16
+ assert @counter.throttle? @key, 10
17
+ end
18
+ it 'allow consecutive valid periods' do
19
+ (0..20).each { |i| do_max_requests_minus_one(11 * i) }
20
+ end
21
+ it 'block consecutive invalid requests' do
22
+ do_max_requests_minus_one
23
+ (0..20).each { |i| assert @counter.throttle?(@key, 10 + i) }
24
+ end
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}"}
28
+ end
29
+ it 'should unblock after blocking requests' do
30
+ do_max_requests_minus_one
31
+ assert @counter.throttle? @key, 10
32
+ assert @counter.throttle? @key, 11
33
+ refute @counter.throttle? @key, 16
34
+ end
35
+ it 'should include throttled(blocked) request into the request count' do
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
40
+ end
41
+ end
42
+
43
+ def do_max_requests_minus_one(offset=0)
44
+ [0, 2, 3, 5, 9].map { |t| t + offset }.each do |t|
45
+ refute @counter.throttle?(@key, t), "timestamp #{t}"
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-defense
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chaker Nakhli
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.5.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.5.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.1.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.1.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: rake
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '10.3'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '10.3'
67
+ - !ruby/object:Gem::Dependency
68
+ name: minitest
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '5.4'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '5.4'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rack-test
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.6'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.6'
95
+ - !ruby/object:Gem::Dependency
96
+ name: timecop
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '0.7'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '0.7'
109
+ description: A rack middleware for throttling and filtering requests
110
+ email:
111
+ - chaker.nakhli@sinbadsoft.com
112
+ executables: []
113
+ extensions: []
114
+ extra_rdoc_files: []
115
+ files:
116
+ - README.md
117
+ - Rakefile
118
+ - lib/rack/defense.rb
119
+ - lib/rack/defense/throttle_counter.rb
120
+ - lib/rack/defense/version.rb
121
+ - spec/defense_ban_spec.rb
122
+ - spec/defense_throttle_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/throttle_counter_spec.rb
125
+ homepage: http://github.com/sinbadsoft/rack-defense
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options:
131
+ - "--charset=UTF-8"
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 1.9.2
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.2.2
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Throttle and filter requests
150
+ test_files:
151
+ - spec/spec_helper.rb
152
+ - spec/throttle_counter_spec.rb
153
+ - spec/defense_ban_spec.rb
154
+ - spec/defense_throttle_spec.rb