rack-attack 6.2.1 → 6.5.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
2
  SHA256:
3
- metadata.gz: 55b137d5d1174ac14bcbc494ba7c6753652c23348fef73c5ac712884e299a8d0
4
- data.tar.gz: 9aae493ec090c669ea43f7a5f9bc8a10e6786266477a4dd65b05495c7559e469
3
+ metadata.gz: 81f6cce465b8441782ffaa3d446e558979de6a2ceadd9d43499b7977fa61586b
4
+ data.tar.gz: a2ee9b82f1144d483e7d26a893594ab6cada4dc52d98eb08d7615a38b49face8
5
5
  SHA512:
6
- metadata.gz: 2e283d8c8b2e5ffcd99435561230c6019a7a1f9637927d4ca29e0df2c9af9522cb84e60dddd040ab3429b1cc697213d80adb4a9b4cb7587933060ad6e426fabd
7
- data.tar.gz: 1ef81c28f633e8c146a54a0930c706a3158aab568328743e0a3b0953bcbcba0bfb4a32a3e589ea1292c001ec6fd8d52a9c6de3cc4063b466045b692e92d27318
6
+ metadata.gz: b90fa7841d1e5319b820d4bca9c1fc8d266fc98c06f2a936ce6d31949d9fdd7ddab9183f82c5bbbc31e3e4280bba8e786bbe155f146fdcfdbf53ad263bfec63f
7
+ data.tar.gz: c91be495fbf25444632a1734f40f9fa85b35bf19d921a129ca402aceadf0d30b2a7d5797283ca7ac68e8f323569fa3edc1be98bcecd2c72aa15c3956dd611455
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  __Note__: You are viewing the development version README.
2
- For the README consistent with the latest released version see https://github.com/kickstarter/rack-attack/blob/6-stable/README.md.
2
+ For the README consistent with the latest released version see https://github.com/rack/rack-attack/blob/6-stable/README.md.
3
3
 
4
4
  # Rack::Attack
5
5
 
@@ -10,7 +10,7 @@ Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily
10
10
  See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
11
11
 
