rack-attack 5.2.0 → 5.3.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -24
  3. data/Rakefile +1 -1
  4. data/lib/rack/attack.rb +28 -23
  5. data/lib/rack/attack/allow2ban.rb +1 -0
  6. data/lib/rack/attack/blocklist.rb +0 -1
  7. data/lib/rack/attack/cache.rb +1 -2
  8. data/lib/rack/attack/check.rb +1 -2
  9. data/lib/rack/attack/fail2ban.rb +2 -1
  10. data/lib/rack/attack/path_normalizer.rb +6 -8
  11. data/lib/rack/attack/safelist.rb +0 -1
  12. data/lib/rack/attack/store_proxy.rb +2 -4
  13. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  14. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
  15. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +30 -0
  16. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +4 -11
  17. data/lib/rack/attack/version.rb +1 -1
  18. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +2 -2
  19. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +2 -2
  20. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +1 -1
  21. data/spec/acceptance/customizing_blocked_response_spec.rb +1 -1
  22. data/spec/acceptance/customizing_throttled_response_spec.rb +1 -1
  23. data/spec/acceptance/safelisting_ip_spec.rb +0 -1
  24. data/spec/acceptance/stores/mem_cache_store_spec.rb +38 -0
  25. data/spec/acceptance/stores/redis_cache_store_spec.rb +41 -0
  26. data/spec/allow2ban_spec.rb +6 -6
  27. data/spec/fail2ban_spec.rb +7 -7
  28. data/spec/integration/rack_attack_cache_spec.rb +4 -1
  29. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  30. data/spec/rack_attack_spec.rb +6 -6
  31. data/spec/rack_attack_throttle_spec.rb +7 -7
  32. data/spec/rack_attack_track_spec.rb +5 -5
  33. data/spec/spec_helper.rb +3 -4
  34. data/spec/support/cache_store_helper.rb +58 -0
  35. metadata +65 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21a52aca7aa6592b23e6a3e99f0b06cf6d4d9eedb8366ec57fe4f9cfe804ea82
4
- data.tar.gz: 32e0db149bc10308fb8b5ae737147e1c11c9f854e08b9f453d9a7066911308fe
3
+ metadata.gz: c8f4c760b1ff68e6fbe79cf7576cbe2e0cc79ef5e2734ff55e63b46665ed6731
4
+ data.tar.gz: 75d7dd7fc98035d3961952582373af467fcfd225dac11af8db4aaf1cbaaf44b8
5
5
  SHA512:
6
- metadata.gz: 560d951d375a9114752b37a2858f48e85af9edcdeabb0073c6e6f8d179b6d10a0331a7abee625df4c0171f9c993bb105cc8e54fed9170fd47d288fb2bf29617a
7
- data.tar.gz: b3993951744e2755873cc7ce0c6810c3780ad44c6c84491bda5277a46536589d15b26bd25e7849e7b20a4e28aebc86ed874cb0b61e0d988da54252a3b3e024dc
6
+ metadata.gz: 53ecb94578ea2497b660ca508b572b8a1f9a5a40e986b4bc6f834e05f90ea49eb4e721aefe4bd431b26754aeb33440fa4b43cc204705c035e61ada8e79d543dc
7
+ data.tar.gz: 22dbd120cdc634115f7096d5e9e4a44cc4ec97ae5dd11fc746b0e21e6b2ad860bb8bbd62c0bf7b7ee0f63d5803c568579b5569d3f849159751b25c236ede1520
data/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to *allow*, *block* and *throttle* based on properties of the request.
6
6
 
7
- See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
7
+ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
8
8
 
