rack-defense 0.1.0

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