rack-attack 6.1.0 → 6.2.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: 273111fdf125be1d54c6c956dd4a4e3abe96184a0c5608304839280e06d655a3
4
- data.tar.gz: 7e9039aaca427b1f9312ce4739d9d1713282f7b29647988577a37b32c6cfb393
3
+ metadata.gz: ac5af22059fcc24c45b9732a806b13ef8a39b3ab425e713d22b7d0b1c9fbae11
4
+ data.tar.gz: fdd20e74080d4254d7910be3d1f0343580a2cedd79b18f2448fa753acd9259e2
5
5
  SHA512:
6
- metadata.gz: deab3999e7a7f72e6e3d240f8312a38590deb1fd88aba2261c115573ac3081a5dc2e17667d184b68db8e06faf1f28dac27e44fc07867dc149c01ce3635069c42
7
- data.tar.gz: bcfc056fd31c5f05c5fa4b517c420df227211186713dab5eeabea74917aa5236ffcac0f00455ac1b4ceb840c1f32929a4c73c84bedf3c34d00f2f04b5e93025a
6
+ metadata.gz: 1f2a7bd75ab8423dde30e482085b19cb5cfbf7347aed13c94da63d31784939075278cc0f891af450bd33e5ef3de4ea092441b26f2519e28ecb5cbe5c6a16d007
7
+ data.tar.gz: fbe8d0cc86c52be9a028a4fcb2f8f2399af143b6ccd77c7377cbe5f762bb344bdb80833c5e53221ffefad57d04ee132650b38cd5e9d44c5073e475ba894a1a3f
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
@@ -6,159 +6,171 @@ require 'rack/attack/path_normalizer'
6
6
  require 'rack/attack/request'
7
7
  require "ipaddr"
8
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
9
+ require 'rack/attack/railtie' if defined?(::Rails)
10
+
11
+ module Rack
12
+ class Attack
13
+ class Error < StandardError; end
14
+ class MisconfiguredStoreError < Error; end
15
+ class MissingStoreError < Error; end
16
+
17
+ autoload :Cache, 'rack/attack/cache'
18
+ autoload :Check, 'rack/attack/check'
19
+ autoload :Throttle, 'rack/attack/throttle'
20
+ autoload :Safelist, 'rack/attack/safelist'
21
+ autoload :Blocklist, 'rack/attack/blocklist'
22
+ 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
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
31
+ autoload :Allow2Ban, 'rack/attack/allow2ban'
32
+
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
39
45
  end
40
- end
41
46
 
42
- def blocklist(name = nil, &block)
43
- blocklist = Blocklist.new(name, &block)
47
+ def blocklist(name = nil, &block)
48
+ blocklist = Blocklist.new(name, &block)
44
49
 
45
- if name
46
- blocklists[name] = blocklist
47
- else
48
- anonymous_blocklists << blocklist
50
+ if name
51
+ blocklists[name] = blocklist
52
+ else
53
+ anonymous_blocklists << blocklist
54
+ end
49
55
  end
50
- end
51
56
 
52
- def blocklist_ip(ip_address)
53
- anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
54
- end
57
+ def blocklist_ip(ip_address)
58
+ anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
59
+ end
55
60
 
56
- def safelist_ip(ip_address)
57
- anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
58
- end
61
+ def safelist_ip(ip_address)
62
+ anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) }
63
+ end
59
64
 
60
- def throttle(name, options, &block)
61
- throttles[name] = Throttle.new(name, options, &block)
62
- end
65
+ def throttle(name, options, &block)
66
+ throttles[name] = Throttle.new(name, options, &block)
67
+ end
63
68
 
64
- def track(name, options = {}, &block)
65
- tracks[name] = Track.new(name, options, &block)
66
- end
69
+ def track(name, options = {}, &block)
70
+ tracks[name] = Track.new(name, options, &block)
71
+ end
67
72
 
68
- def safelists
69
- @safelists ||= {}
70
- end
73
+ def safelists
74
+ @safelists ||= {}
75
+ end
71
76
 
72
- def blocklists
73
- @blocklists ||= {}
74
- end
77
+ def blocklists
78
+ @blocklists ||= {}
79
+ end
75
80
 
76
- def throttles
77
- @throttles ||= {}
78
- end
81
+ def throttles
82
+ @throttles ||= {}
83
+ end
79
84
 
80
- def tracks
81
- @tracks ||= {}
82
- end
85
+ def tracks
86
+ @tracks ||= {}
87
+ end
83
88
 
84
- def safelisted?(request)
85
- anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
86
- safelists.any? { |_name, safelist| safelist.matched_by?(request) }
87
- end
89
+ def safelisted?(request)
90
+ anonymous_safelists.any? { |safelist| safelist.matched_by?(request) } ||
91
+ safelists.any? { |_name, safelist| safelist.matched_by?(request) }
92
+ end
88
93
 
89
- def blocklisted?(request)
90
- anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
91
- blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
92
- end
94
+ def blocklisted?(request)
95
+ anonymous_blocklists.any? { |blocklist| blocklist.matched_by?(request) } ||
96
+ blocklists.any? { |_name, blocklist| blocklist.matched_by?(request) }
97
+ end
93
98
 
94
- def throttled?(request)
95
- throttles.any? do |_name, throttle|
96
- throttle.matched_by?(request)
99
+ def throttled?(request)
100
+ throttles.any? do |_name, throttle|
101
+ throttle.matched_by?(request)
102
+ end
97
103
  end
98
- end
99
104
 
100
- def tracked?(request)
101
- tracks.each_value do |track|
102
- track.matched_by?(request)
105
+ def tracked?(request)
106
+ tracks.each_value do |track|
107
+ track.matched_by?(request)
108
+ end
103
109
  end
