rack-attack 5.3.2 → 5.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: 30b1caccf538327c289b6e4962e2deadc4d7c5fc8509eb1adbf4383c950bd4ad
4
- data.tar.gz: 4b8af97dca4e04414712cce9ba21cc7940f46e2072d3cc9e288b5bf54d066bba
3
+ metadata.gz: 123608043cbaa1604ab2d7b06c056010decd274dd4a5b1fe8f2175cec766fa4d
4
+ data.tar.gz: 2bd3ca91293545b76221608f144c1a43a458fbd9e6f2e8ea2647bf561d8ea9a9
5
5
  SHA512:
6
- metadata.gz: f439d0196c505e73f10671f30d1b9cf919fb1b185468f38915817782cfecdc59d6405bd4d1a895e956de2f788c33524f232bd5c4eadee36dffe1883b0954efdb
7
- data.tar.gz: a736973564ec15f143fbb6b5067441eea50cbc33385a557b1d6d13180b5237982cc676de6ee97989e06fa97185666811945b85d6a8263c19c0a16446407afa22
6
+ metadata.gz: '06388ad68edc65019740c9ee77347f817b0769e782d0f8564ac5ac6b9c7fa819fe2157a10d89b570c78c14ef5a86fe080fac30aba8ebdfed281f2073785f5685'
7
+ data.tar.gz: a15cc2cef9eebda52d662fe3eeee3f187d5b575c089a1fa2cf5e0179012499f6440889349a47108c4017f8b73dd18985655dc933baf8c031e08d73ea23d1dbba
data/README.md CHANGED
@@ -303,13 +303,13 @@ Here's an example response that includes conventional `X-RateLimit-*` headers:
303
303
 
304
304
  ```ruby
305
305
  Rack::Attack.throttled_response = lambda do |env|
306
- now = Time.now
307
306
  match_data = env['rack.attack.match_data']
307
+ now = match_data[:epoch_time]
308
308
 
309
309
  headers = {
310
310
  'X-RateLimit-Limit' => match_data[:limit].to_s,
311
311
  'X-RateLimit-Remaining' => '0',
312
- 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
312
+ 'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
313
313
  }
314
314
 
315
315
  [ 429, headers, ["Throttled\n"]]
@@ -320,7 +320,7 @@ end
320
320
  For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
321
321
 
322
322
  ```ruby
