mirror of
https://github.com/samsonjs/rack-attack.git
synced 2026-03-25 09:25:49 +00:00
Fixes #69. There was a race condition when `Time.now.to_i` changes between when `epoch_time` is computed in line 18, and the cache request is made (and the `key` is expired). I.e., a throttle check starts at t0, but doesn’t reach the cache until t1, the cache will have expired the throttle count. The request will likely be allowed, even if the request exceeded the limit. This has the effect of keeping keys in cache about 1 second longer than strictly necessary. But the extra cache space seems like a good trade-off for correct throttling.
46 lines
1.2 KiB
Ruby
46 lines
1.2 KiB
Ruby
module Rack
|
|
class Attack
|
|
class Cache
|
|
|
|
attr_accessor :prefix
|
|
|
|
def initialize
|
|
self.store = ::Rails.cache if defined?(::Rails.cache)
|
|
@prefix = 'rack::attack'
|
|
end
|
|
|
|
attr_reader :store
|
|
def store=(store)
|
|
@store = StoreProxy.build(store)
|
|
end
|
|
|
|
def count(unprefixed_key, period)
|
|
epoch_time = Time.now.to_i
|
|
# Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA
|
|
expires_in = period - (epoch_time % period) + 1
|
|
key = "#{prefix}:#{(epoch_time/period).to_i}:#{unprefixed_key}"
|
|
do_count(key, expires_in)
|
|
end
|
|
|
|
def read(unprefixed_key)
|
|
store.read("#{prefix}:#{unprefixed_key}")
|
|
end
|
|
|
|
def write(unprefixed_key, value, expires_in)
|
|
store.write("#{prefix}:#{unprefixed_key}", value, :expires_in => expires_in)
|
|
end
|
|
|
|
private
|
|
def do_count(key, expires_in)
|
|
result = store.increment(key, 1, :expires_in => expires_in)
|
|
|
|
# NB: Some stores return nil when incrementing uninitialized values
|
|
if result.nil?
|
|
store.write(key, 1, :expires_in => expires_in)
|
|
end
|
|
result || 1
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|