rack-attack 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-attack might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZmExMGQ3ZWEwMTBlMWY1N2ViNjgyMDM0YTI5Njc4ODQzZjU0NmQ3MQ==
5
- data.tar.gz: !binary |-
6
- MDJmYTBjNWE4NTAzNzQ0Yjg1MjQ4MWE5ZDE1YzU4OWU4YTkxZDBkMA==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- ZDJmYWJjMzhhYTdkZTYxMjM0ZDY2OGQ0ZjA2NzQ2ZGU2YTBiOWY2N2EwYTA2
10
- MGIzNTBiMjAwMDhhYWE2YzMyZTI0OTk3MGM5MjU4NmQzYTc4YzdmNDYxNzcx
11
- OTljOTVjNGE4YTQ1ZDQyMzY2MDdiMjUyZGFhNjFjNzc0NDBiZWQ=
12
- data.tar.gz: !binary |-
13
- Y2E5NGViNGQxMGFmMTM0ZWM2N2YzYWQ4Yjg4ZTkyMDdkNTY5NGVjOWUwNzEx
14
- M2ViYWJlZWFiNTcxNGIwODk0Nzk5MDY5YjA5N2Y2MjE5ZjYxYTkzNzcyMWFk
15
- NjQ4YzVhZGY0N2U2NzE1NjQwODkyYzIzMDFmZDMxYmM0ZmVlM2U=
2
+ SHA1:
3
+ metadata.gz: 30436667301e528e76d76b7454ad73ca9dd08a08
4
+ data.tar.gz: 72e275cb98d8e38b478c4db4d7fa5bdb4759c7ab
5
+ SHA512:
6
+ metadata.gz: d2a8d0690b58f15a6f512077408fa40cec5c7e23b39ebc470a53a753273f1497e3605930106d2c16dd41a8b3edef39a3e9b66b0fd4778b871c5ab0017e2bd4ac
7
+ data.tar.gz: 2a097e071cafcdb11cf36d4150c459723877a852b7920fdf3ed5fa74cedf1ea3d19b25357d0bd31c3b47c48d3a938575e877aade034e8fc7f5eba882101385b5
data/README.md CHANGED
@@ -10,23 +10,30 @@ Throttle state is stored in a configurable cache (e.g. `Rails.cache`), presumabl
10
10
 
11
11
  Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
12
12
 
13
+ ```ruby
13
14
  # In your Gemfile
14
15
  gem 'rack-attack'
15
-
16
+ ```
16
17
  Tell your app to use the Rack::Attack middleware.
17
18
  For Rails 3 apps:
18
19
 
20
+ ```ruby
19
21
  # In config/application.rb
20
22
  config.middleware.use Rack::Attack
23
+ ```
21
24
 
22
25
  Or for Rackup files:
23
26
 
27
+ ```ruby
24
28
  # In config.ru
25
29
  use Rack::Attack
30
+ ```
26
31
 
27
32
  Optionally configure the cache store for throttling:
28
33
 
34
+ ```ruby
29
35
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
36
+ ```
30
37
 
31
38
  Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
32
39
 
@@ -41,6 +48,7 @@ The Rack::Attack middleware compares each request against *whitelists*, *blackli
41
48
 
42
49
  The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
43
50
 
51
+ ```ruby
44
52
  def call(env)
45
53
  req = Rack::Request.new(env)
46
54
 
@@ -55,6 +63,7 @@ The algorithm is actually more concise in code: See [Rack::Attack.call](https://
55
63
  @app.call(env)
56
64
  end
57
65
  end
66
+ ```
58
67
 
59
68
  ## About Tracks
60
69
 