12
12
  [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
13
- [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
13
+ [![Build Status](https://travis-ci.org/rack/rack-attack.svg?branch=master)](https://travis-ci.org/rack/rack-attack)
14
14
  [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
15
15
  [![Join the chat at https://gitter.im/rack-attack/rack-attack](https://badges.gitter.im/rack-attack/rack-attack.svg)](https://gitter.im/rack-attack/rack-attack)
16
16
 
@@ -37,9 +37,9 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
37
37
  - [Customizing responses](#customizing-responses)
38
38
  - [RateLimit headers for well-behaved clients](#ratelimit-headers-for-well-behaved-clients)
39
39
  - [Logging & Instrumentation](#logging--instrumentation)
40
+ - [Testing](#testing)
40
41
  - [How it works](#how-it-works)
41
42
  - [About Tracks](#about-tracks)
42
- - [Testing](#testing)
43
43
  - [Performance](#performance)
44
44
  - [Motivation](#motivation)
45
45
  - [Contributing](#contributing)
@@ -71,12 +71,7 @@ Or install it yourself as:
71
71
 
72
72
  Then tell your ruby web application to use rack-attack as a middleware.
73
73
 
74
- a) For __rails__ applications with versions >= 5.1 it is used by default. For older rails versions you should enable it explicitly:
75
- ```ruby
76
- # In config/application.rb
77
-
78
- config.middleware.use Rack::Attack
79
- ```
74
+ a) For __rails__ applications it is used by default.
80
75
 
81
76
  You can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing:
82
77
 
@@ -140,7 +135,7 @@ E.g.
140
135
  # Provided that trusted users use an HTTP request header named APIKey
141
136
  Rack::Attack.safelist("mark any authenticated access safe") do |request|
142
137
  # Requests are allowed if the return value is truthy
143
- request.env["APIKey"] == "secret-string"
138
+ request.env["HTTP_APIKEY"] == "secret-string"
144
139
  end
145
140
 
146
141
  # Always allow requests from localhost
@@ -263,10 +258,12 @@ Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
263
258
  end
264
259
 
265
260
  # Throttle login attempts for a given email parameter to 6 reqs/minute
266
- # Return the email as a discriminator on POST /login requests
261
+ # Return the *normalized* email as a discriminator on POST /login requests
267
262
  Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
268
263
  if req.path == '/login' && req.post?
269
- req.params['email']
264
+ # Normalize the email, using the same logic as your authentication process, to
265
+ # protect against rate limit bypasses.
266
+ req.params['email'].to_s.downcase.gsub(/\s+/, "")
270
267
  end
271
268
  end
272
269
 
@@ -342,6 +339,11 @@ end
342
339
  While Rack::Attack's primary focus is minimizing harm from abusive clients, it
343
340
  can also be used to return rate limit data that's helpful for well-behaved clients.
344
341
 
342
+ If you want to return to user how many seconds to wait until they can start sending requests again, this can be done through enabling `Retry-After` header:
343
+ ```ruby
344
+ Rack::Attack.throttled_response_retry_after_header = true
345
+ ```
346
+
345
347
  Here's an example response that includes conventional `RateLimit-*` headers:
346
348
 
347
349
  ```ruby
@@ -372,7 +374,7 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
372
374
 
373
375
  You can subscribe to `rack_attack` events and log it, graph it, etc.
374
376
 
375
- To get notified about specific type of events, subscribe to the event name followed by the `rack_attack` namesapce.
377
+ To get notified about specific type of events, subscribe to the event name followed by the `rack_attack` namespace.
376
378
  E.g. for throttles use:
377
379
 
378
380
  ```ruby
@@ -393,6 +395,20 @@ ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, r
393
395
  end
394
396
  ```
395
397
 
398
+ ## Testing
399
+
400
+ A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
401
+ need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
402
+ for more on how to do this.
403
+
404
+ ### Disabling
405
+
406
+ `Rack::Attack.enabled = false` can be used to either completely disable Rack::Attack in your tests, or to disable/enable for specific test cases only.
407
+
408
+ ### Test case isolation
409
+
410
+ `Rack::Attack.reset!` can be used in your test suite to clear any Rack::Attack state between different test cases.
411
+
396
412
  ## How it works
397
413
 
398
414
  The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
@@ -429,13 +445,6 @@ can cleanly monkey patch helper methods onto the
429
445
 
430
446
  `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
431
447
 
432
-
433
- ## Testing
434
-
435
- A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
436
- need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
437
- for more on how to do this.
438
-
439
448
  ## Performance
440
449
 
441
450
  The overhead of running Rack::Attack is typically negligible (a few milliseconds per request),
data/lib/rack/attack.rb CHANGED
@@ -2,9 +2,16 @@
2
2
 
3
3
  require 'rack'
4
4
  require 'forwardable'
5
+ require 'rack/attack/cache'
6
+ require 'rack/attack/configuration'
5
7
  require 'rack/attack/path_normalizer'
6
8
  require 'rack/attack/request'
7
- require "ipaddr"
9
+ require 'rack/attack/store_proxy/dalli_proxy'
10
+ require 'rack/attack/store_proxy/mem_cache_store_proxy'
11
+ require 'rack/attack/store_proxy/redis_proxy'
12
+ require 'rack/attack/store_proxy/redis_store_proxy'
13
+ require 'rack/attack/store_proxy/redis_cache_store_proxy'
14
+ require 'rack/attack/store_proxy/active_support_redis_store_proxy'
8
15
 
9
16
  require 'rack/attack/railtie' if defined?(::Rails)
10
17
 
@@ -13,100 +20,19 @@ module Rack
13
20
  class Error < StandardError; end
14
21
  class MisconfiguredStoreError < Error; end
15
22
  class MissingStoreError < Error; end
23
+ class IncompatibleStoreError < Error; end
16
24
 
17
- autoload :Cache, 'rack/attack/cache'
18
25
  autoload :Check, 'rack/attack/check'
19
26
  autoload :Throttle, 'rack/attack/throttle'
20
27
  autoload :Safelist, 'rack/attack/safelist'
21
28
  autoload :Blocklist, 'rack/attack/blocklist'
22
29
  autoload :Track, 'rack/attack/track'
23
- autoload :StoreProxy, 'rack/attack/store_proxy'
24
- autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
25
- autoload :MemCacheStoreProxy, 'rack/attack/store_proxy/mem_cache_store_proxy'
26
- autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
27
- autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
28
- autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
29
- autoload :ActiveSupportRedisStoreProxy, 'rack/attack/store_proxy/active_support_redis_store_proxy'
30
30
  autoload :Fail2Ban, 'rack/attack/fail2ban'
31
31
  autoload :Allow2Ban, 'rack/attack/allow2ban'
32
32
 
33
33
  class << self
34
- attr_accessor :enabled, :notifier, :blocklisted_response, :throttled_response,
35
- :anonymous_blocklists, :anonymous_safelists
36
-
37
- def safelist(name = nil, &block)
38
- safelist = Safelist.new(name, &block)
39
-
40
- if name
41
- safelists[name] = safelist
42
- else
43
- anonymous_safelists << safelist
44
- end
45
- end
46
-
47
- def blocklist(name = nil, &block)
48
- blocklist = Blocklist.new(name, &block)
49
-
50
- if name
51
- blocklists[name] = blocklist
52
- else
53
- anonymous_blocklists << blocklist
54
- end
55
- end
56
-
57
- def blocklist_ip(ip_address)
58
- anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
59
- end
60
-
61
- def safelist_ip(ip_address)
62
- anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
63
- end
64
-
65
- def throttle(name, options, &block)
66
- throttles[name] = Throttle.new(name, options, &block)
67
- end
68
-
69
- def track(name, options = {}, &block)
70
- tracks[name] = Track.new(name, options, &block)
71
- end
72
-
73
- def safelists
74
- @safelists ||= {}
75
- end
76
-
77
- def blocklists
78
- @blocklists ||= {}
79
- end
80
-
81
- def throttles
82
- @throttles ||= {}
83
- end
84
-
85
- def tracks
86
- @tracks ||= {}
87
- end
88
-
89
- def safelisted?(request)
90
- anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
91
- safelists.any? { |_name, safelist| safelist.matched_by?(request) }
92
- end
93
-
94
- def blocklisted?(request)
95
- anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
96
- blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
97
- end
98
-
99
- def throttled?(request)
100
- throttles.any? do |_name, throttle|
101
- throttle.matched_by?(request)
102
- end
103
- end
104
-
105
- def tracked?(request)
106
- tracks.each_value do |track|
107
- track.matched_by?(request)
108
- end
109
- end
34
+ attr_accessor :enabled, :notifier, :throttle_discriminator_normalizer
35
+ attr_reader :configuration
110
36
 
111
37
  def instrument(request)
112
38
  if notifier
@@ -122,34 +48,51 @@ module Rack
122
48
  @cache ||= Cache.new
123
49
  end
124
50
 
125
- def clear_configuration
126
- @safelists = {}
127
- @blocklists = {}
128
- @throttles = {}
129
- @tracks = {}
130
- self.anonymous_blocklists = []
131
- self.anonymous_safelists = []
132
- end
133
-
134
51
  def clear!
135
52
  warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
136
- clear_configuration
137
- end
53
+ @configuration.clear_configuration
54
+ end
55
+
56
+ def reset!
57
+ cache.reset!
58
+ end
59
+
60
+ extend Forwardable
61
+ def_delegators(
62
+ :@configuration,
63
+ :safelist,
64
+ :blocklist,
65
+ :blocklist_ip,
66
+ :safelist_ip,
67
+ :throttle,
68
+ :track,
69
+ :blocklisted_response,
70
+ :blocklisted_response=,
71
+ :throttled_response,
72
+ :throttled_response=,
73
+ :throttled_response_retry_after_header,
74
+ :throttled_response_retry_after_header=,
75
+ :clear_configuration,
76
+ :safelists,
77
+ :blocklists,
78
+ :throttles,
79
+ :tracks
80
+ )
138
81
  end
139
82
 
140
83
  # Set defaults
141
84
  @enabled = true
142
- @anonymous_blocklists = []
143
- @anonymous_safelists = []
144
85
  @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
145
- @blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
146
- @throttled_response = lambda do |env|
147
- retry_after = (env['rack.attack.match_data'] || {})[:period]
148
- [429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
86
+ @throttle_discriminator_normalizer = lambda do |discriminator|
87
+ discriminator.to_s.strip.downcase
149
88
  end
89
+ @configuration = Configuration.new
90
+
91
+ attr_reader :configuration
150
92
 
151
93
  def initialize(app)
152
94
  @app = app
95
+ @configuration = self.class.configuration
153
96
  end
154
97
 
155
98
  def call(env)
@@ -159,19 +102,16 @@ module Rack
159
102
  env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
160
103
  request = Rack::Attack::Request.new(env)
161
104
 
162
- if safelisted?(request)
105
+ if configuration.safelisted?(request)
163
106
  @app.call(env)
164
- elsif blocklisted?(request)
165
- self.class.blocklisted_response.call(env)
166
- elsif throttled?(request)
167
- self.class.throttled_response.call(env)
107
+ elsif configuration.blocklisted?(request)
108
+ configuration.blocklisted_response.call(env)
109
+ elsif configuration.throttled?(request)
110
+ configuration.throttled_response.call(env)
168
111
  else
169
- tracked?(request)
112
+ configuration.tracked?(request)
170
113
  @app.call(env)
171
114
  end
172
115
  end
173
-
174
- extend Forwardable
175
- def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
176
116
  end
177
117
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Rack
6
+ class Attack
7
+ class BaseProxy < SimpleDelegator
8
+ class << self
9
+ def proxies
10
+ @@proxies ||= []
11
+ end
12
+
13
+ def inherited(klass)
14
+ proxies << klass
15
+ end
16
+
17
+ def lookup(store)
18
+ proxies.find { |proxy| proxy.handle?(store) }
19
+ end
20
+
21
+ def handle?(_store)
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -12,8 +12,14 @@ module Rack
12
12
  end
13
13
 
14
14
  attr_reader :store
15
+
15
16
  def store=(store)
16
- @store = StoreProxy.build(store)
17
+ @store =
18
+ if (proxy = BaseProxy.lookup(store))
19
+ proxy.new(store)
20
+ else
21
+ store
22
+ end
17
23
  end
18
24
 
19
25
  def count(unprefixed_key, period)
@@ -41,6 +47,17 @@ module Rack
41
47
  store.delete("#{prefix}:#{unprefixed_key}")
42
48
  end
43
49
 
50
+ def reset!
51
+ if store.respond_to?(:delete_matched)
52
+ store.delete_matched("#{prefix}*")
53
+ else
54
+ raise(
55
+ Rack::Attack::IncompatibleStoreError,
56
+ "Configured store #{store.class.name} doesn't respond to #delete_matched method"
57
+ )
58
+ end
59
+ end
60
+
44
61
  private
45
62
 
46
63
  def key_and_expiry(unprefixed_key, period)
@@ -4,6 +4,7 @@ module Rack
4
4
  class Attack
5
5
  class Check
6
6
  attr_reader :name, :block, :type
7
+
7
8
  def initialize(name, options = {}, &block)
8
9
  @name = name
9
10
  @block = block
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Rack
6
+ class Attack
7
+ class Configuration
8
+ DEFAULT_BLOCKLISTED_RESPONSE = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
9
+
10
+ DEFAULT_THROTTLED_RESPONSE = lambda do |env|
11
+ if Rack::Attack.configuration.throttled_response_retry_after_header
12
+ match_data = env['rack.attack.match_data']
13
+ now = match_data[:epoch_time]
14
+ retry_after = match_data[:period] - (now % match_data[:period])
15
+
16
+ [429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
17
+ else
18
+ [429, { 'Content-Type' => 'text/plain' }, ["Retry later\n"]]
19
+ end
20
+ end
21
+
22
+ attr_reader :safelists, :blocklists, :throttles, :anonymous_blocklists, :anonymous_safelists
23
+ attr_accessor :blocklisted_response, :throttled_response, :throttled_response_retry_after_header
24
+
25
+ def initialize
26
+ set_defaults
27
+ end
28
+
29
+ def safelist(name = nil, &block)
30
+ safelist = Safelist.new(name, &block)
31
+
32
+ if name
33
+ @safelists[name] = safelist
34
+ else
35
+ @anonymous_safelists << safelist
36
+ end
37
+ end
38
+
39
+ def blocklist(name = nil, &block)
40
+ blocklist = Blocklist.new(name, &block)
41
+
42
+ if name
43
+ @blocklists[name] = blocklist
44
+ else
45
+ @anonymous_blocklists << blocklist
46
+ end
47
+ end
48
+
49
+ def blocklist_ip(ip_address)
50
+ @anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
51
+ end
52
+
53
+ def safelist_ip(ip_address)
54
+ @anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
55
+ end
56
+
57
+ def throttle(name, options, &block)
58
+ @throttles[name] = Throttle.new(name, options, &block)
59
+ end
60
+
61
+ def track(name, options = {}, &block)
62
+ @tracks[name] = Track.new(name, options, &block)
63
+ end
64
+
65
+ def safelisted?(request)
66
+ @anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
67
+ @safelists.any? { |_name, safelist| safelist.matched_by?(request) }
68
+ end
69
+
70
+ def blocklisted?(request)
71
+ @anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
72
+ @blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
73
+ end
74
+
75
+ def throttled?(request)
76
+ @throttles.any? do |_name, throttle|
77
+ throttle.matched_by?(request)
78
+ end
79
+ end
80
+
81
+ def tracked?(request)
82
+ @tracks.each_value do |track|
83
+ track.matched_by?(request)
84
+ end
85
+ end
86
+
87
+ def clear_configuration
88
+ set_defaults
89
+ end
90
+
91
+ private
92
+
93
+ def set_defaults
94
+ @safelists = {}
95
+ @blocklists = {}
96
+ @throttles = {}
97
+ @tracks = {}
98
+ @anonymous_blocklists = []
99
+ @anonymous_safelists = []
100
+ @throttled_response_retry_after_header = false
101
+
102
+ @blocklisted_response = DEFAULT_BLOCKLISTED_RESPONSE
103
+ @throttled_response = DEFAULT_THROTTLED_RESPONSE
104
+ end
105
+ end
106
+ end
107
+ end
@@ -4,9 +4,7 @@ module Rack
4
4
  class Attack
5
5
  class Railtie < ::Rails::Railtie
6
6
  initializer "rack-attack.middleware" do |app|
7
- if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.1")
8
- app.middleware.use(Rack::Attack)
9
- end
7
+ app.middleware.use(Rack::Attack)
10
8
  end
11
9
  end
12
10
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/base_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
7
7
  module StoreProxy
8
- class ActiveSupportRedisStoreProxy < SimpleDelegator
8
+ class ActiveSupportRedisStoreProxy < BaseProxy
9
9
  def self.handle?(store)
10
10
  defined?(::Redis) &&
11
11
  defined?(::ActiveSupport::Cache::RedisStore) &&
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/base_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
7
7
  module StoreProxy
8
- class DalliProxy < SimpleDelegator
8
+ class DalliProxy < BaseProxy
9
9
  def self.handle?(store)
10
10
  return false unless defined?(::Dalli)
11
11
 
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/base_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
7
7
  module StoreProxy
8
- class MemCacheStoreProxy < SimpleDelegator
8
+ class MemCacheStoreProxy < BaseProxy
9
9
  def self.handle?(store)
10
10
  defined?(::Dalli) &&
11
11
  defined?(::ActiveSupport::Cache::MemCacheStore) &&
@@ -1,47 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/base_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
7
7
  module StoreProxy
8
- class RedisCacheStoreProxy < SimpleDelegator
8
+ class RedisCacheStoreProxy < BaseProxy
9
9
  def self.handle?(store)
10
10
  store.class.name == "ActiveSupport::Cache::RedisCacheStore"
11
11
  end
12
12
 
13
- def increment(name, amount = 1, options = {})
13
+ def increment(name, amount = 1, **options)
14
14
  # RedisCacheStore#increment ignores options[:expires_in].
15
15
  #
16
16
  # So in order to workaround this we use RedisCacheStore#write (which sets expiration) to initialize
17
17
  # the counter. After that we continue using the original RedisCacheStore#increment.
18
- rescuing do
19
- if options[:expires_in] && !read(name)
20
- write(name, amount, options)
18
+ if options[:expires_in] && !read(name)
19
+ write(name, amount, options)
21
20
 
22
- amount
23
- else
24
- super
25
- end
21
+ amount
22
+ else
23
+ super
26
24
  end
27
25
  end
28
26
 
29
- def read(*_args)
30
- rescuing { super }
27
+ def read(name, options = {})
28
+ super(name, options.merge!(raw: true))
31
29
  end
32
30
 
33
31
  def write(name, value, options = {})
34
- rescuing do
35
- super(name, value, options.merge!(raw: true))
36
- end
37
- end
38
-
39
- private
40
-
41
- def rescuing
42
- yield
43
- rescue Redis::BaseError
44
- nil
32
+ super(name, value, options.merge!(raw: true))
45
33
  end
46
34
  end
47
35
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/base_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
7
7
  module StoreProxy
8
- class RedisProxy < SimpleDelegator
8
+ class RedisProxy < BaseProxy
9
9
  def initialize(*args)
10
10
  if Gem::Version.new(Redis::VERSION) < Gem::Version.new("3")
11
11
  warn 'RackAttack requires Redis gem >= 3.0.0.'
@@ -15,7 +15,7 @@ module Rack
15
15
  end
16
16
 
17
17
  def self.handle?(store)
18
- defined?(::Redis) && store.is_a?(::Redis)
18
+ defined?(::Redis) && store.class == ::Redis
19
19
  end
20
20
 
21
21
  def read(key)
@@ -31,27 +31,36 @@ module Rack
31
31
  end
32
32
 
33
33
  def increment(key, amount, options = {})
34
- count = nil
35
-
36
34
  rescuing do
37
35
  pipelined do
38
- count = incrby(key, amount)
36
+ incrby(key, amount)
39
37
  expire(key, options[:expires_in]) if options[:expires_in]
40
- end
38
+ end.first
41
39
  end
42
-
43
- count.value if count
44
40
  end
45
41
 
46
42
  def delete(key, _options = {})
47
43
  rescuing { del(key) }
48
44
  end
49
45
 
46
+ def delete_matched(matcher, _options = nil)
47
+ cursor = "0"
48
+
49
+ rescuing do
50
+ # Fetch keys in batches using SCAN to avoid blocking the Redis server.
51
+ loop do
52
+ cursor, keys = scan(cursor, match: matcher, count: 1000)
53
+ del(*keys) unless keys.empty?
54
+ break if cursor == "0"
55
+ end
56
+ end
57
+ end
58
+
50
59
  private
51
60
 
52
61
  def rescuing
53
62
  yield
54
- rescue Redis::BaseError
63
+ rescue Redis::BaseConnectionError
55
64
  nil
56
65
  end
57
66
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
3
+ require 'rack/attack/store_proxy/redis_proxy'
4
4
 
5
5
  module Rack
6
6
  class Attack
@@ -6,6 +6,7 @@ module Rack
6
6
  MANDATORY_OPTIONS = [:limit, :period].freeze
7
7
 
8
8
  attr_reader :name, :limit, :period, :block, :type
9
+
9
10
  def initialize(name, options, &block)
10
11
  @name = name
11
12
  @block = block
@@ -22,35 +23,58 @@ module Rack
22
23
  end
23
24
 
24
25
  def matched_by?(request)
25
- discriminator = block.call(request)
26
+ discriminator = discriminator_for(request)
26
27
  return false unless discriminator
27
28
 
28
- current_period = period.respond_to?(:call) ? period.call(request) : period
29
- current_limit = limit.respond_to?(:call) ? limit.call(request) : limit
30
- key = "#{name}:#{discriminator}"
31
- count = cache.count(key, current_period)
32
- epoch_time = cache.last_epoch_time
29
+ current_period = period_for(request)
30
+ current_limit = limit_for(request)
31
+ count = cache.count("#{name}:#{discriminator}", current_period)
33
32
 
34
33
  data = {
35
34
  discriminator: discriminator,
36
35
  count: count,
37
36
  period: current_period,
38
37
  limit: current_limit,
39
- epoch_time: epoch_time
38
+ epoch_time: cache.last_epoch_time
40
39
  }
41
40
 
42
- (request.env['rack.attack.throttle_data'] ||= {})[name] = data
43
-
44
41
  (count > current_limit).tap do |throttled|
42
+ annotate_request_with_throttle_data(request, data)
45
43
  if throttled
46
- request.env['rack.attack.matched'] = name
47
- request.env['rack.attack.match_discriminator'] = discriminator
48
- request.env['rack.attack.match_type'] = type
49
- request.env['rack.attack.match_data'] = data
44
+ annotate_request_with_matched_data(request, data)
50
45
  Rack::Attack.instrument(request)
51
46
  end
52
47
  end
53
48
  end
49
+
50
+ private
51
+
52
+ def discriminator_for(request)
53
+ discriminator = block.call(request)
54
+ if discriminator && Rack::Attack.throttle_discriminator_normalizer
55
+ discriminator = Rack::Attack.throttle_discriminator_normalizer.call(discriminator)
56
+ end
57
+ discriminator
58
+ end
59
+
60
+ def period_for(request)
61
+ period.respond_to?(:call) ? period.call(request) : period
62
+ end
63
+
64
+ def limit_for(request)
65
+ limit.respond_to?(:call) ? limit.call(request) : limit
66
+ end
67
+
68
+ def annotate_request_with_throttle_data(request, data)
69
+ (request.env['rack.attack.throttle_data'] ||= {})[name] = data
70
+ end
71
+
72
+ def annotate_request_with_matched_data(request, data)
73
+ request.env['rack.attack.matched'] = name
74
+ request.env['rack.attack.match_discriminator'] = data[:discriminator]
75
+ request.env['rack.attack.match_type'] = type
76
+ request.env['rack.attack.match_data'] = data
77
+ end
54
78
  end
55
79
  end
56
80
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class Attack
5
- VERSION = '6.2.1'
5
+ VERSION = '6.5.0'
6
6
  end
7
7
  end
@@ -12,24 +12,9 @@ if defined?(Rails)
12
12
  end
13
13
  end
14
14
 
15
- if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("5.1")
16
- it "is used by default" do
17
- @app.initialize!
18
- assert_equal 1, @app.middleware.count(Rack::Attack)
19
- end
20
-
21
- it "is not added when it was explicitly deleted" do
22
- @app.config.middleware.delete(Rack::Attack)
23
- @app.initialize!
24
- refute @app.middleware.include?(Rack::Attack)
25
- end
26
- end
27
-
28
- if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5.1")
29
- it "is not used by default" do
30
- @app.initialize!
31
- assert_equal 0, @app.middleware.count(Rack::Attack)
32
- end
15
+ it "is used by default" do
16
+ @app.initialize!
17
+ assert @app.middleware.include?(Rack::Attack)
33
18
  end
34
19
  end
35
20
  end
@@ -21,6 +21,6 @@ if should_run
21
21
  Rack::Attack.cache.store.clear
22
22
  end
23
23
 
24
- it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
24
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
25
25
  end
26
26
  end
@@ -20,6 +20,6 @@ if should_run
20
20
  Rack::Attack.cache.store.clear
21
21
  end
22
22
 
23
- it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.fetch(key) })
23
+ it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) })
24
24
  end
25
25
  end
@@ -20,7 +20,7 @@ describe "#throttle" do
20
20
  get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
21
21
 
22
22
  assert_equal 429, last_response.status
23
- assert_equal "60", last_response.headers["Retry-After"]
23
+ assert_nil last_response.headers["Retry-After"]
24
24
  assert_equal "Retry later\n", last_response.body
25
25
 
26
26
  get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
@@ -34,6 +34,24 @@ describe "#throttle" do
34
34
  end
35
35
  end
36
36
 
37
+ it "returns correct Retry-After header if enabled" do
38
+ Rack::Attack.throttled_response_retry_after_header = true
39
+
40
+ Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
41
+ request.ip
42
+ end
43
+
44
+ Timecop.freeze(Time.at(0)) do
45
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
46
+ assert_equal 200, last_response.status
47
+ end
48
+
49
+ Timecop.freeze(Time.at(25)) do
50
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
51
+ assert_equal "35", last_response.headers["Retry-After"]
52
+ end
53
+ end
54
+
37
55
  it "supports limit to be dynamic" do
38
56
  # Could be used to have different rate limits for authorized
39
57
  # vs general requests
@@ -13,7 +13,11 @@ OfflineExamples = Minitest::SharedExamples.new do
13
13
  end
14
14
 
15
15
  it 'should count' do
16
- @cache.send(:do_count, 'rack::attack::cache-test-key', 1)
16
+ @cache.count('cache-test-key', 1)
17
+ end
18
+
19
+ it 'should delete' do
20
+ @cache.delete('cache-test-key')
17
21
  end
18
22
  end
19
23
 
@@ -29,6 +33,18 @@ if defined?(::ActiveSupport::Cache::RedisStore)
29
33
  end
30
34
  end
31
35
 
36
+ if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
37
+ describe 'when Redis is offline' do
38
+ include OfflineExamples
39
+
40
+ before do
41
+ @cache = Rack::Attack::Cache.new
42
+ # Use presumably unused port for Redis client
43
+ @cache.store = ActiveSupport::Cache::RedisCacheStore.new(host: '127.0.0.1', port: 3333)
44
+ end
45
+ end
46
+ end
47
+
32
48
  if defined?(::Dalli)
33
49
  describe 'when Memcached is offline' do
34
50
  include OfflineExamples
@@ -45,3 +61,32 @@ if defined?(::Dalli)
45
61
  end
46
62
  end
47
63
  end
64
+
65
+ if defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore)
66
+ describe 'when Memcached is offline' do
67
+ include OfflineExamples
68
+
69
+ before do
70
+ Dalli.logger.level = Logger::FATAL
71
+
72
+ @cache = Rack::Attack::Cache.new
73
+ @cache.store = ActiveSupport::Cache::MemCacheStore.new('127.0.0.1:22122')
74
+ end
75
+
76
+ after do
77
+ Dalli.logger.level = Logger::INFO
78
+ end
79
+ end
80
+ end
81
+
82
+ if defined?(Redis)
83
+ describe 'when Redis is offline' do
84
+ include OfflineExamples
85
+
86
+ before do
87
+ @cache = Rack::Attack::Cache.new
88
+ # Use presumably unused port for Redis client
89
+ @cache.store = Redis.new(host: '127.0.0.1', port: 3333)
90
+ end
91
+ end
92
+ end
@@ -99,4 +99,26 @@ describe 'Rack::Attack' do
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ describe 'reset!' do
104
+ it 'raises an error when is not supported by cache store' do
105
+ Rack::Attack.cache.store = Class.new
106
+ assert_raises(Rack::Attack::IncompatibleStoreError) do
107
+ Rack::Attack.reset!
108
+ end
109
+ end
110
+
111
+ if defined?(Redis)
112
+ it 'should delete rack attack keys' do
113
+ redis = Redis.new
114
+ redis.set('key', 'value')
115
+ redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
116
+ Rack::Attack.cache.store = redis
117
+ Rack::Attack.reset!
118
+
119
+ _(redis.get('key')).must_equal 'value'
120
+ _(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
121
+ end
122
+ end
123
+ end
102
124
  end
@@ -57,10 +57,6 @@ describe 'Rack::Attack.throttle' do
57
57
 
58
58
  _(last_request.env['rack.attack.match_discriminator']).must_equal('1.2.3.4')
59
59
  end
60
-
61
- it 'should set a Retry-After header' do
62
- _(last_response.headers['Retry-After']).must_equal @period.to_s
63
- end
64
60
  end
65
61
  end
66
62
 
@@ -148,3 +144,47 @@ describe 'Rack::Attack.throttle with block retuning nil' do
148
144
  end
149
145
  end
150
146
  end
147
+
148
+ describe 'Rack::Attack.throttle with throttle_discriminator_normalizer' do
149
+ before do
150
+ @period = 60
151
+ @emails = [
152
+ "person@example.com",
153
+ "PERSON@example.com ",
154
+ " person@example.com\r\n ",
155
+ ]
156
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
157
+ Rack::Attack.throttle('logins/email', limit: 4, period: @period) do |req|
158
+ if req.path == '/login' && req.post?
159
+ req.params['email']
160
+ end
161
+ end
162
+ end
163
+
164
+ it 'should not differentiate requests when throttle_discriminator_normalizer is enabled' do
165
+ post_logins
166
+ key = "rack::attack:#{Time.now.to_i / @period}:logins/email:person@example.com"
167
+ _(Rack::Attack.cache.store.read(key)).must_equal 3
168
+ end
169
+
170
+ it 'should differentiate requests when throttle_discriminator_normalizer is disabled' do
171
+ begin
172
+ prev = Rack::Attack.throttle_discriminator_normalizer
173
+ Rack::Attack.throttle_discriminator_normalizer = nil
174
+
175
+ post_logins
176
+ @emails.each do |email|
177
+ key = "rack::attack:#{Time.now.to_i / @period}:logins/email:#{email}"
178
+ _(Rack::Attack.cache.store.read(key)).must_equal 1
179
+ end
180
+ ensure
181
+ Rack::Attack.throttle_discriminator_normalizer = prev
182
+ end
183
+ end
184
+
185
+ def post_logins
186
+ @emails.each do |email|
187
+ post '/login', email: email
188
+ end
189
+ end
190
+ end
data/spec/spec_helper.rb CHANGED
@@ -30,16 +30,11 @@ class MiniTest::Spec
30
30
 
31
31
  before do
32
32
  Rails.cache = nil
33
- @_original_throttled_response = Rack::Attack.throttled_response
34
- @_original_blocklisted_response = Rack::Attack.blocklisted_response
35
33
  end
36
34
 
37
35
  after do
38
36
  Rack::Attack.clear_configuration
39
37
  Rack::Attack.instance_variable_set(:@cache, nil)
40
-
41
- Rack::Attack.throttled_response = @_original_throttled_response
42
- Rack::Attack.blocklisted_response = @_original_blocklisted_response
43
38
  end
44
39
 
45
40
  def app
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: 6.2.1
4
+ version: 6.5.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: 2019-10-30 00:00:00.000000000 Z
11
+ date: 2021-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -126,14 +126,14 @@ dependencies:
126
126
  requirements:
127
127
  - - '='
128
128
  - !ruby/object:Gem::Version
129
- version: 0.75.0
129
+ version: 0.89.1
130
130
  type: :development
131
131
  prerelease: false
132
132
  version_requirements: !ruby/object:Gem::Requirement
133
133
  requirements:
134
134
  - - '='
135
135
  - !ruby/object:Gem::Version
136
- version: 0.75.0
136
+ version: 0.89.1
137
137
  - !ruby/object:Gem::Dependency
138
138
  name: rubocop-performance
139
139
  requirement: !ruby/object:Gem::Requirement
@@ -185,7 +185,7 @@ dependencies:
185
185
  version: '4.2'
186
186
  - - "<"
187
187
  - !ruby/object:Gem::Version
188
- version: '6.1'
188
+ version: '6.2'
189
189
  type: :development
190
190
  prerelease: false
191
191
  version_requirements: !ruby/object:Gem::Requirement
@@ -195,7 +195,7 @@ dependencies:
195
195
  version: '4.2'
196
196
  - - "<"
197
197
  - !ruby/object:Gem::Version
198
- version: '6.1'
198
+ version: '6.2'
199
199
  description: A rack middleware for throttling and blocking abusive requests
200
200
  email: aaron@ktheory.com
201
201
  executables: []
@@ -204,18 +204,18 @@ extra_rdoc_files: []
204
204
  files:
205
205
  - README.md
206
206
  - Rakefile
207
- - bin/setup
208
207
  - lib/rack/attack.rb
209
208
  - lib/rack/attack/allow2ban.rb
209
+ - lib/rack/attack/base_proxy.rb
210
210
  - lib/rack/attack/blocklist.rb
211
211
  - lib/rack/attack/cache.rb
212
212
  - lib/rack/attack/check.rb
213
+ - lib/rack/attack/configuration.rb
213
214
  - lib/rack/attack/fail2ban.rb
214
215
  - lib/rack/attack/path_normalizer.rb
215
216
  - lib/rack/attack/railtie.rb
216
217
  - lib/rack/attack/request.rb
217
218
  - lib/rack/attack/safelist.rb
218
- - lib/rack/attack/store_proxy.rb
219
219
  - lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb
220
220
  - lib/rack/attack/store_proxy/dalli_proxy.rb
221
221
  - lib/rack/attack/store_proxy/mem_cache_store_proxy.rb
@@ -267,13 +267,13 @@ files:
267
267
  - spec/rack_attack_track_spec.rb
268
268
  - spec/spec_helper.rb
269
269
  - spec/support/cache_store_helper.rb
270
- homepage: https://github.com/kickstarter/rack-attack
270
+ homepage: https://github.com/rack/rack-attack
271
271
  licenses:
272
272
  - MIT
273
273
  metadata:
274
- bug_tracker_uri: https://github.com/kickstarter/rack-attack/issues
275
- changelog_uri: https://github.com/kickstarter/rack-attack/blob/master/CHANGELOG.md
276
- source_code_uri: https://github.com/kickstarter/rack-attack
274
+ bug_tracker_uri: https://github.com/rack/rack-attack/issues
275
+ changelog_uri: https://github.com/rack/rack-attack/blob/master/CHANGELOG.md
276
+ source_code_uri: https://github.com/rack/rack-attack
277
277
  post_install_message:
278
278
  rdoc_options:
279
279
  - "--charset=UTF-8"
@@ -283,57 +283,57 @@ required_ruby_version: !ruby/object:Gem::Requirement
283
283
  requirements:
284
284
  - - ">="
285
285
  - !ruby/object:Gem::Version
286
- version: '2.3'
286
+ version: '2.4'
287
287
  required_rubygems_version: !ruby/object:Gem::Requirement
288
288
  requirements:
289
289
  - - ">="
290
290
  - !ruby/object:Gem::Version
291
291
  version: '0'
292
292
  requirements: []
293
- rubygems_version: 3.0.6
293
+ rubygems_version: 3.2.8
294
294
  signing_key:
295
295
  specification_version: 4
296
296
  summary: Block & throttle abusive requests
297
297
  test_files:
298
- - spec/integration/offline_spec.rb
299
- - spec/rack_attack_path_normalizer_spec.rb
300
- - spec/acceptance/safelisting_subnet_spec.rb
301
- - spec/acceptance/rails_middleware_spec.rb
302
- - spec/acceptance/track_throttle_spec.rb
303
- - spec/acceptance/cache_store_config_for_fail2ban_spec.rb
304
- - spec/acceptance/cache_store_config_with_rails_spec.rb
305
- - spec/acceptance/cache_store_config_for_allow2ban_spec.rb
306
- - spec/acceptance/safelisting_ip_spec.rb
307
- - spec/acceptance/track_spec.rb
308
- - spec/acceptance/blocking_subnet_spec.rb
309
- - spec/acceptance/blocking_ip_spec.rb
310
298
  - spec/acceptance/allow2ban_spec.rb
311
- - spec/acceptance/throttling_spec.rb
299
+ - spec/acceptance/blocking_ip_spec.rb
312
300
  - spec/acceptance/blocking_spec.rb
301
+ - spec/acceptance/blocking_subnet_spec.rb
302
+ - spec/acceptance/cache_store_config_for_allow2ban_spec.rb
303
+ - spec/acceptance/cache_store_config_for_fail2ban_spec.rb
304
+ - spec/acceptance/cache_store_config_for_throttle_spec.rb
305
+ - spec/acceptance/cache_store_config_with_rails_spec.rb
306
+ - spec/acceptance/customizing_blocked_response_spec.rb
313
307
  - spec/acceptance/customizing_throttled_response_spec.rb
314
308
  - spec/acceptance/extending_request_object_spec.rb
315
- - spec/acceptance/safelisting_spec.rb
316
- - spec/acceptance/cache_store_config_for_throttle_spec.rb
317
309
  - spec/acceptance/fail2ban_spec.rb
310
+ - spec/acceptance/rails_middleware_spec.rb
311
+ - spec/acceptance/safelisting_ip_spec.rb
312
+ - spec/acceptance/safelisting_spec.rb
313
+ - spec/acceptance/safelisting_subnet_spec.rb
314
+ - spec/acceptance/stores/active_support_dalli_store_spec.rb
318
315
  - spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb
319
- - spec/acceptance/stores/active_support_redis_cache_store_spec.rb
320
- - spec/acceptance/stores/active_support_memory_store_spec.rb
321
- - spec/acceptance/stores/active_support_redis_store_spec.rb
322
316
  - spec/acceptance/stores/active_support_mem_cache_store_spec.rb
317
+ - spec/acceptance/stores/active_support_memory_store_spec.rb
323
318
  - spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb
319
+ - spec/acceptance/stores/active_support_redis_cache_store_spec.rb
320
+ - spec/acceptance/stores/active_support_redis_store_spec.rb
324
321
  - spec/acceptance/stores/connection_pool_dalli_client_spec.rb
325
- - spec/acceptance/stores/active_support_dalli_store_spec.rb
326
- - spec/acceptance/stores/redis_store_spec.rb
327
322
  - spec/acceptance/stores/dalli_client_spec.rb
328
323
  - spec/acceptance/stores/redis_spec.rb
329
- - spec/acceptance/customizing_blocked_response_spec.rb
330
- - spec/spec_helper.rb
324
+ - spec/acceptance/stores/redis_store_spec.rb
325
+ - spec/acceptance/throttling_spec.rb
326
+ - spec/acceptance/track_spec.rb
327
+ - spec/acceptance/track_throttle_spec.rb
331
328
  - spec/allow2ban_spec.rb
332
- - spec/rack_attack_instrumentation_spec.rb
329
+ - spec/fail2ban_spec.rb
330
+ - spec/integration/offline_spec.rb
333
331
  - spec/rack_attack_dalli_proxy_spec.rb
332
+ - spec/rack_attack_instrumentation_spec.rb
333
+ - spec/rack_attack_path_normalizer_spec.rb
334
+ - spec/rack_attack_request_spec.rb
334
335
  - spec/rack_attack_spec.rb
335
336
  - spec/rack_attack_throttle_spec.rb
336
- - spec/rack_attack_request_spec.rb
337
- - spec/fail2ban_spec.rb
338
337
  - spec/rack_attack_track_spec.rb
338
+ - spec/spec_helper.rb
339
339
  - spec/support/cache_store_helper.rb
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rack
4
- class Attack
5
- module StoreProxy
6
- PROXIES = [
7
- DalliProxy,
8
- MemCacheStoreProxy,
9
- RedisStoreProxy,
10
- RedisProxy,
11
- RedisCacheStoreProxy,
12
- ActiveSupportRedisStoreProxy
13
- ].freeze
14
-
15
- def self.build(store)
16
- klass = PROXIES.find { |proxy| proxy.handle?(store) }
17
- klass ? klass.new(store) : store
18
- end
19
- end
20
- end
21
- end