rack-attack 6.0.0 → 6.3.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: f85825803ce676e10466175d4bb99cc151d649130d6f4008bdffdc1381b4650a
4
- data.tar.gz: 2bcd9d6a9d75491df5a9ddf2f7a5128eae152245fce8f27a069f36037ebc8ddb
3
+ metadata.gz: cba47e380843d184fd3df1af08b252aca3fc2411de7ccbd62a9b7da6ee933b72
4
+ data.tar.gz: 0dc5300a553830ca7e1cfa84de814fd743e608b9ec0140fd6b1145c421085ba7
5
5
  SHA512:
6
- metadata.gz: 5c9b03bbb0e55105ebe4d2ea9a0f13a025ea7ab4903f86e07956ecbd7f7ddfcacfecbbc8239bc9ac427fcc4bd5445fa85e20732bd4d90b51f6dafeb7194bc063
7
- data.tar.gz: a31dad7fb5c9220d4a44b303103ffb12cb23843bc150b14cec38a9d0151bc73791a66a80e8a543fa09f58d36a7b0627f0b5fda4fb93953d525747840d7e1d97f
6
+ metadata.gz: 71fd5eace9c851dab06317f3f4f4f28a2cbc20dd20c663228fbd073a052b4f30c4de87a06109ce9cf6b7395fc547b0c99b6f4e579285a699be40a93a9511452f
7
+ data.tar.gz: 5139f0be932f94273dba2d8d59ccbcb0e14ca92656b68d126099a124f04160c2c426e72f330b9e3c5dd8ed560f2fb292aa68dde2c32f2238d36c8c7e95422d01
data/README.md CHANGED
@@ -1,3 +1,6 @@
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.
3
+
1
4
  # Rack::Attack
2
5
 
3
6
  *Rack middleware for blocking & throttling abusive requests*
@@ -9,6 +12,7 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha
9
12
  [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
10
13
  [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
11
14
  [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
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)
12
16
 
13
17
  ## Table of contents
14
18
 
@@ -67,14 +71,19 @@ Or install it yourself as:
67
71
 
68
72
  Then tell your ruby web application to use rack-attack as a middleware.
69
73
 
70
- a) For __rails__ applications:
71
-
74
+ a) For __rails__ applications with versions >= 5.1 it is used by default. For older rails versions you should enable it explicitly:
72
75
  ```ruby
73
76
  # In config/application.rb
74
77
 
75
78
  config.middleware.use Rack::Attack
76
79
  ```
77
80
 
81
+ You can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing:
82
+
83
+ ```ruby
84
+ Rack::Attack.enabled = false
85
+ ```
86
+
78
87
  b) For __rack__ applications:
79
88
 
80
89
  ```ruby
@@ -285,9 +294,9 @@ Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
285
294
  end
286
295
 
287
296
  # Track it using ActiveSupport::Notification
288
- ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, payload|
297
+ ActiveSupport::Notifications.subscribe("track.rack_attack") do |name, start, finish, request_id, payload|
289
298
  req = payload[:request]
290
- if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track
299
+ if req.env['rack.attack.matched'] == "special_agent"
291
300
  Rails.logger.info "special_agent: #{req.path}"
292
301
  STATSD.increment("special_agent")
293
302
  end
@@ -333,6 +342,11 @@ end
333
342
  While Rack::Attack's primary focus is minimizing harm from abusive clients, it
334
343
  can also be used to return rate limit data that's helpful for well-behaved clients.
335
344
 
345
+ If you want to return to user how many seconds to wait until he can start sending requests again, this can be done through enabling `Retry-After` header:
346
+ ```ruby
347
+ Rack::Attack.throttled_response_retry_after_header = true
348
+ ```
349
+
336
350
  Here's an example response that includes conventional `RateLimit-*` headers:
337
351
 
338
352
  ```ruby
@@ -354,7 +368,7 @@ end
354
368
  For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
355
369
 
356
370
  ```ruby
357
- request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l, :epoch_time => t }
371
+ request.env['rack.attack.throttle_data'][name] # => { discriminator: d, count: n, period: p, limit: l, epoch_time: t }
358
372
  ```
359
373
 
360
374
  ## Logging & Instrumentation