323
- request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
323
+ request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l, :epoch_time => t }
324
324
  ```
325
325
 
326
326
  ## Logging & Instrumentation
data/Rakefile CHANGED
@@ -20,7 +20,8 @@ namespace :test do
20
20
  end
21
21
  end
22
22
 
23
- desc 'Run tests'
24
- task :test => %w[test:units test:integration test:acceptance]
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.pattern = "spec/**/*_spec.rb"
25
+ end
25
26
 
26
27
  task :default => [:rubocop, :test]
@@ -17,6 +17,7 @@ class Rack::Attack
17
17
  autoload :StoreProxy, 'rack/attack/store_proxy'
18
18
  autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
19
19
  autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy'
20
+ autoload :RedisProxy, 'rack/attack/store_proxy/redis_proxy'
20
21
  autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
21
22
  autoload :RedisCacheStoreProxy, 'rack/attack/store_proxy/redis_cache_store_proxy'
22
23
  autoload :Fail2Ban, 'rack/attack/fail2ban'
@@ -2,6 +2,7 @@ module Rack
2
2
  class Attack
3
3
  class Cache
4
4
  attr_accessor :prefix
5
+ attr_reader :last_epoch_time
5
6
 
6
7
  def initialize
7
8
  self.store = ::Rails.cache if defined?(::Rails.cache)
@@ -41,10 +42,10 @@ module Rack
41
42
  private
42
43
 
43
44
  def key_and_expiry(unprefixed_key, period)
44
- epoch_time = Time.now.to_i
45
+ @last_epoch_time = Time.now.to_i
45
46
  # Add 1 to expires_in to avoid timing error: https://git.io/i1PHXA
46
- expires_in = (period - (epoch_time % period) + 1).to_i
47
- ["#{prefix}:#{(epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
47
+ expires_in = (period - (@last_epoch_time % period) + 1).to_i
48
+ ["#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}", expires_in]
48
49
  end
49
50
 
50
51
  def do_count(key, expires_in)
@@ -1,7 +1,7 @@
1
1
  module Rack
2
2
  class Attack
3
3
  module StoreProxy
4
- PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisCacheStoreProxy].freeze
4
+ PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy, RedisProxy, RedisCacheStoreProxy].freeze
5
5
 
6
6
  ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore']).freeze
7
7
  ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Rack
6
+ class Attack
7
+ module StoreProxy
8
+ class RedisProxy < SimpleDelegator
9
+ def initialize(*args)
10
+ if Gem::Version.new(Redis::VERSION) < Gem::Version.new("3")
11
+ warn 'RackAttack requires Redis gem >= 3.0.0.'
12
+ end
13
+
14
+ super(*args)
15
+ end
16
+
17
+ def self.handle?(store)
18
+ defined?(::Redis) && store.is_a?(::Redis)
19
+ end
20
+
21
+ def read(key)
22
+ get(key)
23
+ rescue Redis::BaseError
24
+ end
25
+
26
+ def write(key, value, options = {})
27
+ if (expires_in = options[:expires_in])
28
+ setex(key, expires_in, value)
29
+ else
30
+ set(key, value)
31
+ end
32
+ rescue Redis::BaseError
33
+ end
34
+
35
+ def increment(key, amount, options = {})
36
+ count = nil
37
+
38
+ pipelined do
39
+ count = incrby(key, amount)
40
+ expire(key, options[:expires_in]) if options[:expires_in]
41
+ end
42
+
43
+ count.value if count
44
+ rescue Redis::BaseError
45
+ end
46
+
47
+ def delete(key, _options = {})
48
+ del(key)
49
+ rescue Redis::BaseError
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,15 +3,11 @@ require 'delegate'
3
3
  module Rack
4
4
  class Attack
5
5
  module StoreProxy
6
- class RedisStoreProxy < SimpleDelegator
6
+ class RedisStoreProxy < RedisProxy
7
7
  def self.handle?(store)
8
8
  defined?(::Redis::Store) && store.is_a?(::Redis::Store)
9
9
  end
10
10
 
11
- def initialize(store)
12
- super(store)
13
- end
14
-
15
11
  def read(key)
16
12
  get(key, raw: true)
17
13
  rescue Redis::BaseError
@@ -25,23 +21,6 @@ module Rack
25
21
  end
26
22
  rescue Redis::BaseError
27
23
  end
28
-
29
- def increment(key, amount, options = {})
30
- count = nil
31
-
32
- pipelined do
33
- count = incrby(key, amount)
34
- expire(key, options[:expires_in]) if options[:expires_in]
35
- end
36
-
37
- count.value if count
38
- rescue Redis::BaseError
39
- end
40
-
41
- def delete(key, _options = {})
42
- del(key)
43
- rescue Redis::BaseError
44
- end
45
24
  end
46
25
  end
47
26
  end
@@ -26,12 +26,15 @@ module Rack
26
26
  current_limit = limit.respond_to?(:call) ? limit.call(request) : limit
27
27
  key = "#{name}:#{discriminator}"
28
28
  count = cache.count(key, current_period)
29
+ epoch_time = cache.last_epoch_time
29
30
 
30
31
  data = {
31
32
  :count => count,
32
33
  :period => current_period,
33
- :limit => current_limit
34
+ :limit => current_limit,
35
+ :epoch_time => epoch_time
34
36
  }
37
+
35
38
  (request.env['rack.attack.throttle_data'] ||= {})[name] = data
36
39
 
37
40
  (count > current_limit).tap do |throttled|
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class Attack
3
- VERSION = '5.3.2'
3
+ VERSION = '5.4.0'
4
4
  end
5
5
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ if defined?(::Redis)
6
+ require_relative "../../support/cache_store_helper"
7
+ require "timecop"
8
+
9
+ describe "Plain redis as a cache backend" do
10
+ before do
11
+ Rack::Attack.cache.store = Redis.new
12
+ end
13
+
14
+ after do
15
+ Rack::Attack.cache.store.flushdb
16
+ end
17
+
18
+ it_works_for_cache_backed_features
19
+
20
+ it "doesn't leak keys" do
21
+ Rack::Attack.throttle("by ip", limit: 1, period: 1) do |request|
22
+ request.ip
23
+ end
24
+
25
+ key = nil
26
+
27
+ # Freeze time during these statement to be sure that the key used by rack attack is the same
28
+ # we pre-calculate in local variable `key`
29
+ Timecop.freeze do
30
+ key = "rack::attack:#{Time.now.to_i}:by ip:1.2.3.4"
31
+
32
+ get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
33
+ end
34
+
35
+ assert Rack::Attack.cache.store.get(key)
36
+
37
+ sleep 2.1
38
+
39
+ assert_nil Rack::Attack.cache.store.get(key)
40
+ end
41
+ end
42
+ end
@@ -20,7 +20,7 @@ describe 'Rack::Attack.throttle' do
20
20
  end
21
21
 
22
22
  it 'should populate throttle data' do
23
- data = { :count => 1, :limit => 1, :period => @period }
23
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
24
24
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
25
25
  end
26
26
  end
@@ -37,7 +37,7 @@ describe 'Rack::Attack.throttle' do
37
37
  it 'should tag the env' do
38
38
  last_request.env['rack.attack.matched'].must_equal 'ip/sec'
39
39
  last_request.env['rack.attack.match_type'].must_equal :throttle
40
- last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period)
40
+ last_request.env['rack.attack.match_data'].must_equal(:count => 2, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i)
41
41
  last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4')
42
42
  end
43
43
 
@@ -65,7 +65,7 @@ describe 'Rack::Attack.throttle with limit as proc' do
65
65
  end
66
66
 
67
67
  it 'should populate throttle data' do
68
- data = { :count => 1, :limit => 1, :period => @period }
68
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
69
69
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
70
70
  end
71
71
  end
@@ -89,7 +89,7 @@ describe 'Rack::Attack.throttle with period as proc' do
89
89
  end
90
90
 
91
91
  it 'should populate throttle data' do
92
- data = { :count => 1, :limit => 1, :period => @period }
92
+ data = { :count => 1, :limit => 1, :period => @period, epoch_time: Rack::Attack.cache.last_epoch_time.to_i }
93
93
  last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
94
94
  end
95
95
  end
@@ -1,4 +1,3 @@
1
- require "rubygems"
2
1
  require "bundler/setup"
3
2
 
4
3
  require "minitest/autorun"
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: 5.3.2
4
+ version: 5.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: 2018-06-25 00:00:00.000000000 Z
11
+ date: 2018-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: appraisal
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +44,20 @@ dependencies:
38
44
  - - "~>"
39
45
  - !ruby/object:Gem::Version
40
46
  version: '2.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.16'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.16'
41
61
  - !ruby/object:Gem::Dependency
42
62
  name: minitest
43
63
  requirement: !ruby/object:Gem::Requirement
@@ -140,30 +160,30 @@ dependencies:
140
160
  name: actionpack
141
161
  requirement: !ruby/object:Gem::Requirement
142
162
  requirements:
143
- - - ">="
163
+ - - "~>"
144
164
  - !ruby/object:Gem::Version
145
- version: 3.0.0
165
+ version: '5.2'
146
166
  type: :development
147
167
  prerelease: false
148
168
  version_requirements: !ruby/object:Gem::Requirement
149
169
  requirements:
150
- - - ">="
170
+ - - "~>"
151
171
  - !ruby/object:Gem::Version
152
- version: 3.0.0
172
+ version: '5.2'
153
173
  - !ruby/object:Gem::Dependency
154
174
  name: activesupport
155
175
  requirement: !ruby/object:Gem::Requirement
156
176
  requirements:
157
- - - ">="
177
+ - - "~>"
158
178
  - !ruby/object:Gem::Version
159
- version: 3.0.0
179
+ version: '5.2'
160
180
  type: :development
161
181
  prerelease: false
162
182
  version_requirements: !ruby/object:Gem::Requirement
163
183
  requirements:
164
- - - ">="
184
+ - - "~>"
165
185
  - !ruby/object:Gem::Version
166
- version: 3.0.0
186
+ version: '5.2'
167
187
  description: A rack middleware for throttling and blocking abusive requests
168
188
  email: aaron@ktheory.com
169
189
  executables: []
@@ -185,6 +205,7 @@ files:
185
205
  - lib/rack/attack/store_proxy/dalli_proxy.rb
186
206
  - lib/rack/attack/store_proxy/mem_cache_proxy.rb
187
207
  - lib/rack/attack/store_proxy/redis_cache_store_proxy.rb
208
+ - lib/rack/attack/store_proxy/redis_proxy.rb
188
209
  - lib/rack/attack/store_proxy/redis_store_proxy.rb
189
210
  - lib/rack/attack/throttle.rb
190
211
  - lib/rack/attack/track.rb
@@ -212,6 +233,7 @@ files:
212
233
  - spec/acceptance/stores/active_support_redis_store_spec.rb
213
234
  - spec/acceptance/stores/connection_pool_dalli_client_spec.rb
214
235
  - spec/acceptance/stores/dalli_client_spec.rb
236
+ - spec/acceptance/stores/redis_spec.rb
215
237
  - spec/acceptance/stores/redis_store_spec.rb
216
238
  - spec/acceptance/throttling_spec.rb
217
239
  - spec/acceptance/track_spec.rb
@@ -284,6 +306,7 @@ test_files:
284
306
  - spec/acceptance/stores/active_support_dalli_store_spec.rb
285
307
  - spec/acceptance/stores/redis_store_spec.rb
286
308
  - spec/acceptance/stores/dalli_client_spec.rb
309
+ - spec/acceptance/stores/redis_spec.rb
287
310
  - spec/acceptance/customizing_blocked_response_spec.rb
288
311
  - spec/spec_helper.rb
289
312
  - spec/allow2ban_spec.rb