rack-attack 6.2.0 → 6.4.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: 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