@@ -2,163 +2,114 @@
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
- class Rack::Attack
10
- class MisconfiguredStoreError < StandardError; end
11
- class MissingStoreError < StandardError; end
12
-
13
- autoload :Cache, 'rack/attack/cache'
14
- autoload :Check, 'rack/attack/check'
15
- autoload :Throttle, 'rack/attack/throttle'
16
- autoload :Safelist, 'rack/attack/safelist'
17
- autoload :Blocklist, 'rack/attack/blocklist'
18
- autoload :Track, 'rack/attack/track'
19
- autoload :StoreProxy, 'rack/attack/store_proxy'
20
- autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
21
- autoload :MemCacheStoreProxy, 'rack/attack/store_proxy/mem_cache_store_proxy'
22
- autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
23
- autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
24
- autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
25
- autoload :ActiveSupportRedisStoreProxy, 'rack/attack/store_proxy/active_support_redis_store_proxy'
26
- autoload :Fail2Ban, 'rack/attack/fail2ban'
27
- autoload :Allow2Ban, 'rack/attack/allow2ban'
28
-
29
- class << self
30
- attr_accessor :notifier, :blocklisted_response, :throttled_response, :anonymous_blocklists, :anonymous_safelists
31
-
32
- def safelist(name = nil, &block)
33
- safelist = Safelist.new(name, &block)
34
-
35
- if name
36
- safelists[name] = safelist
37
- else
38
- anonymous_safelists << safelist
39
- end
40
- end
41
9
 
42
- def blocklist(name = nil, &block)
43
- blocklist = Blocklist.new(name, &block)
44
-
45
- if name
46
- blocklists[name] = blocklist
47
- else
48
- anonymous_blocklists << blocklist
10
+ require 'rack/attack/railtie' if defined?(::Rails)
11
+
12
+ module Rack
13
+ class Attack
14
+ class Error < StandardError; end
15
+ class MisconfiguredStoreError < Error; end
16
+ class MissingStoreError < Error; end
17
+ class IncompatibleStoreError < Error; end
18
+
19
+ autoload :Check, 'rack/attack/check'
20
+ autoload :Throttle, 'rack/attack/throttle'
21
+ autoload :Safelist, 'rack/attack/safelist'
22
+ autoload :Blocklist, 'rack/attack/blocklist'
23
+ autoload :Track, 'rack/attack/track'
24
+ autoload :StoreProxy, 'rack/attack/store_proxy'
25
+ autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
26
+ autoload :MemCacheStoreProxy, 'rack/attack/store_proxy/mem_cache_store_proxy'
27
+ autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
28
+ autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
29
+ autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
30
+ autoload :ActiveSupportRedisStoreProxy, 'rack/attack/store_proxy/active_support_redis_store_proxy'
31
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
32
+ autoload :Allow2Ban, 'rack/attack/allow2ban'
33
+
34
+ class << self
35
+ attr_accessor :enabled, :notifier
36
+ attr_reader :configuration
37
+
38
+ def instrument(request)
39
+ if notifier
40
+ event_type = request.env["rack.attack.match_type"]
41
+ notifier.instrument("#{event_type}.rack_attack", request: request)
42
+
43
+ # Deprecated: Keeping just for backwards compatibility
44
+ notifier.instrument("rack.attack", request: request)
45
+ end
49
46
  end
50
- end
51
-
52
- def blocklist_ip(ip_address)
53
- anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
54
- end
55
-
56
- def safelist_ip(ip_address)
57
- anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
58
- end
59
-
60
- def throttle(name, options, &block)
61
- throttles[name] = Throttle.new(name, options, &block)
62
- end
63
-
64
- def track(name, options = {}, &block)
65
- tracks[name] = Track.new(name, options, &block)
66
- end
67
-
68
- def safelists
69
- @safelists ||= {}
70
- end
71
-
72
- def blocklists
73
- @blocklists ||= {}
74
- end
75
-
76
- def throttles
77
- @throttles ||= {}
78
- end
79
-
80
- def tracks
81
- @tracks ||= {}
82
- end
83
-
84
- def safelisted?(request)
85
- anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
86
- safelists.any? { |_name, safelist| safelist.matched_by?(request) }
87
- end
88
47
 
89
- def blocklisted?(request)
90
- anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
91
- blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
92
- end
93
-
94
- def throttled?(request)
95
- throttles.any? do |_name, throttle|
96
- throttle.matched_by?(request)
48
+ def cache
49
+ @cache ||= Cache.new
97
50
  end