@@ -62,20 +71,24 @@ The algorithm is actually more concise in code: See [Rack::Attack.call](https://
62
71
 
63
72
  ## Usage
64
73
 
65
- Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise.
74
+ Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
75
+ these go in an initializer in `config/initializers/`.
66
76
  A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object is passed to the block (named 'req' in the examples).
67
77
 
68
78
  ### Whitelists
69
79
 
80
+ ```ruby
70
81
  # Always allow requests from localhost
71
82
  # (blacklist & throttles are skipped)
72
83
  Rack::Attack.whitelist('allow from localhost') do |req|
73
84
  # Requests are allowed if the return value is truthy
74
85
  '127.0.0.1' == req.ip
75
86
  end
87
+ ```
76
88
 
77
89
  ### Blacklists
78
90
 
91
+ ```ruby
79
92
  # Block requests from 1.2.3.4
80
93
  Rack::Attack.blacklist('block 1.2.3.4') do |req|
81
94
  # Request are blocked if the return value is truthy
@@ -86,9 +99,31 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
86
99
  Rack::Attack.blacklist('block bad UA logins') do |req|
87
100
  req.path == '/login' && req.post? && req.user_agent == 'BadUA'
88
101
  end
102
+ ```
103
+
104
+ #### Fail2Ban
105
+
106
+ `Fail2Ban.filter` can be used within a blacklist to block all requests from misbehaving clients.
107
+ This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
108
+ See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
109
+ how the parameters work.
110
+
111
+ ```ruby
112
+ # Block requests containing '/etc/password' in the params.
113
+ # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
114
+ Rack::Attack.blacklist('fail2ban pentesters') do |req|
115
+ # `filter` returns truthy value if request fails, or if it's from a previously banned IP
116
+ # so the request is blocked
117
+ Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
118
+ # The count for the IP is incremented if the return value is truthy.
119
+ CGI.unescape(req.query_string) =~ %r{/etc/passwd}
120
+ end
121
+ end
122
+ ```
89
123
 
90
124
  ### Throttles
91
125
 
126
+ ```ruby
92
127
  # Throttle requests to 5 requests per second per ip
93
128
  Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
94
129
  # If the return value is truthy, the cache key for the return value
@@ -101,12 +136,15 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
101
136
  end
102
137
 
103
138
  # Throttle login attempts for a given email parameter to 6 reqs/minute
139
+ # Return the email as a discriminator on POST /login requests
104
140
  Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
105
- req.path == '/login' && req.post? && req.params['email']
141
+ req.params['email'] if req.path == '/login' && req.post?
106
142
  end
143
+ ```
107
144
 
108
145
  ### Tracks
109
146
 
147
+ ```ruby
110
148
  # Track requests from a special user agent
111
149
  Rack::Attack.track("special_agent") do |req|
112
150
  req.user_agent == "SpecialAgent"
@@ -119,12 +157,13 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
119
157
  STATSD.increment("special_agent")
120
158
  end
121
159
  end
122
-
160
+ ```
123
161
 
124
162
  ## Responses
125
163
 
126
164
  Customize the response of blacklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
127
165
 
166
+ ```ruby
128
167
  Rack::Attack.blacklisted_response = lambda do |env|
129
168
  [ 503, {}, ['Blocked']]
130
169
  end
@@ -139,10 +178,13 @@ Customize the response of blacklisted and throttled requests using an object tha
139
178
 
140
179
  [ 503, {}, [body]]
141
180
  end
181
+ ```
142
182
 
143
183
  For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
144
184
 
185
+ ```ruby
145
186
  request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
187
+ ```
146
188
 
147
189
  ## Logging & Instrumentation
148
190
 
@@ -150,9 +192,17 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
150
192
 
151
193
  You can subscribe to 'rack.attack' events and log it, graph it, etc:
152
194
 
195
+ ```ruby
153
196
  ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
154
197
  puts req.inspect
155
198
  end
199
+ ```
200
+
201
+ ## Testing
202
+
203
+ A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
204
+ need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
205
+ for more on how to do this.
156
206
 
157
207
  ## Performance
158
208
 
@@ -6,6 +6,8 @@ module Rack::Attack
6
6
  autoload :Whitelist, 'rack/attack/whitelist'
7
7
  autoload :Blacklist, 'rack/attack/blacklist'
8
8
  autoload :Track, 'rack/attack/track'
9
+ autoload :StoreProxy,'rack/attack/store_proxy'
10
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
9
11
 
10
12
  class << self
11
13
 
@@ -11,15 +11,7 @@ module Rack
11
11
 
12
12
  attr_reader :store
13
13
  def store=(store)
14
- # RedisStore#increment needs different behavior, so detect that
15
- # (method has an arity of 2; must call #expire separately
16
- if defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
17
- # ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
18
- # so use the raw Redis::Store instead
19
- @store = store.instance_variable_get(:@data)
20
- else
21
- @store = store
22
- end
14
+ @store = StoreProxy.build(store)
23
15
  end
24
16
 
25
17
  def count(unprefixed_key, period)
@@ -29,15 +21,18 @@ module Rack
29
21
  do_count(key, expires_in)
30
22
  end
31
23
 
24
+ def read(unprefixed_key)
25
+ store.read("#{prefix}:#{unprefixed_key}")
26
+ end
27
+
28
+ def write(unprefixed_key, value, expires_in)
29
+ store.write("#{prefix}:#{unprefixed_key}", value, :expires_in => expires_in)
30
+ end
31
+
32
32
  private
33
33
  def do_count(key, expires_in)
34
- # Workaround Redis::Store's interface
35
- if defined?(::Redis::Store) && store.is_a?(::Redis::Store)
36
- result = store.incr(key)
37
- store.expire(key, expires_in)
38
- else
39
- result = store.increment(key, 1, :expires_in => expires_in)
40
- end
34
+ result = store.increment(key, 1, :expires_in => expires_in)
35
+
41
36
  # NB: Some stores return nil when incrementing uninitialized values
42
37
  if result.nil?
43
38
  store.write(key, 1, :expires_in => expires_in)
@@ -0,0 +1,43 @@
1
+ module Rack
2
+ module Attack
3
+ class Fail2Ban
4
+ class << self
5
+ def filter(discriminator, options)
6
+ bantime = options[:bantime] or raise ArgumentError, "Must pass bantime option"
7
+ findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
8
+ maxretry = options[:maxretry] or raise ArgumentError, "Must pass maxretry option"
9
+
10
+ if banned?(discriminator)
11
+ # Return true for blacklist
12
+ true
13
+ elsif yield
14
+ fail!(discriminator, bantime, findtime, maxretry)
15
+ end
16
+ end
17
+
18
+ private
19
+ def fail!(discriminator, bantime, findtime, maxretry)
20
+ count = cache.count("fail2ban:count:#{discriminator}", findtime)
21
+ if count >= maxretry
22
+ ban!(discriminator, bantime)
23
+ end
24
+
25
+ # Return true for blacklist
26
+ true
27
+ end
28
+
29
+ def ban!(discriminator, bantime)
30
+ cache.write("fail2ban:ban:#{discriminator}", 1, bantime)
31
+ end
32
+
33
+ def banned?(discriminator)
34
+ cache.read("fail2ban:ban:#{discriminator}")
35
+ end
36
+
37
+ def cache
38
+ Rack::Attack.cache
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
1
+ require 'delegate'
2
+
3
+ module Rack
4
+ module Attack
5
+ class StoreProxy
6
+ def self.build(store)
7
+ # RedisStore#increment needs different behavior, so detect that
8
+ # (method has an arity of 2; must call #expire separately
9
+ if defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
10
+ # ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
11
+ # so use the raw Redis::Store instead
12
+ store = store.instance_variable_get(:@data)
13
+ end
14
+
15
+ if defined?(::Redis::Store) && store.is_a?(::Redis::Store)
16
+ RedisStoreProxy.new(store)
17
+ else
18
+ store
19
+ end
20
+ end
21
+
22
+ class RedisStoreProxy < SimpleDelegator
23
+ def initialize(store)
24
+ super(store)
25
+ end
26
+
27
+ def read(key)
28
+ self.get(key)
29
+ end
30
+
31
+ def write(key, value, options={})
32
+ if (expires_in = options[:expires_in])
33
+ self.setex(key, expires_in, value)
34
+ else
35
+ self.set(key, value)
36
+ end
37
+ end
38
+
39
+ def increment(key, amount, options={})
40
+ count = nil
41
+ self.pipelined do
42
+ count = self.incrby(key, amount)
43
+ self.expire(key, options[:expires_in]) if options[:expires_in]
44
+ end
45
+ count.value if count
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Attack
3
- VERSION = '2.1.1'
3
+ VERSION = '2.2.0'
4
4
  end
5
5
  end
@@ -0,0 +1,121 @@
1
+ require_relative 'spec_helper'
2
+ describe 'Rack::Attack.Fail2Ban' do
3
+ before do
4
+ # Use a long findtime; failures due to cache key rotation less likely
5
+ @cache = Rack::Attack.cache
6
+ @findtime = 60
7
+ @bantime = 60
8
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
+ @f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
10
+ Rack::Attack.blacklist('pentest') do |req|
11
+ Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
12
+ end
13
+ end
14
+
15
+ describe 'discriminator has not been banned' do
16
+ describe 'making ok request' do
17
+ it 'succeeds' do
18
+ get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
19
+ last_response.status.must_equal 200
20
+ end
21
+ end
22
+
23
+ describe 'making failing request' do
24
+ describe 'when not at maxretry' do
25
+ before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
26
+ it 'fails' do
27
+ last_response.status.must_equal 503
28
+ end
29
+
30
+ it 'increases fail count' do
31
+ key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
32
+ @cache.store.read(key).must_equal 1
33
+ end
34
+
35
+ it 'is not banned' do
36
+ key = "rack::attack:fail2ban:1.2.3.4"
37
+ @cache.store.read(key).must_be_nil
38
+ end
39
+ end
40
+
41
+ describe 'when at maxretry' do
42
+ before do
43
+ # maxretry is 2 - so hit with an extra failed request first
44
+ get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
45
+ get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
46
+ end
47
+
48
+ it 'fails' do
49
+ last_response.status.must_equal 503
50
+ end
51
+
52
+ it 'increases fail count' do
53
+ key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
54
+ @cache.store.read(key).must_equal 2
55
+ end
56
+
57
+ it 'is banned' do
58
+ key = "rack::attack:fail2ban:ban:1.2.3.4"
59
+ @cache.store.read(key).must_equal 1
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'discriminator has been banned' do
67
+ before do
68
+ # maxretry is 2 - so hit enough times to get banned
69
+ get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
70
+ get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
71
+ end
72
+
73
+ describe 'making request for other discriminator' do
74
+ it 'succeeds' do
75
+ get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
76
+ last_response.status.must_equal 200
77
+ end
78
+ end
79
+
80
+ describe 'making ok request' do
81
+ before do
82
+ get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
83
+ end
84
+
85
+ it 'fails' do
86
+ last_response.status.must_equal 503
87
+ end
88
+
89
+ it 'does not increase fail count' do
90
+ key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
91
+ @cache.store.read(key).must_equal 2
92
+ end
93
+
94
+ it 'is still banned' do
95
+ key = "rack::attack:fail2ban:ban:1.2.3.4"
96
+ @cache.store.read(key).must_equal 1
97
+ end
98
+ end
99
+
100
+ describe 'making failing request' do
101
+ before do
102
+ get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
103
+ end
104
+
105
+ it 'fails' do
106
+ last_response.status.must_equal 503
107
+ end
108
+
109
+ it 'does not increase fail count' do
110
+ key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
111
+ @cache.store.read(key).must_equal 2
112
+ end
113
+
114
+ it 'is still banned' do
115
+ key = "rack::attack:fail2ban:ban:1.2.3.4"
116
+ @cache.store.read(key).must_equal 1
117
+ end
118
+ end
119
+
120
+ end
121
+ end
@@ -20,6 +20,7 @@ if ENV['TEST_INTEGRATION']
20
20
  ]
