rack-attack 5.0.1 → 5.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 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