98
- end
99
51
 
100
- def tracked?(request)
101
- tracks.each_value do |track|
102
- track.matched_by?(request)
52
+ def clear!
53
+ warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
54
+ @configuration.clear_configuration
103
55
  end
104
- end
105
-
106
- def instrument(request)
107
- if notifier
108
- event_type = request.env["rack.attack.match_type"]
109
- notifier.instrument("#{event_type}.rack_attack", request: request)
110
56
 
111
- # Deprecated: Keeping just for backwards compatibility
112
- notifier.instrument("rack.attack", request: request)
57
+ def reset!
58
+ cache.reset!
113
59
  end
114
- end
115
-
116
- def cache
117
- @cache ||= Cache.new
118
- end
119
60
 
120
- def clear_configuration
121
- @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
122
- self.anonymous_blocklists = []
123
- self.anonymous_safelists = []
124
- end
125
-
126
- def clear!
127
- warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
128
- clear_configuration
129
- end
130
- end
131
-
132
- # Set defaults
133
- @anonymous_blocklists = []
134
- @anonymous_safelists = []
135
- @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
136
- @blocklisted_response = lambda { |_env| [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
137
- @throttled_response = lambda { |env|
138
- retry_after = (env['rack.attack.match_data'] || {})[:period]
139
- [429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
140
- }
141
-
142
- def initialize(app)
143
- @app = app
144
- end
145
-
146
- def call(env)
147
- env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
148
- request = Rack::Attack::Request.new(env)
149
-
150
- if safelisted?(request)
151
- @app.call(env)
152
- elsif blocklisted?(request)
153
- self.class.blocklisted_response.call(env)
154
- elsif throttled?(request)
155
- self.class.throttled_response.call(env)
156
- else
157
- tracked?(request)
158
- @app.call(env)
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
+ )
82
+ end
83
+
84
+ # Set defaults
85
+ @enabled = true
86
+ @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
87
+ @configuration = Configuration.new
88
+
89
+ attr_reader :configuration
90
+
91
+ def initialize(app)
92
+ @app = app
93
+ @configuration = self.class.configuration
94
+ end
95
+
96
+ def call(env)
97
+ return @app.call(env) if !self.class.enabled || env["rack.attack.called"]
98
+
99
+ env["rack.attack.called"] = true
100
+ env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
101
+ request = Rack::Attack::Request.new(env)
102
+
103
+ if configuration.safelisted?(request)
104
+ @app.call(env)
105
+ elsif configuration.blocklisted?(request)
106
+ configuration.blocklisted_response.call(env)
107
+ elsif configuration.throttled?(request)
108
+ configuration.throttled_response.call(env)
109
+ else
110
+ configuration.tracked?(request)
111
+ @app.call(env)
112
+ end
159
113
  end
160
114
  end
161
-
162
- extend Forwardable
163
- def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
164
115
  end
@@ -41,6 +41,17 @@ module Rack
41
41
  store.delete("#{prefix}:#{unprefixed_key}")
42
42
  end
43
43
 
44
+ def reset!
45
+ if store.respond_to?(:delete_matched)
46
+ store.delete_matched("#{prefix}*")
47
+ else
48
+ raise(
49
+ Rack::Attack::IncompatibleStoreError,
50
+ "Configured store #{store.class.name} doesn't respond to #delete_matched method"
51
+ )
52
+ end
53
+ end
54
+
44
55
  private
45
56
 
46
57
  def key_and_expiry(unprefixed_key, period)
@@ -73,7 +84,10 @@ module Rack
73
84
 
74
85
  def enforce_store_method_presence!(method_name)
75
86
  if !store.respond_to?(method_name)
76
- raise Rack::Attack::MisconfiguredStoreError, "Configured store #{store.class.name} doesn't respond to ##{method_name} method"
87
+ raise(
88
+ Rack::Attack::MisconfiguredStoreError,
89
+ "Configured store #{store.class.name} doesn't respond to ##{method_name} method"
90
+ )
77
91
  end
78
92
  end
79
93
  end
@@ -5,7 +5,8 @@ module Rack
5
5
  class Check
6
6
  attr_reader :name, :block, :type
7
7
  def initialize(name, options = {}, &block)
8
- @name, @block = name, block
8
+ @name = name
9
+ @block = block
9
10
  @type = options.fetch(:type, nil)
10
11
  end
11
12
 
@@ -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