21
21
 
22
22
  cache_stores.each do |store|
23
+ store = Rack::Attack::StoreProxy.build(store)
23
24
  describe "with #{store.class}" do
24
25
 
25
26
  before {
@@ -51,6 +52,28 @@ if ENV['TEST_INTEGRATION']
51
52
  @cache.send(:do_count, @key, @expires_in).must_equal 1
52
53
  end
53
54
  end
55
+
56
+ describe "write" do
57
+ it "should write a value to the store with prefix" do
58
+ @cache.write("cache-test-key", "foobar", 1)
59
+ store.read(@key).must_equal "foobar"
60
+ end
61
+ end
62
+
63
+ describe "write after expiry" do
64
+ it "must not have a value" do
65
+ @cache.write("cache-test-key", "foobar", @expires_in)
66
+ sleep @expires_in # tick... tick... tick...
67
+ store.read(@key).must_be :nil?
68
+ end
69
+ end
70
+
71
+ describe "read" do
72
+ it "must read the value with a prefix" do
73
+ store.write(@key, "foobar", :expires_in => @expires_in)
74
+ @cache.read("cache-test-key").must_equal "foobar"
75
+ end
76
+ end
54
77
  end
55
78
 
56
79
  end
metadata CHANGED
@@ -1,83 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-attack
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Suggs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-16 00:00:00.000000000 Z
11
+ date: 2013-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rack-test
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - '>='
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: activesupport
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - '>='
74
74
  - !ruby/object:Gem::Version
75
75
  version: 3.0.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - '>='
81
81
  - !ruby/object:Gem::Version
82
82
  version: 3.0.0
83
83
  - !ruby/object:Gem::Dependency
@@ -98,28 +98,28 @@ dependencies:
98
98
  name: redis-activesupport
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ! '>='
101
+ - - '>='
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ! '>='
108
+ - - '>='
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: dalli
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ! '>='
115
+ - - '>='
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ! '>='
122
+ - - '>='
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  description: A rack middleware for throttling and blocking abusive requests
@@ -131,6 +131,8 @@ files:
131
131
  - lib/rack/attack/blacklist.rb
132
132
  - lib/rack/attack/cache.rb
133
133
  - lib/rack/attack/check.rb
134
+ - lib/rack/attack/fail2ban.rb
135
+ - lib/rack/attack/store_proxy.rb
134
136
  - lib/rack/attack/throttle.rb
135
137
  - lib/rack/attack/track.rb
136
138
  - lib/rack/attack/version.rb
@@ -138,6 +140,7 @@ files:
138
140
  - lib/rack/attack.rb
139
141
  - Rakefile
140
142
  - README.md
143
+ - spec/fail2ban_spec.rb
141
144
  - spec/rack_attack_cache_spec.rb
142
145
  - spec/rack_attack_spec.rb
143
146
  - spec/rack_attack_throttle_spec.rb
@@ -153,24 +156,24 @@ require_paths:
153
156
  - lib
154
157
  required_ruby_version: !ruby/object:Gem::Requirement
155
158
  requirements:
156
- - - ! '>='
159
+ - - '>='
157
160
  - !ruby/object:Gem::Version
158
161
  version: 1.9.3
159
162
  required_rubygems_version: !ruby/object:Gem::Requirement
160
163
  requirements:
161
- - - ! '>='
164
+ - - '>='
162
165
  - !ruby/object:Gem::Version
163
166
  version: '0'
164
167
  requirements: []
165
168
  rubyforge_project:
166
- rubygems_version: 2.0.3
169
+ rubygems_version: 2.0.2
167
170
  signing_key:
168
171
  specification_version: 4
169
172
  summary: Block & throttle abusive requests
170
173
  test_files:
174
+ - spec/fail2ban_spec.rb
171
175
  - spec/rack_attack_cache_spec.rb
172
176
  - spec/rack_attack_spec.rb
173
177
  - spec/rack_attack_throttle_spec.rb
174
178
  - spec/rack_attack_track_spec.rb
175
179
  - spec/spec_helper.rb
176
- has_rdoc: