rack-attack 6.2.0 → 6.4.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
2
  SHA256:
3
- metadata.gz: ac5af22059fcc24c45b9732a806b13ef8a39b3ab425e713d22b7d0b1c9fbae11
4
- data.tar.gz: fdd20e74080d4254d7910be3d1f0343580a2cedd79b18f2448fa753acd9259e2
3
+ metadata.gz: e7d44de650fae1c83d5a3da49dc8f304e44280f72bd209d3f78643b90d573bd8
4
+ data.tar.gz: a39d0270489617a8c0a49e01868c24cc87311e80457fbc104c86e45d29978f51
5
5
  SHA512:
6
- metadata.gz: 1f2a7bd75ab8423dde30e482085b19cb5cfbf7347aed13c94da63d31784939075278cc0f891af450bd33e5ef3de4ea092441b26f2519e28ecb5cbe5c6a16d007
7
- data.tar.gz: fbe8d0cc86c52be9a028a4fcb2f8f2399af143b6ccd77c7377cbe5f762bb344bdb80833c5e53221ffefad57d04ee132650b38cd5e9d44c5073e475ba894a1a3f
6
+ metadata.gz: 7d9d965cc672bba8ab2b9f333746e32091363d6b65bf290104c248799a811f272ad8388e7f7b3d870d382e9c80a1003a300f9380c2d8082195972817146a281d
7
+ data.tar.gz: fbfa381116824ea4de492b66408d15bd708692a74275548c9b167868c0bee566f79a216046213c43a8eb3117b869e86ac99f989e5e4c045c30267db2981b2c6b
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)
@@ -140,7 +140,7 @@ E.g.
140
140
  # Provided that trusted users use an HTTP request header named APIKey
141
141
  Rack::Attack.safelist("mark any authenticated access safe") do |request|
142
142
  # Requests are allowed if the return value is truthy
143
- request.env["APIKey"] == "secret-string"
143
+ request.env["HTTP_APIKEY"] == "secret-string"
144
144
  end
145
145
 
146
146
  # Always allow requests from localhost
@@ -263,10 +263,12 @@ Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
263
263
  end
264
264
 
265
265
  # Throttle login attempts for a given email parameter to 6 reqs/minute
266
- # Return the email as a discriminator on POST /login requests
266
+ # Return the *normalized* email as a discriminator on POST /login requests
267
267
  Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
268
268
  if req.path == '/login' && req.post?
269
- req.params['email']
269
+ # Normalize the email, using the same logic as your authentication process, to
270
+ # protect against rate limit bypasses.
271
+ req.params['email'].to_s.downcase.gsub(/\s+/, "")
270
272
  end
271
273
  end
272
274
 
@@ -342,6 +344,11 @@ end
342
344
  While Rack::Attack's primary focus is minimizing harm from abusive clients, it
343
345
  can also be used to return rate limit data that's helpful for well-behaved clients.
344
346
 
347
+ 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:
348
+ ```ruby
349
+ Rack::Attack.throttled_response_retry_after_header = true
350
+ ```
351
+
345
352
  Here's an example response that includes conventional `RateLimit-*` headers:
346
353
 
347
354
  ```ruby
@@ -372,7 +379,7 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
372
379
 
373
380
  You can subscribe to `rack_attack` events and log it, graph it, etc.
374
381
 
375
- To get notified about specific type of events, subscribe to the event name followed by the `rack_attack` namesapce.
382
+ To get notified about specific type of events, subscribe to the event name followed by the `rack_attack` namespace.
376
383
  E.g. for throttles use:
377
384
 
378
385
  ```ruby
@@ -393,6 +400,20 @@ ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, r
393
400
  end
394
401
  ```
395
402
 
403
+ ## Testing
404
+
405
+ A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
406
+ need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
407
+ for more on how to do this.
408
+
409
+ ### Disabling
410
+
411
+ `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.
412
+
413
+ ### Test case isolation
414
+
415
+ `Rack::Attack.reset!` can be used in your test suite to clear any Rack::Attack state between different test cases.
416
+
396
417
  ## How it works
397
418
 
398
419
  The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
@@ -429,13 +450,6 @@ can cleanly monkey patch helper methods onto the
429
450
 
