rack-attack 5.0.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0d4400f3695de545e524a11ed1eaa0ae14a5f3d2
4
- data.tar.gz: 76c98ade06cea2447a46de122ab23dbc858b8c1d
2
+ SHA256:
3
+ metadata.gz: 6adfaa8597fafd710ea09558be12046bf35eb3816a2cb81c275e07f12c3ebc9f
4
+ data.tar.gz: 9e256c9b94880a4118f47d3bec2826bdb1b0d6fb501567384cd3a8049b44fa86
5
5
  SHA512:
6
- metadata.gz: 44d33cd6011e99ec3ca5cbdd8d1bcdecb8f2463fbd55e22f0266f0b17d660416f68d7ce46ac6f318549edc25b937ea3f0f73df14e9865dcb26f0d935f43a3682
7
- data.tar.gz: 1be320077b7533ad2edb48a7799ee9681641beeb4a0232d2079f664ac73494c3cd5c50f403405656177e70a377435834b1b8af7fc922ecbd3bc72165f8659bf5
6
+ metadata.gz: ab110884b8a75cec12ce734457a5e71f1f04351ad21f6ce160dbfda8161c642521e23c6d0d655dd03f51bafffecdff188cb89d88bf2b2e3b40e109bbf1ebffa2
7
+ data.tar.gz: c9e46912da6c457bc53970122c4cfc3afab4977037bf52c1dcd5ba9502ba6d83c0137b939d217645df347d2ac1cbec1b796f0d114bae2d0a668e4850dcc018b2
data/README.md CHANGED
@@ -8,10 +8,14 @@ Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache
8
8
 
9
9
  See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
10
10
 
11
- [![Gem Version](https://badge.fury.io/rb/rack-attack.png)](http://badge.fury.io/rb/rack-attack)
12
- [![Build Status](https://travis-ci.org/kickstarter/rack-attack.png?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
13
- [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](https://codeclimate.com/github/kickstarter/rack-attack)
11
+ [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](http://badge.fury.io/rb/rack-attack)
12
+ [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
13
+ [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
14
14
 
15
+ ## Looking for maintainers
16
+
17
+ I'm looking for new maintainers to help me support Rack::Attack. Check out
18
+ [issue #219 for details](https://github.com/kickstarter/rack-attack/issues/219).
15
19
 
16
20
  ## Getting started
17
21
 
@@ -22,7 +26,7 @@ Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it t
22
26
  gem 'rack-attack'
23
27
  ```
24
28
  Tell your app to use the Rack::Attack middleware.
25
- For Rails 3+ apps:
29
+ For Rails apps:
26
30
 
27
31
  ```ruby
28
32
  # In config/application.rb
@@ -36,9 +40,9 @@ Or for Rackup files:
36
40
  use Rack::Attack
37
41
  ```
38
42
 
39
- Add a `rack-attack.rb` file to `config/initializers/`:
43
+ Add a `rack_attack.rb` file to `config/initializers/`:
40
44
  ```ruby
41
- # In config/initializers/rack-attack.rb
45
+ # In config/initializers/rack_attack.rb
42
46
  class Rack::Attack
43
47
  # your custom configuration...
44
48
  end
@@ -136,7 +140,7 @@ how the parameters work. For multiple filters, be sure to put each filter in a
136
140
  Rack::Attack.blocklist('fail2ban pentesters') do |req|
137
141
  # `filter` returns truthy value if request fails, or if it's from a previously banned IP
138
142
  # so the request is blocked
139
- Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
143
+ Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
140
144
  # The count for the IP is incremented if the return value is truthy
141
145
  CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
142
146
  req.path.include?('/etc/passwd') ||
@@ -159,7 +163,7 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
159
163
  # `filter` returns false value if request is to your login page (but still
160
164
  # increments the count) so request below the limit are not blocked until
161
165
  # they hit the limit. At that point, filter will return true and block.
162
- Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do
166
+ Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 20, findtime: 1.minute, bantime: 1.hour) do
163
167
  # The count for the IP is incremented if the return value is truthy.
164
168
  req.path == '/login' and req.post?
165
169
  end
@@ -171,7 +175,7 @@ end
171
175
 
172
176
  ```ruby
173
177
  # Throttle requests to 5 requests per second per ip
174
- Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
178
+ Rack::Attack.throttle('req/ip', limit: 5, period: 1.second) do |req|
175
179
  # If the return value is truthy, the cache key for the return value
176
180
  # is incremented and compared with the limit. In this case:
177
181
  # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
@@ -183,7 +187,7 @@ end
183
187
 
184
188
  # Throttle login attempts for a given email parameter to 6 reqs/minute
185
189
  # Return the email as a discriminator on POST /login requests
186
- Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
190
+ Rack::Attack.throttle('logins/email', limit: 6, period: 60) do |req|
187
191
  req.params['email'] if req.path == '/login' && req.post?
188
192
  end
189
193
 
@@ -191,7 +195,7 @@ end
191
195
  # Rack::Auth::Basic has authenticated the user:
192
196
  limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
193
197
  period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute}
194
- Rack::Attack.throttle('req/ip', :limit => limit_proc, :period => period_proc) do |req|
198
+ Rack::Attack.throttle('req/ip', limit: limit_proc, period: period_proc) do |req|
195
199
  req.ip
196
200
  end
197
201
  ```
@@ -205,7 +209,7 @@ Rack::Attack.track("special_agent") do |req|
205
209
  end
206
210
 
207
211
  # Supports optional limit and period, triggers the notification only when the limit is reached.
208
- Rack::Attack.track("special_agent", :limit => 6, :period => 60.seconds) do |req|
212
+ Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
209
213
  req.user_agent == "SpecialAgent"
210
214
  end
211
215
 
@@ -233,7 +237,8 @@ Rack::Attack.throttled_response = lambda do |env|
233
237
  # NB: you have access to the name and other data about the matched throttle
234
238
  # env['rack.attack.matched'],
235
239
  # env['rack.attack.match_type'],
236
- # env['rack.attack.match_data']
240
+ # env['rack.attack.match_data'],
241
+ # env['rack.attack.match_discriminator']
237
242
 
238
243
  # Using 503 because it may make attacker think that they have successfully
239
244
  # DOSed the site. Rack::Attack returns 429 for throttling by default
@@ -316,6 +321,23 @@ Pull requests and issues are greatly appreciated. This project is intended to be
316
321
  a safe, welcoming space for collaboration, and contributors are expected to
317
322
  adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
318
323
 
324
+ ### Testing pull requests
325
+
326
+ To run the minitest test suite, you will need both [Redis](http://redis.io/) and
327
+ [Memcached](https://memcached.org/) running locally and bound to IP `127.0.0.1` on
328
+ default ports (`6379` for Redis, and `11211` for Memcached) and able to be
329
+ accessed without authentication.
330
+
331
+ Install dependencies by running
332
+ ```sh
333
+ bundle install
334
+ ```
335
+
336
+ Then run the test suite by running
337
+ ```sh
338
+ bundle exec rake
339
+ ```
340
+
319
341
  ## Mailing list
320
342
 
321
343
  New releases of Rack::Attack are announced on
data/Rakefile CHANGED
@@ -10,11 +10,14 @@ namespace :test do
10
10
 
11
11
  Rake::TestTask.new(:integration) do |t|
12
12
  t.pattern = "spec/integration/*_spec.rb"
13
- t.warning = false
13
+ end
14
+
15
+ Rake::TestTask.new(:acceptance) do |t|
16
+ t.pattern = "spec/acceptance/*_spec.rb"
14
17
  end
15
18
  end
16
19
 
17
20
  desc 'Run tests'
18
- task :test => %w[test:units test:integration]
21
+ task :test => %w[test:units test:integration test:acceptance]
19
22
 
20
23
  task :default => :test
@@ -2,11 +2,14 @@ require 'rack'
2
2
  require 'forwardable'
3
3
 
4
4
  class Rack::Attack
5
+ class MisconfiguredStoreError < StandardError; end
6
+ class MissingStoreError < StandardError; end
7
+
5
8
  autoload :Cache, 'rack/attack/cache'
6
9
  autoload :PathNormalizer, 'rack/attack/path_normalizer'
7
10
  autoload :Check, 'rack/attack/check'
8
11
  autoload :Throttle, 'rack/attack/throttle'
9
- autoload :Safelist, 'rack/attack/safelist'
12
+ autoload :Safelist, 'rack/attack/safelist'
10
13
  autoload :Blocklist, 'rack/attack/blocklist'
11
14
  autoload :Track, 'rack/attack/track'
12
15
  autoload :StoreProxy, 'rack/attack/store_proxy'
@@ -47,7 +50,7 @@ class Rack::Attack
47
50
  self.tracks[name] = Track.new(name, options, block)
48
51
  end
49
52
 
50
- def safelists; @safelists ||= {}; end
53
+ def safelists; @safelists ||= {}; end
51
54
  def blocklists; @blocklists ||= {}; end
52
55
  def throttles; @throttles ||= {}; end
53
56
  def tracks; @tracks ||= {}; end
@@ -115,7 +118,7 @@ class Rack::Attack
115
118
 
116
119
  def blacklisted_response
117
120
  warn "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead."
118
- self.blocklisted_response
121
+ blocklisted_response
119
122
  end
120
123
 
121
124
  end
@@ -7,7 +7,7 @@ module Rack
7
7
  'allow2ban'
8
8
  end
9
9
 
10
- # everything the same here except we return only return true
10
+ # everything is the same here except we only return true
11
11
  # (blocking the request) if they have tripped the limit.
12
12
  def fail!(discriminator, bantime, findtime, maxretry)
13
13
  count = cache.count("#{key_prefix}:count:#{discriminator}", findtime)
@@ -46,15 +46,20 @@ module Rack
46
46
  end
47
47
 
48
48
  def do_count(key, expires_in)
49
- result = store.increment(key, 1, :expires_in => expires_in)
49
+ if store.nil?
50
+ raise Rack::Attack::MissingStoreError
51
+ elsif !store.respond_to?(:increment)
52
+ raise Rack::Attack::MisconfiguredStoreError, "Store needs to respond to #increment"
53
+ else
54
+ result = store.increment(key, 1, :expires_in => expires_in)
50
55
 
51
- # NB: Some stores return nil when incrementing uninitialized values
52
- if result.nil?
53
- store.write(key, 1, :expires_in => expires_in)
56
+ # NB: Some stores return nil when incrementing uninitialized values
57
+ if result.nil?
58
+ store.write(key, 1, :expires_in => expires_in)
59
+ end
60
+ result || 1
54
61
  end
55
- result || 1
56
62
  end
57
-
58
63
  end
59
64
  end
60
65
  end
@@ -15,11 +15,8 @@ class Rack::Attack
15
15
  end
16
16
 
17
17
  PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
18
- # For Rails 4+ apps
18
+ # For Rails apps
19
19
  ::ActionDispatch::Journey::Router::Utils
20
- elsif defined?(::Journey::Router::Utils)
21
- # for Rails 3.2
22
- ::Journey::Router::Utils
23
20
  else
24
21
  FallbackPathNormalizer
25
22
  end
@@ -1,7 +1,7 @@
1
1
  module Rack
2
2
  class Attack
3
3
  module StoreProxy
4
- PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy]
4
+ PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy].freeze
5
5
 
6
6
  ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore']).freeze
7
7
  ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
@@ -20,7 +20,10 @@ module Rack
20
20
  # We also want to use the underlying Dalli client instead of ::ActiveSupport::Cache::MemCacheStore,
21
21
  # and the MemCache client if using Rails 3.x
22
22
 
23
- client = store.instance_variable_get(:@data)
23
+ if store.instance_variable_defined?(:@data)
24
+ client = store.instance_variable_get(:@data)
25
+ end
26
+
24
27
  if ACTIVE_SUPPORT_WRAPPER_CLASSES.include?(store.class.to_s) && ACTIVE_SUPPORT_CLIENTS.include?(client.class.to_s)
25
28
  client
26
29
  else
@@ -5,7 +5,14 @@ module Rack
5
5
  module StoreProxy
6
6
  class RedisStoreProxy < SimpleDelegator
7
7
  def self.handle?(store)
8
- defined?(::Redis::Store) && store.is_a?(::Redis::Store)
8
+ # Using const_defined? for now.
9
+ #
10
+ # Go back to use defined? once this ruby issue is
11
+ # fixed and released:
12
+ # https://bugs.ruby-lang.org/issues/14407
13
+ #
14
+ # defined?(::Redis::Store) && store.is_a?(::Redis::Store)
15
+ const_defined?("::Redis::Store") && store.is_a?(::Redis::Store)
9
16
  end
10
17
 
11
18
  def initialize(store)
@@ -13,31 +20,33 @@ module Rack
13
20
  end
14
21
 
15
22
  def read(key)
16
- self.get(key, raw: true)
23
+ get(key, raw: true)
17
24
  rescue Redis::BaseError
18
25
  end
19
26
 
20
27
  def write(key, value, options={})
21
28
  if (expires_in = options[:expires_in])
22
- self.setex(key, expires_in, value, raw: true)
29
+ setex(key, expires_in, value, raw: true)
23
30
  else
24
- self.set(key, value, raw: true)
31
+ set(key, value, raw: true)
25
32
  end
26
33
  rescue Redis::BaseError
27
34
  end
28
35
 
29
36
  def increment(key, amount, options={})
30
37
  count = nil
31
- self.pipelined do
32
- count = self.incrby(key, amount)
33
- self.expire(key, options[:expires_in]) if options[:expires_in]
38
+
39
+ pipelined do
40
+ count = incrby(key, amount)
41
+ expire(key, options[:expires_in]) if options[:expires_in]
34
42
  end
43
+
35
44
  count.value if count
36
45
  rescue Redis::BaseError
37
46
  end
38
47
 
39
48
  def delete(key, options={})
40
- self.del(key)
49
+ del(key)
41
50
  rescue Redis::BaseError
42
51
  end
43
52
  end
@@ -1,7 +1,8 @@
1
1
  module Rack
2
2
  class Attack
3
3
  class Throttle
4
- MANDATORY_OPTIONS = [:limit, :period]
4
+ MANDATORY_OPTIONS = [:limit, :period].freeze
5
+
5
6
  attr_reader :name, :limit, :period, :block, :type
6
7
  def initialize(name, options, block)
7
8
  @name, @block = name, block
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Attack
3
- VERSION = '5.0.1'
3
+ VERSION = '5.1.0'
4
4
  end
5
5
  end
@@ -0,0 +1,21 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "#blocklist" do
4
+ before do
5
+ Rack::Attack.blocklist("block 1.2.3.4") do |request|
6
+ request.ip == "1.2.3.4"
7
+ end
8
+ end
9
+
10
+ it "forbids request if blocklist condition is true" do
11
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
12
+
13
+ assert_equal 403, last_response.status
14
+ end
15
+
16
+ it "succeeds if blocklist condition is false" do
17
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
18
+
19
+ assert_equal 200, last_response.status
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "../spec_helper"
2
+
3
+ describe "#safelist" do
4
+ before do
5
+ Rack::Attack.blocklist("block 1.2.3.4") do |request|
6
+ request.ip == "1.2.3.4"
7
+ end
8
+
9
+ Rack::Attack.safelist("safe path") do |request|
10
+ request.path == "/safe_space"
11
+ end
12
+ end
13
+
14
+ it "forbids request if blocklist condition is true and safelist is false" do
15
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
16
+
17
+ assert_equal 403, last_response.status
18
+ end
19
+
20
+ it "succeeds if blocklist condition is false and safelist is false" do
21
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
22
+
23
+ assert_equal 200, last_response.status
24
+ end
25
+
26
+ it "succeeds request if blocklist condition is false and safelist is true" do
27
+ get "/safe_space", {}, "REMOTE_ADDR" => "5.6.7.8"
28
+
29
+ assert_equal 200, last_response.status
30
+ end
31
+
32
+ it "succeeds request if both blocklist and safelist conditions are true" do
33
+ get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
34
+
35
+ assert_equal 200, last_response.status
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "../spec_helper"
2
+ require "timecop"
3
+
4
+ describe "#throttle" do
5
+ it "allows one request per minute by IP" do
6
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+
8
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
9
+ request.ip
10
+ end
11
+
12
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
13
+
14
+ assert_equal 200, last_response.status
15
+
16
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
17
+
18
+ assert_equal 429, last_response.status
19
+
20
+ get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
21
+
22
+ assert_equal 200, last_response.status
23
+
24
+ Timecop.travel(60) do
25
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
26
+
27
+ assert_equal 200, last_response.status
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'spec_helper'
2
+
2
3
  describe 'Rack::Attack.Allow2Ban' do
3
4
  before do
4
5
  # Use a long findtime; failures due to cache key rotation less likely
@@ -7,6 +8,7 @@ describe 'Rack::Attack.Allow2Ban' do
7
8
  @bantime = 60
8
9
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
10
  @f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
11
+
10
12
  Rack::Attack.blocklist('pentest') do |req|
11
13
  Rack::Attack::Allow2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
12
14
  end
@@ -23,6 +25,7 @@ describe 'Rack::Attack.Allow2Ban' do
23
25
  describe 'making qualifying request' do
24
26
  describe 'when not at maxretry' do
25
27
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
28
+
26
29
  it 'succeeds' do
27
30
  last_response.status.must_equal 200
28
31
  end
@@ -58,7 +61,6 @@ describe 'Rack::Attack.Allow2Ban' do
58
61
  key = "rack::attack:allow2ban:ban:1.2.3.4"
59
62
  @cache.store.read(key).must_equal 1
60
63
  end
61
-
62
64
  end
63
65
  end
64
66
  end
@@ -116,6 +118,5 @@ describe 'Rack::Attack.Allow2Ban' do
116
118
  @cache.store.read(key).must_equal 1
117
119
  end
118
120
  end
119
-
120
121
  end
121
122
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'spec_helper'
2
+
2
3
  describe 'Rack::Attack.Fail2Ban' do
3
4
  before do
4
5
  # Use a long findtime; failures due to cache key rotation less likely
@@ -7,6 +8,7 @@ describe 'Rack::Attack.Fail2Ban' do
7
8
  @bantime = 60
8
9
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
9
10
  @f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
11
+
10
12
  Rack::Attack.blocklist('pentest') do |req|
11
13
  Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
12
14
  end
@@ -23,6 +25,7 @@ describe 'Rack::Attack.Fail2Ban' do
23
25
  describe 'making failing request' do
24
26
  describe 'when not at maxretry' do
25
27
  before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
28
+
26
29
  it 'fails' do
27
30
  last_response.status.must_equal 403
28
31
  end
@@ -73,7 +76,7 @@ describe 'Rack::Attack.Fail2Ban' do
73
76
 
74
77
  it 'resets fail count' do
75
78
  key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
76
- @cache.store.read(key).must_equal nil
79
+ assert_nil @cache.store.read(key)
77
80
  end
78
81
 
79
82
  it 'IP is not banned' do
@@ -136,6 +139,5 @@ describe 'Rack::Attack.Fail2Ban' do
136
139
  @cache.store.read(key).must_equal 1
137
140
  end
138
141
  end
139
-
140
142
  end
141
143
  end
@@ -4,7 +4,6 @@ require 'dalli'
4
4
  require_relative '../spec_helper'
5
5
 
6
6
  OfflineExamples = Minitest::SharedExamples.new do
7
-
8
7
  it 'should write' do
9
8
  @cache.write('cache-test-key', 'foobar', 1)
10
9
  end
@@ -16,7 +15,6 @@ OfflineExamples = Minitest::SharedExamples.new do
16
15
  it 'should count' do
17
16
  @cache.send(:do_count, 'rack::attack::cache-test-key', 1)
18
17
  end
19
-
20
18
  end
21
19
 
22
20
  describe 'when Redis is offline' do
@@ -27,7 +25,6 @@ describe 'when Redis is offline' do
27
25
  # Use presumably unused port for Redis client
28
26
  @cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333)
29
27
  }
30
-
31
28
  end
32
29
 
33
30
  describe 'when Memcached is offline' do
@@ -43,5 +40,4 @@ describe 'when Memcached is offline' do
43
40
  after {
44
41
  Dalli.logger.level = Logger::INFO
45
42
  }
46
-
47
43
  end
@@ -1,7 +1,6 @@
1
1
  require_relative '../spec_helper'
2
2
 
3
3
  describe Rack::Attack::Cache do
4
-
5
4
  # A convenience method for deleting a key from cache.
6
5
  # Slightly differnet than @cache.delete, which adds a prefix.
7
6
  def delete(key)
@@ -20,6 +19,7 @@ describe Rack::Attack::Cache do
20
19
  require 'active_support/cache/mem_cache_store'
21
20
  require 'active_support/cache/redis_store'
22
21
  require 'connection_pool'
22
+
23
23
  cache_stores = [
24
24
  ActiveSupport::Cache::MemoryStore.new,
25
25
  ActiveSupport::Cache::DalliStore.new("127.0.0.1"),
@@ -32,8 +32,8 @@ describe Rack::Attack::Cache do
32
32
 
33
33
  cache_stores.each do |store|
34
34
  store = Rack::Attack::StoreProxy.build(store)
35
- describe "with #{store.class}" do
36
35
 
36
+ describe "with #{store.class}" do
37
37
  before {
38
38
  @cache = Rack::Attack::Cache.new
39
39
  @key = "rack::attack:cache-test-key"
@@ -92,7 +92,7 @@ describe Rack::Attack::Cache do
92
92
  store.write(@key, "foobar", :expires_in => @expires_in)
93
93
  @cache.read('cache-test-key').must_equal "foobar"
94
94
  store.delete(@key)
95
- @cache.read('cache-test-key').must_equal nil
95
+ assert_nil @cache.read('cache-test-key')
96
96
  end
97
97
  end
98
98
 
@@ -113,10 +113,9 @@ describe Rack::Attack::Cache do
113
113
  period_key, _ = @cache.send(:key_and_expiry, 'cache-test-key', period)
114
114
  store.read(period_key).to_i.must_equal 1
115
115
  @cache.reset_count(unprefixed_key, period)
116
- store.read(period_key).must_equal nil
116
+ assert_nil store.read(period_key)
117
117
  end
118
118
  end
119
119
  end
120
-
121
120
  end
122
121
  end
@@ -14,6 +14,6 @@ describe 'Rack::Attack' do
14
14
  end
15
15
  end
16
16
 
17
- allow_ok_requests
17
+ it_allows_ok_requests
18
18
  end
19
19
  end
@@ -1,7 +1,7 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  describe 'Rack::Attack' do
4
- allow_ok_requests
4
+ it_allows_ok_requests
5
5
 
6
6
  describe 'normalizing paths' do
7
7
  before do
@@ -33,17 +33,18 @@ describe 'Rack::Attack' do
33
33
 
34
34
  describe "a bad request" do
35
35
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
36
+
36
37
  it "should return a blocklist response" do
37
- get '/', {}, 'REMOTE_ADDR' => @bad_ip
38
38
  last_response.status.must_equal 403
39
39
  last_response.body.must_equal "Forbidden\n"
40
40
  end
41
+
41
42
  it "should tag the env" do
42
43
  last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
43
44
  last_request.env['rack.attack.match_type'].must_equal :blocklist
44
45
  end
45
46
 
46
- allow_ok_requests
47
+ it_allows_ok_requests
47
48
  end
48
49
 
49
50
  describe "and safelist" do
@@ -52,7 +53,7 @@ describe 'Rack::Attack' do
52
53
  Rack::Attack.safelist("good ua") {|req| req.user_agent == @good_ua }
53
54
  end
54
55
 
55
- it('has a safelist'){ Rack::Attack.safelists.key?("good ua") }
56
+ it('has a safelist') { Rack::Attack.safelists.key?("good ua") }
56
57
 
57
58
  it('has a whitelist with a deprication warning') {
58
59
  _, stderror = capture_io do
@@ -63,10 +64,11 @@ describe 'Rack::Attack' do
63
64
 
64
65
  describe "with a request match both safelist & blocklist" do
65
66
  before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
67
+
66
68
  it "should allow safelists before blocklists" do
67
- get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
68
69
  last_response.status.must_equal 200
69
70
  end
71
+
70
72
  it "should tag the env" do
71
73
  last_request.env['rack.attack.matched'].must_equal 'good ua'
72
74
  last_request.env['rack.attack.match_type'].must_equal :safelist
@@ -84,7 +86,6 @@ describe 'Rack::Attack' do
84
86
  Rack::Attack.blacklisted_response
85
87
  end
86
88
  assert_match "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead.", stderror
87
-
88
89
  end
89
90
  end
90
91
 
@@ -93,7 +94,5 @@ describe 'Rack::Attack' do
93
94
  Rack::Attack.throttled_response.must_respond_to :call
94
95
  end
95
96
  end
96
-
97
97
  end
98
-
99
98
  end
@@ -0,0 +1,20 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'Store configuration' do
4
+ it "gives clear error when store it's not configured if it's needed" do
5
+ Rack::Attack.throttle('ip/sec', limit: 1, period: 60) { |req| req.ip }
6
+
7
+ assert_raises(Rack::Attack::MissingStoreError) do
8
+ get '/'
9
+ end
10
+ end
11
+
12
+ it "gives clear error when store isn't configured properly" do
13
+ Rack::Attack.cache.store = Object.new
14
+ Rack::Attack.throttle('ip/sec', limit: 1, period: 60) { |req| req.ip }
15
+
16
+ assert_raises(Rack::Attack::MisconfiguredStoreError) do
17
+ get '/'
18
+ end
19
+ end
20
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'spec_helper'
2
+
2
3
  describe 'Rack::Attack.throttle' do
3
4
  before do
4
5
  @period = 60 # Use a long period; failures due to cache key rotation less likely
@@ -6,11 +7,13 @@ describe 'Rack::Attack.throttle' do
6
7
  Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
7
8
  end
8
9
 
9
- it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
10
- allow_ok_requests
10
+ it('should have a throttle') { Rack::Attack.throttles.key?('ip/sec') }
11
+
12
+ it_allows_ok_requests
11
13
 
12
14
  describe 'a single request' do
13
15
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
16
+
14
17
  it 'should set the counter for one request' do
15
18
  key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
16
19
  Rack::Attack.cache.store.read(key).must_equal 1
@@ -21,19 +24,23 @@ describe 'Rack::Attack.throttle' do
21
24
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
22
25
  end
23
26
  end
27
+
24
28
  describe "with 2 requests" do
25
29
  before do
26
30
  2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
27
31
  end
32
+
28
33
  it 'should block the last request' do
29
34
  last_response.status.must_equal 429
30
35
  end
36
+
31
37
  it 'should tag the env' do
32
38
  last_request.env['rack.attack.matched'].must_equal 'ip/sec'
33
39
  last_request.env['rack.attack.match_type'].must_equal :throttle
34
40
  last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period})
35
41
  last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
36
42
  end
43
+
37
44
  it 'should set a Retry-After header' do
38
45
  last_response.headers['Retry-After'].must_equal @period.to_s
39
46
  end
@@ -47,10 +54,11 @@ describe 'Rack::Attack.throttle with limit as proc' do
47
54
  Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => @period) { |req| req.ip }
48
55
  end
49
56
 
50
- allow_ok_requests
57
+ it_allows_ok_requests
51
58
 
52
59
  describe 'a single request' do
53
60
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
61
+
54
62
  it 'should set the counter for one request' do
55
63
  key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
56
64
  Rack::Attack.cache.store.read(key).must_equal 1
@@ -70,10 +78,11 @@ describe 'Rack::Attack.throttle with period as proc' do
70
78
  Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => lambda { |req| @period }) { |req| req.ip }
71
79
  end
72
80
 
73
- allow_ok_requests
81
+ it_allows_ok_requests
74
82
 
75
83
  describe 'a single request' do
76
84
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
85
+
77
86
  it 'should set the counter for one request' do
78
87
  key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
79
88
  Rack::Attack.cache.store.read(key).must_equal 1
@@ -93,17 +102,18 @@ describe 'Rack::Attack.throttle with block retuning nil' do
93
102
  Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |_| nil }
94
103
  end
95
104
 
96
- allow_ok_requests
105
+ it_allows_ok_requests
97
106
 
98
107
  describe 'a single request' do
99
108
  before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
109
+
100
110
  it 'should not set the counter' do
101
111
  key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
102
- Rack::Attack.cache.store.read(key).must_equal nil
112
+ assert_nil Rack::Attack.cache.store.read(key)
103
113
  end
104
114
 
105
115
  it 'should not populate throttle data' do
106
- last_request.env['rack.attack.throttle_data'].must_equal nil
116
+ assert_nil last_request.env['rack.attack.throttle_data']
107
117
  end
108
118
  end
109
- end
119
+ end
@@ -18,7 +18,9 @@ describe 'Rack::Attack.track' do
18
18
  before do
19
19
  Rack::Attack.track("everything"){ |req| true }
20
20
  end
21
- allow_ok_requests
21
+
22
+ it_allows_ok_requests
23
+
22
24
  it "should tag the env" do
23
25
  get '/'
24
26
  last_request.env['rack.attack.matched'].must_equal 'everything'
@@ -34,6 +36,7 @@ describe 'Rack::Attack.track' do
34
36
  ActiveSupport::Notifications.subscribe("rack.attack") do |*args|
35
37
  Counter.incr
36
38
  end
39
+
37
40
  get "/"
38
41
  end
39
42
 
@@ -7,9 +7,6 @@ require "rack/test"
7
7
  require 'active_support'
8
8
  require 'action_dispatch'
9
9
 
10
- # Load Journey for Rails 3.2
11
- require 'journey' if ActionPack::VERSION::MAJOR == 3
12
-
13
10
  require "rack/attack"
14
11
 
15
12
  begin
@@ -22,16 +19,23 @@ class MiniTest::Spec
22
19
 
23
20
  include Rack::Test::Methods
24
21
 
25
- after { Rack::Attack.clear! }
22
+ after do
23
+ Rack::Attack.clear!
24
+ Rack::Attack.instance_variable_set(:@cache, nil)
25
+ end
26
26
 
27
27
  def app
28
28
  Rack::Builder.new {
29
+ # Use Rack::Lint to test that rack-attack is complying with the rack spec
30
+ use Rack::Lint
29
31
  use Rack::Attack
32
+ use Rack::Lint
33
+
30
34
  run lambda {|env| [200, {}, ['Hello World']]}
31
35
  }.to_app
32
36
  end
33
37
 
34
- def self.allow_ok_requests
38
+ def self.it_allows_ok_requests
35
39
  it "must allow ok requests" do
36
40
  get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
37
41
  last_response.status.must_equal 200
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-attack
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.1.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: 2016-08-11 00:00:00.000000000 Z
11
+ date: 2018-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -164,6 +164,62 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: timecop
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: pry
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: guard-minitest
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: guard
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
167
223
  description: A rack middleware for throttling and blocking abusive requests
168
224
  email: aaron@ktheory.com
169
225
  executables: []
@@ -188,6 +244,9 @@ files:
188
244
  - lib/rack/attack/throttle.rb
189
245
  - lib/rack/attack/track.rb
190
246
  - lib/rack/attack/version.rb
247
+ - spec/acceptance/blocking_spec.rb
248
+ - spec/acceptance/safelisting_spec.rb
249
+ - spec/acceptance/throttling_spec.rb
191
250
  - spec/allow2ban_spec.rb
192
251
  - spec/fail2ban_spec.rb
193
252
  - spec/integration/offline_spec.rb
@@ -196,6 +255,7 @@ files:
196
255
  - spec/rack_attack_path_normalizer_spec.rb
197
256
  - spec/rack_attack_request_spec.rb
198
257
  - spec/rack_attack_spec.rb
258
+ - spec/rack_attack_store_config_spec.rb
199
259
  - spec/rack_attack_throttle_spec.rb
200
260
  - spec/rack_attack_track_spec.rb
201
261
  - spec/spec_helper.rb
@@ -220,19 +280,23 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
280
  version: '0'
221
281
  requirements: []
222
282
  rubyforge_project:
223
- rubygems_version: 2.5.1
283
+ rubygems_version: 2.7.3
224
284
  signing_key:
225
285
  specification_version: 4
226
286
  summary: Block & throttle abusive requests
227
287
  test_files:
228
- - spec/allow2ban_spec.rb
229
- - spec/fail2ban_spec.rb
288
+ - spec/spec_helper.rb
289
+ - spec/rack_attack_store_config_spec.rb
290
+ - spec/rack_attack_throttle_spec.rb
291
+ - spec/rack_attack_spec.rb
230
292
  - spec/integration/offline_spec.rb
231
293
  - spec/integration/rack_attack_cache_spec.rb
294
+ - spec/rack_attack_track_spec.rb
295
+ - spec/fail2ban_spec.rb
232
296
  - spec/rack_attack_dalli_proxy_spec.rb
233
297
  - spec/rack_attack_path_normalizer_spec.rb
298
+ - spec/acceptance/throttling_spec.rb
299
+ - spec/acceptance/blocking_spec.rb
300
+ - spec/acceptance/safelisting_spec.rb
234
301
  - spec/rack_attack_request_spec.rb
235
- - spec/rack_attack_spec.rb
236
- - spec/rack_attack_throttle_spec.rb
237
- - spec/rack_attack_track_spec.rb
238
- - spec/spec_helper.rb
302
+ - spec/allow2ban_spec.rb