rack-attack 2.1.1 → 2.2.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.

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: