rack-attack 5.3.2 → 5.4.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: 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