104
- end
105
110
 
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)
111
+ def instrument(request)
112
+ if notifier
113
+ event_type = request.env["rack.attack.match_type"]
114
+ notifier.instrument("#{event_type}.rack_attack", request: request)
110
115
 
111
- # Deprecated: Keeping just for backwards compatibility
112
- notifier.instrument("rack.attack", request: request)
116
+ # Deprecated: Keeping just for backwards compatibility
117
+ notifier.instrument("rack.attack", request: request)
118
+ end
113
119
  end
114
- end
115
120
 
116
- def cache
117
- @cache ||= Cache.new
121
+ def cache
122
+ @cache ||= Cache.new
123
+ end
124
+
125
+ def clear_configuration
126
+ @safelists = {}
127
+ @blocklists = {}
128
+ @throttles = {}
129
+ @tracks = {}
130
+ self.anonymous_blocklists = []
131
+ self.anonymous_safelists = []
132
+ end
133
+
134
+ def clear!
135
+ warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
136
+ clear_configuration
137
+ end
118
138
  end
119
139
 
120
- def clear_configuration
121
- @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
122
- self.anonymous_blocklists = []
123
- self.anonymous_safelists = []
140
+ # Set defaults
141
+ @enabled = true
142
+ @anonymous_blocklists = []
143
+ @anonymous_safelists = []
144
+ @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"]]
124
149
  end
125
150
 
126
- def clear!
127
- warn "[DEPRECATION] Rack::Attack.clear! is deprecated. Please use Rack::Attack.clear_configuration instead"
128
- clear_configuration
151
+ def initialize(app)
152
+ @app = app
129
153
  end
130
- end
131
154
 
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
155
+ def call(env)
156
+ return @app.call(env) unless self.class.enabled
157
+
158
+ env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
159
+ request = Rack::Attack::Request.new(env)
145
160
 
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)
161
+ if safelisted?(request)
162
+ @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)
167
+ else
168
+ tracked?(request)
169
+ @app.call(env)
170
+ end
159
171
  end
160
- end
161
172
 
162
- extend Forwardable
163
- def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
173
+ extend Forwardable
174
+ def_delegators self, :safelisted?, :blocklisted?, :throttled?, :tracked?
175
+ end
164
176
  end
@@ -73,7 +73,10 @@ module Rack
73
73
 
74
74
  def enforce_store_method_presence!(method_name)
75
75
  if !store.respond_to?(method_name)
76
- raise Rack::Attack::MisconfiguredStoreError, "Configured store #{store.class.name} doesn't respond to ##{method_name} method"
76
+ raise(
77
+ Rack::Attack::MisconfiguredStoreError,
78
+ "Configured store #{store.class.name} doesn't respond to ##{method_name} method"
79
+ )
77
80
  end
78
81
  end
79
82
  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
 
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Rack::Attack
4
- # When using Rack::Attack with a Rails app, developers expect the request path
5
- # to be normalized. In particular, trailing slashes are stripped.
6
- # (See https://git.io/v0rrR for implementation.)
7
- #
8
- # Look for an ActionDispatch utility class that Rails folks would expect
9
- # to normalize request paths. If unavailable, use a fallback class that
10
- # doesn't normalize the path (as a non-Rails rack app developer expects).
3
+ module Rack
4
+ class Attack
5
+ # When using Rack::Attack with a Rails app, developers expect the request path
6
+ # to be normalized. In particular, trailing slashes are stripped.
7
+ # (See https://git.io/v0rrR for implementation.)
8
+ #
9
+ # Look for an ActionDispatch utility class that Rails folks would expect
10
+ # to normalize request paths. If unavailable, use a fallback class that
11
+ # doesn't normalize the path (as a non-Rails rack app developer expects).
11
12
 
12
- module FallbackPathNormalizer
13
- def self.normalize_path(path)
14
- path
13
+ module FallbackPathNormalizer
14
+ def self.normalize_path(path)
15
+ path
16
+ end
15
17
  end
16
- end
17
18
 
18
- PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
19
- # For Rails apps
20
- ::ActionDispatch::Journey::Router::Utils
21
- else
22
- FallbackPathNormalizer
23
- end
19
+ PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils)
20
+ # For Rails apps
21
+ ::ActionDispatch::Journey::Router::Utils
22
+ else
23
+ FallbackPathNormalizer
24
+ end
25
+ end
24
26
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Attack
5
+ class Railtie < ::Rails::Railtie
6
+ initializer 'rack.attack.middleware', after: :load_config_initializers, before: :build_middleware_stack do |app|
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
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,7 +7,9 @@ module Rack
7
7
  module StoreProxy
8
8
  class ActiveSupportRedisStoreProxy < SimpleDelegator
9
9
  def self.handle?(store)
10
- defined?(::Redis) && defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
10
+ defined?(::Redis) &&
11
+ defined?(::ActiveSupport::Cache::RedisStore) &&
12
+ store.is_a?(::ActiveSupport::Cache::RedisStore)
11
13
  end
12
14
 
13
15
  def increment(name, amount = 1, options = {})
@@ -7,7 +7,9 @@ module Rack
7
7
  module StoreProxy
8
8
  class MemCacheStoreProxy < SimpleDelegator
9
9
  def self.handle?(store)
10
- defined?(::Dalli) && defined?(::ActiveSupport::Cache::MemCacheStore) && store.is_a?(::ActiveSupport::Cache::MemCacheStore)
10
+ defined?(::Dalli) &&
11
+ defined?(::ActiveSupport::Cache::MemCacheStore) &&
12
+ store.is_a?(::ActiveSupport::Cache::MemCacheStore)
11
13
  end
12
14
 
13
15
  def write(name, value, options = {})