9
- [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](http://badge.fury.io/rb/rack-attack)
9
+ [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
10
10
  [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
11
11
  [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
12
12
 
@@ -133,7 +133,7 @@ Rack::Attack.blocklist_ip("1.2.0.0/16")
133
133
 
134
134
  #### `blocklist(name, &block)`
135
135
 
136
- Name your custom blocklist and make your ruby-block argument returna a truthy value if you want the request to be blocked, and falsy otherwise.
136
+ Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
137
137
 
138
138
  The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
139
139
 
@@ -155,8 +155,8 @@ end
155
155
  #### Fail2Ban
156
156
 
157
157
  `Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
158
- This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
159
- See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
158
+ This pattern is inspired by [fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page).
159
+ See the [fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
160
160
  how the parameters work. For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.
161
161
 
162
162
  Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
@@ -272,7 +272,7 @@ Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2b
272
272
 
273
273
  ## Customizing responses
274
274
 
275
- Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
275
+ Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://www.rubydoc.info/github/rack/rack/file/SPEC).
276
276
 
277
277
  ```ruby
278
278
  Rack::Attack.blocklisted_response = lambda do |env|
@@ -388,7 +388,7 @@ so try to keep the number of throttle checks per request low.
388
388
  If a request is blocklisted or throttled, the response is a very simple Rack response.
389
389
  A single typical ruby web server thread can block several hundred requests per second.
390
390
 
391
- Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
391
+ Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
392
392
 
393
393
  ## Motivation
394
394
 
@@ -402,26 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
402
402
 
403
403
  ## Contributing
404
404
 
405
- Pull requests and issues are greatly appreciated. This project is intended to be
406
- a safe, welcoming space for collaboration, and contributors are expected to
407
- adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
405
+ Check out the [Contributing guide](CONTRIBUTING.md).
408
406
 
409
- ### Testing pull requests
407
+ ## Code of Conduct
410
408
 
411
- To run the minitest test suite, you will need both [Redis](http://redis.io/) and
412
- [Memcached](https://memcached.org/) running locally and bound to IP `127.0.0.1` on
413
- default ports (`6379` for Redis, and `11211` for Memcached) and able to be
414
- accessed without authentication.
409
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
415
410
 
416
- Install dependencies by running
417
- ```sh
418
- bundle install
419
- ```
411
+ ## Development setup
420
412
 
421
- Then run the test suite by running
422
- ```sh
423
- bundle exec rake
424
- ```
413
+ Check out the [Development guide](docs/development.md).
425
414
 
426
415
  ## Mailing list
427
416
 
@@ -434,4 +423,4 @@ New releases of Rack::Attack are announced on
434
423
 
435
424
  Copyright Kickstarter, PBC.
436
425
 
437
- Released under an [MIT License](http://opensource.org/licenses/MIT).
426
+ Released under an [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ namespace :test do
13
13
  end
14
14
 
15
15
  Rake::TestTask.new(:acceptance) do |t|
16
- t.pattern = "spec/acceptance/*_spec.rb"
16
+ t.pattern = "spec/acceptance/**/*_spec.rb"
17
17
  end
18
18
  end
19
19
 
@@ -8,21 +8,21 @@ class Rack::Attack
8
8
  class MisconfiguredStoreError < StandardError; end
9
9
  class MissingStoreError < StandardError; end
10
10
 
11
- autoload :Cache, 'rack/attack/cache'
12
- autoload :Check, 'rack/attack/check'
13
- autoload :Throttle, 'rack/attack/throttle'
14
- autoload :Safelist, 'rack/attack/safelist'
15
- autoload :Blocklist, 'rack/attack/blocklist'
16
- autoload :Track, 'rack/attack/track'
17
- autoload :StoreProxy, 'rack/attack/store_proxy'
18
- autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
19
- autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
20
- autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
21
- autoload :Fail2Ban, 'rack/attack/fail2ban'
22
- autoload :Allow2Ban, 'rack/attack/allow2ban'
11
+ autoload :Cache, 'rack/attack/cache'
12
+ autoload :Check, 'rack/attack/check'
13
+ autoload :Throttle, 'rack/attack/throttle'
14
+ autoload :Safelist, 'rack/attack/safelist'
15
+ autoload :Blocklist, 'rack/attack/blocklist'
16
+ autoload :Track, 'rack/attack/track'
17
+ autoload :StoreProxy, 'rack/attack/store_proxy'
18
+ autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
19
+ autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
20
+ autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
21
+ autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
22
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
23
+ autoload :Allow2Ban, 'rack/attack/allow2ban'
23
24
 
24
25
  class << self
25
-
26
26
  attr_accessor :notifier, :blocklisted_response, :throttled_response
27
27
 
28
28
  def safelist(name, &block)
@@ -64,8 +64,11 @@ class Rack::Attack
64
64
  end
65
65
 
66
66
  def safelists; @safelists ||= {}; end
67
+
67
68
  def blocklists; @blocklists ||= {}; end
69
+
68
70
  def throttles; @throttles ||= {}; end
71
+
69
72
  def tracks; @tracks ||= {}; end
70
73
 
71
74
  def whitelists
@@ -99,7 +102,7 @@ class Rack::Attack
99
102
  end
100
103
 
101
104
  def throttled?(req)
102
- throttles.any? do |name, throttle|
105
+ throttles.any? do |_name, throttle|
103
106
  throttle[req]
104
107
  end
105
108
  end
@@ -118,15 +121,20 @@ class Rack::Attack
118
121
  @cache ||= Cache.new
119
122
  end
120
123
 
121
- def clear!
124
+ def clear_configuration
122
125
  @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
123
126
  @ip_blocklists = []
124
127
  @ip_safelists = []
125
128
  end
126
129
 
130
+ def clear!
131
+ warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
132
+ clear_configuration
133
+ end
134
+
127
135
  def blacklisted_response=(res)
128
136
  warn "[DEPRECATION] 'Rack::Attack.blacklisted_response=' is deprecated. Please use 'blocklisted_response=' instead."
129
- self.blocklisted_response=(res)
137
+ self.blocklisted_response = res
130
138
  end
131
139
 
132
140
  def blacklisted_response
@@ -147,10 +155,10 @@ class Rack::Attack
147
155
 
148
156
  # Set defaults
149
157
  @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
150
- @blocklisted_response = lambda {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
151
- @throttled_response = lambda {|env|
158
+ @blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
159
+ @throttled_response = lambda { |env|
152
160
  retry_after = (env['rack.attack.match_data'] || {})[:period]
153
- [429, {'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
161
+ [429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
154
162
  }
155
163
 
156
164
  def initialize(app)
@@ -174,8 +182,5 @@ class Rack::Attack
174
182
  end
175
183
 
176
184
  extend Forwardable
177
- def_delegators self, :safelisted?,
178
- :blocklisted?,
179
- :throttled?,
180
- :tracked?
185
+ def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
181
186
  end
@@ -3,6 +3,7 @@ module Rack
3
3
  class Allow2Ban < Fail2Ban
4
4
  class << self
5
5
  protected
6
+
6
7
  def key_prefix
7
8
  'allow2ban'
8
9
  end
@@ -5,7 +5,6 @@ module Rack
5
5
  super
6
6
  @type = :blocklist
7
7
  end
8
-
9
8
  end
10
9
  end
11
10
  end
@@ -1,7 +1,6 @@
1
1
  module Rack
2
2
  class Attack
3
3
  class Cache
4
-
5
4
  attr_accessor :prefix
6
5
 
7
6
  def initialize
@@ -43,7 +42,7 @@ module Rack
43
42
 
44
43
  def key_and_expiry(unprefixed_key, period)
45
44
  epoch_time = Time.now.to_i
46
- # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA
45
+ # Add 1 to expires_in to avoid timing error: https://git.io/i1PHXA
47
46
  expires_in = (period - (epoch_time % period) + 1).to_i
48
47
  ["#{prefix}:#{(epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
49
48
  end
@@ -8,7 +8,7 @@ module Rack
8
8
  end
9
9
 
10
10
  def [](req)
11
- block[req].tap {|match|
11
+ block[req].tap { |match|
12
12
  if match
13
13
  req.env["rack.attack.matched"] = name
14
14
  req.env["rack.attack.match_type"] = type
@@ -21,4 +21,3 @@ module Rack
21
21
  end
22
22
  end
23
23
  end
24
-
@@ -27,6 +27,7 @@ module Rack
27
27
  end
28
28
 
29
29
  protected
30
+
30
31
  def key_prefix
31
32
  'fail2ban'
32
33
  end
@@ -40,8 +41,8 @@ module Rack
40
41
  true
41
42
  end
42
43
 
43
-
44
44
  private
45
+
45
46
  def ban!(discriminator, bantime)
46
47
  cache.write("#{key_prefix}:ban:#{discriminator}", 1, bantime)
47
48
  end
@@ -1,8 +1,7 @@
1
1
  class Rack::Attack
2
-
3
2
  # When using Rack::Attack with a Rails app, developers expect the request path
4
3
  # to be normalized. In particular, trailing slashes are stripped.
5
- # (See http://git.io/v0rrR for implementation.)
4
+ # (See https://git.io/v0rrR for implementation.)
6
5
  #
7
6
  # Look for an ActionDispatch utility class that Rails folks would expect
8
7
  # to normalize request paths. If unavailable, use a fallback class that
@@ -15,10 +14,9 @@ class Rack::Attack
15
14
  end
16
15
 
17
16
  PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
18
- # For Rails apps
19
- ::ActionDispatch::Journey::Router::Utils
20
- else
21
- FallbackPathNormalizer
22
- end
23
-
17
+ # For Rails apps
18
+ ::ActionDispatch::Journey::Router::Utils
19
+ else
20
+ FallbackPathNormalizer
21
+ end
24
22
  end
@@ -5,7 +5,6 @@ module Rack
5
5
  super
6
6
  @type = :safelist
7
7
  end
8
-
9
8
  end
10
9
  end
11
10
  end
@@ -1,9 +1,9 @@
1
1
  module Rack
2
2
  class Attack
3
3
  module StoreProxy
4
- PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy].freeze
4
+ PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisCacheStoreProxy].freeze
5
5
 
6
- ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore']).freeze
6
+ ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore']).freeze
7
7
  ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
8
8
 
9
9
  def self.build(store)
@@ -12,8 +12,6 @@ module Rack
12
12
  klass ? klass.new(client) : client
13
13
  end
14
14
 
15
-
16
- private
17
15
  def self.unwrap_active_support_stores(store)
18
16
  # ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
19
17
  # so use the raw Redis::Store instead.
@@ -28,14 +28,14 @@ module Rack
28
28
  rescue Dalli::DalliError
29
29
  end
30
30
 
31
- def write(key, value, options={})
31
+ def write(key, value, options = {})
32
32
  with do |client|
33
33
  client.set(key, value, options.fetch(:expires_in, 0), raw: true)
34
34
  end
35
35
  rescue Dalli::DalliError
36
36
  end
37
37
 
38
- def increment(key, amount, options={})
38
+ def increment(key, amount, options = {})
39
39
  with do |client|
40
40
  client.incr(key, amount, options.fetch(:expires_in, 0), amount)
41
41
  end
@@ -58,7 +58,6 @@ module Rack
58
58
  end
59
59
  end
60
60
  end
61
-
62
61
  end
63
62
  end
64
63
  end
@@ -14,21 +14,21 @@ module Rack
14
14
  def read(key)
15
15
  # Second argument: reading raw value
16
16
  get(key, true)
17
- rescue MemCache::MemCacheError
17
+ rescue MemCache::MemCacheError
18
18
  end
19
19
 
20
- def write(key, value, options={})
20
+ def write(key, value, options = {})
21
21
  # Third argument: writing raw value
22
22
  set(key, value, options.fetch(:expires_in, 0), true)
23
23
  rescue MemCache::MemCacheError
24
24
  end
25
25
 
26
- def increment(key, amount, options={})
26
+ def increment(key, amount, _options = {})
27
27
  incr(key, amount)
28
28
  rescue MemCache::MemCacheError
29
29
  end
30
30
 
31
- def delete(key, options={})
31
+ def delete(key, _options = {})
32
32
  with do |client|
33
33
  client.delete(key)
34
34
  end
@@ -44,7 +44,6 @@ module Rack
44
44
  end
45
45
  end
46
46
  end
47
-
48
47
  end
49
48
  end
50
49
  end
@@ -0,0 +1,30 @@
1
+ require 'delegate'
2
+
3
+ module Rack
4
+ class Attack
5
+ module StoreProxy
6
+ class RedisCacheStoreProxy < SimpleDelegator
7
+ def self.handle?(store)
8
+ defined?(::ActiveSupport::Cache::RedisCacheStore) && store.is_a?(::ActiveSupport::Cache::RedisCacheStore)
9
+ end
10
+
11
+ def increment(name, amount, options = {})
12
+ # Redis doesn't check expiration on the INCRBY command. See https://redis.io/commands/expire
13
+ count = redis.pipelined do
14
+ redis.incrby(name, amount)
15
+ redis.expire(name, options[:expires_in]) if options[:expires_in]
16
+ end
17
+ count.first
18
+ end
19
+
20
+ def read(name, options = {})
21
+ super(name, options.merge!({ raw: true }))
22
+ end
23
+
24
+ def write(name, value, options = {})
25
+ super(name, value, options.merge!({ raw: true }))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -5,14 +5,7 @@ module Rack
5
5
  module StoreProxy
6
6
  class RedisStoreProxy < SimpleDelegator
7
7
  def self.handle?(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)
8
+ defined?(::Redis::Store) && store.is_a?(::Redis::Store)
16
9
  end
17
10
 
18
11
  def initialize(store)
@@ -24,7 +17,7 @@ module Rack
24
17
  rescue Redis::BaseError
25
18
  end
26
19
 
27
- def write(key, value, options={})
20
+ def write(key, value, options = {})
28
21
  if (expires_in = options[:expires_in])
29
22
  setex(key, expires_in, value, raw: true)
30
23
  else
@@ -33,7 +26,7 @@ module Rack
33
26
  rescue Redis::BaseError
34
27
  end
35
28
 
36
- def increment(key, amount, options={})
29
+ def increment(key, amount, options = {})
37
30
  count = nil
38
31
 
39
32
  pipelined do
@@ -45,7 +38,7 @@ module Rack
45
38
  rescue Redis::BaseError
46
39
  end
47
40
 
48
- def delete(key, options={})
41
+ def delete(key, _options = {})
49
42
  del(key)
50
43
  rescue Redis::BaseError
51
44
  end