430
451
  `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
431
452
 
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
453
  ## Performance
440
454
 
441
455
  The overhead of running Rack::Attack is typically negligible (a few milliseconds per request),
@@ -2,9 +2,10 @@
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"
8
9
 
9
10
  require 'rack/attack/railtie' if defined?(::Rails)
10
11
 
@@ -13,8 +14,8 @@ module Rack
13
14
  class Error < StandardError; end
14
15
  class MisconfiguredStoreError < Error; end
15
16
  class MissingStoreError < Error; end
17
+ class IncompatibleStoreError < Error; end
16
18
 
17
- autoload :Cache, 'rack/attack/cache'
18
19
  autoload :Check, 'rack/attack/check'
19
20
  autoload :Throttle, 'rack/attack/throttle'
20
21
  autoload :Safelist, 'rack/attack/safelist'
@@ -31,82 +32,8 @@ module Rack
31
32
  autoload :Allow2Ban, 'rack/attack/allow2ban'
32
33
 
33
34
  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
35
+ attr_accessor :enabled, :notifier
36
+ attr_reader :configuration
110
37
 
111
38
  def instrument(request)
112
39
  if notifier
@@ -122,55 +49,67 @@ module Rack
122
49
  @cache ||= Cache.new
123
50
  end
124
51
 
125
- def clear_configuration
126
- @safelists = {}
127
- @blocklists = {}
128
- @throttles = {}
129
- @tracks = {}
130
- self.anonymous_blocklists = []
131
- self.anonymous_safelists = []
132
- end
133
-
134
52
  def clear!
135
53
  warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
136
- clear_configuration
137
- end
54
+ @configuration.clear_configuration
55
+ end
56
+
57
+ def reset!
58
+ cache.reset!
59
+ end
60
+
61
+ extend Forwardable
62
+ def_delegators(
63
+ :@configuration,
64
+ :safelist,
65
+ :blocklist,
66
+ :blocklist_ip,
67
+ :safelist_ip,
68
+ :throttle,
69
+ :track,
70
+ :blocklisted_response,
71
+ :blocklisted_response=,
72
+ :throttled_response,
73
+ :throttled_response=,
74
+ :throttled_response_retry_after_header,
75
+ :throttled_response_retry_after_header=,
76
+ :clear_configuration,
77
+ :safelists,
78
+ :blocklists,
79
+ :throttles,
80
+ :tracks
81
+ )
138
82
  end
139
83
 
140
84
  # Set defaults
141
85
  @enabled = true
142
- @anonymous_blocklists = []
143
- @anonymous_safelists = []
144
86
  @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"]]
149
- end
87
+ @configuration = Configuration.new
88
+
89
+ attr_reader :configuration
150
90
 
151
91
  def initialize(app)
152
92
  @app = app
93
+ @configuration = self.class.configuration
153
94
  end
154
95
 
155
96
  def call(env)
156
- return @app.call(env) unless self.class.enabled
97
+ return @app.call(env) if !self.class.enabled || env["rack.attack.called"]
157
98
 
99
+ env["rack.attack.called"] = true
158
100
  env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
159
101
  request = Rack::Attack::Request.new(env)
160
102
 
161
- if safelisted?(request)
103
+ if configuration.safelisted?(request)
162
104
  @app.call(env)
163
- elsif blocklisted?(request)
164
- self.class.blocklisted_response.call(env)
165
- elsif throttled?(request)
166
- self.class.throttled_response.call(env)
105
+ elsif configuration.blocklisted?(request)
106
+ configuration.blocklisted_response.call(env)
107
+ elsif configuration.throttled?(request)
108
+ configuration.throttled_response.call(env)
167
109
  else
168
- tracked?(request)
110
+ configuration.tracked?(request)
169
111
  @app.call(env)
170
112
  end
171
113
  end
172
-
173
- extend Forwardable
174
- def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
175
114
  end
176
115
  end
@@ -12,6 +12,7 @@ module Rack
12
12
  end
13
13
 
14
14
  attr_reader :store
15
+
15
16
  def store=(store)
16
17
  @store = StoreProxy.build(store)
17
18
  end
@@ -41,6 +42,17 @@ module Rack
41
42
  store.delete("#{prefix}:#{unprefixed_key}")
42
43
  end
43
44
 
45
+ def reset!
46
+ if store.respond_to?(:delete_matched)
47
+ store.delete_matched("#{prefix}*")
48
+ else
49
+ raise(
50
+ Rack::Attack::IncompatibleStoreError,
51
+ "Configured store #{store.class.name} doesn't respond to #delete_matched method"
52
+ )
53
+ end
54
+ end
55
+
44
56
  private
45
57
 
46
58
  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
@@ -3,17 +3,9 @@
3
3
  module Rack
4
4
  class Attack
5
5
  class Railtie < ::Rails::Railtie
6
- initializer 'rack.attack.middleware', after: :load_config_initializers, before: :build_middleware_stack do |app|
6
+ initializer "rack-attack.middleware" do |app|
7
7
  if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5.1")
8
- middlewares = app.config.middleware
9
- operations = middlewares.send(:operations) + middlewares.send(:delete_operations)
10
-
11
- use_middleware = operations.none? do |operation|
12
- middleware = operation[1]
13
- middleware.include?(Rack::Attack)
14
- end
15
-
16
- middlewares.use(Rack::Attack) if use_middleware
8
+ app.middleware.use(Rack::Attack)
17
9
  end
18
10
  end
19
11
  end
@@ -10,38 +10,26 @@ module Rack
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
@@ -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
@@ -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
@@ -23,34 +24,50 @@ module Rack
23
24
 
24
25
  def matched_by?(request)
25
26
  discriminator = block.call(request)
27
+
26
28
  return false unless discriminator
27
29
 
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
30
+ current_period = period_for(request)
31
+ current_limit = limit_for(request)
32
+ count = cache.count("#{name}:#{discriminator}", current_period)
33
33
 
34
34
  data = {
35
35
  discriminator: discriminator,
36
36
  count: count,
37
37
  period: current_period,
38
38
  limit: current_limit,
39
- epoch_time: epoch_time
39
+ epoch_time: cache.last_epoch_time
40
40
  }
41
41
 
42
- (request.env['rack.attack.throttle_data'] ||= {})[name] = data
43
-
44
42
  (count > current_limit).tap do |throttled|
43
+ annotate_request_with_throttle_data(request, data)
45
44
  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
45
+ annotate_request_with_matched_data(request, data)
50
46
  Rack::Attack.instrument(request)
51
47
  end
52
48
  end
53
49
  end
50
+
51
+ private
52
+
53
+ def period_for(request)
54
+ period.respond_to?(:call) ? period.call(request) : period
55
+ end
56
+
57
+ def limit_for(request)
58
+ limit.respond_to?(:call) ? limit.call(request) : limit
59
+ end
60
+
61
+ def annotate_request_with_throttle_data(request, data)
62
+ (request.env['rack.attack.throttle_data'] ||= {})[name] = data
63
+ end
64
+
65
+ def annotate_request_with_matched_data(request, data)
66
+ request.env['rack.attack.matched'] = name
67
+ request.env['rack.attack.match_discriminator'] = data[:discriminator]
68
+ request.env['rack.attack.match_type'] = type
69
+ request.env['rack.attack.match_data'] = data
70
+ end
54
71
  end
55
72
  end
56
73
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class Attack
5
- VERSION = '6.2.0'
5
+ VERSION = '6.4.0'
6
6
  end
7
7
  end
@@ -18,12 +18,6 @@ if defined?(Rails)
18
18
  assert_equal 1, @app.middleware.count(Rack::Attack)
19
19
  end
20
20
 
21
- it "is not added when it was added explicitly" do
22
- @app.config.middleware.use(Rack::Attack)
23
- @app.initialize!
24
- assert_equal 1, @app.middleware.count(Rack::Attack)
25
- end
26
-
27
21
  it "is not added when it was explicitly deleted" do
28
22
  @app.config.middleware.delete(Rack::Attack)
29
23
  @app.initialize!
@@ -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
 
@@ -30,22 +30,19 @@ 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
46
41
  Rack::Builder.new do
47
42
  # Use Rack::Lint to test that rack-attack is complying with the rack spec
48
43
  use Rack::Lint
44
+ # Intentionally added twice to test idempotence property
45
+ use Rack::Attack
49
46
  use Rack::Attack
50
47
  use Rack::Lint
51
48
 
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.0
4
+ version: 6.4.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-12 00:00:00.000000000 Z
11
+ date: 2021-01-24 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,12 +204,12 @@ 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
210
209
  - lib/rack/attack/blocklist.rb
211
210
  - lib/rack/attack/cache.rb
212
211
  - lib/rack/attack/check.rb
212
+ - lib/rack/attack/configuration.rb
213
213
  - lib/rack/attack/fail2ban.rb
214
214
  - lib/rack/attack/path_normalizer.rb
215
215
  - lib/rack/attack/railtie.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.6
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