rack-attack 6.1.0 → 6.2.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: 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 = {})