Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ Style/SpecialGlobalVars:

Style/UnneededPercentQ:
Enabled: true

Lint/UnusedMethodArgument:
Exclude:
- "lib/rack/attack/store_adapter.rb"
12 changes: 6 additions & 6 deletions lib/rack/attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
require 'rack/attack/configuration'
require 'rack/attack/path_normalizer'
require 'rack/attack/request'
require 'rack/attack/store_proxy/dalli_proxy'
require 'rack/attack/store_proxy/mem_cache_store_proxy'
require 'rack/attack/store_proxy/redis_proxy'
require 'rack/attack/store_proxy/redis_store_proxy'
require 'rack/attack/store_proxy/redis_cache_store_proxy'
require 'rack/attack/store_proxy/active_support_redis_store_proxy'
require 'rack/attack/store_adapters/dalli_adapter'
require 'rack/attack/store_adapters/mem_cache_store_adapter'
require 'rack/attack/store_adapters/redis_adapter'
require 'rack/attack/store_adapters/redis_store_adapter'
require 'rack/attack/store_adapters/redis_cache_store_adapter'
require 'rack/attack/store_adapters/active_support_redis_store_adapter'

require 'rack/attack/railtie' if defined?(::Rails)

Expand Down
27 changes: 0 additions & 27 deletions lib/rack/attack/base_proxy.rb

This file was deleted.

39 changes: 13 additions & 26 deletions lib/rack/attack/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ def initialize

attr_reader :store
def store=(store)
@store =
if (proxy = BaseProxy.lookup(store))
proxy.new(store)
else
store
end
raise Rack::Attack::MissingStoreError if store.nil?

adapter = StoreAdapter.lookup(store)
if adapter
@store = adapter.new(store)
elsif store?(store)
@store = store
else
raise Rack::Attack::MisconfiguredStoreError
end
end

def count(unprefixed_key, period)
Expand All @@ -27,9 +31,6 @@ def count(unprefixed_key, period)
end

def read(unprefixed_key)
enforce_store_presence!
enforce_store_method_presence!(:read)

store.read("#{prefix}:#{unprefixed_key}")
end

Expand Down Expand Up @@ -67,33 +68,19 @@ def key_and_expiry(unprefixed_key, period)
end

def do_count(key, expires_in)
enforce_store_presence!
enforce_store_method_presence!(:increment)

result = store.increment(key, 1, expires_in: expires_in)

# NB: Some stores return nil when incrementing uninitialized values
if result.nil?
enforce_store_method_presence!(:write)

store.write(key, 1, expires_in: expires_in)
end
result || 1
end

def enforce_store_presence!
if store.nil?
raise Rack::Attack::MissingStoreError
end
end
STORE_METHODS = [:read, :write, :increment, :delete].freeze

def enforce_store_method_presence!(method_name)
if !store.respond_to?(method_name)
raise(
Rack::Attack::MisconfiguredStoreError,
"Configured store #{store.class.name} doesn't respond to ##{method_name} method"
)
end
def store?(object)
STORE_METHODS.all? { |meth| object.respond_to?(meth) }
end
end
end
Expand Down
47 changes: 47 additions & 0 deletions lib/rack/attack/store_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module Rack
class Attack
class StoreAdapter
class << self
def adapters
@@adapters ||= []
end

def inherited(klass)
adapters << klass
end

def lookup(store)
adapters.find { |adapter| adapter.handle?(store) }
end

def handle?(store)
raise NotImplementedError
end
end

attr_reader :store

def initialize(store)
@store = store
end

def read(key)
raise NotImplementedError
end

def write(key, value, options = {})
raise NotImplementedError
end

def increment(key, amount, options = {})
raise NotImplementedError
end

def delete(key, options = {})
raise NotImplementedError
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require 'rack/attack/store_adapter'

module Rack
class Attack
module StoreAdapters
class ActiveSupportRedisStoreAdapter < StoreAdapter
def self.handle?(store)
defined?(::Redis) &&
defined?(::ActiveSupport::Cache::RedisStore) &&
store.is_a?(::ActiveSupport::Cache::RedisStore)
end

def read(key, options = {})
store.read(key, options.merge!(raw: true))
end

def write(key, value, options = {})
store.write(key, value, options.merge!(raw: true))
end

def increment(key, amount = 1, options = {})
# #increment ignores options[:expires_in].
#
# So in order to workaround this we use #write (which sets expiration) to initialize
# the counter. After that we continue using the original #increment.
if options[:expires_in] && !read(key)
write(key, amount, options)

amount
else
store.increment(key, amount, options)
end
end

def delete(key, options = {})
store.delete(key, options)
end

def delete_matched(matcher, options = nil)
store.delete_matched(matcher, options)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

require 'rack/attack/base_proxy'
require 'rack/attack/store_adapter'

module Rack
class Attack
module StoreProxy
class DalliProxy < BaseProxy
module StoreAdapters
class DalliAdapter < StoreAdapter
def self.handle?(store)
return false unless defined?(::Dalli)

Expand All @@ -18,38 +18,38 @@ def self.handle?(store)
end
end

def initialize(client)
super(client)
def initialize(store)
super
stub_with_if_missing
end

def read(key)
rescuing do
with do |client|
store.with do |client|
client.get(key)
end
end
end

def write(key, value, options = {})
rescuing do
with do |client|
store.with do |client|
client.set(key, value, options.fetch(:expires_in, 0), raw: true)
end
end
end

def increment(key, amount, options = {})
rescuing do
with do |client|
store.with do |client|
client.incr(key, amount, options.fetch(:expires_in, 0), amount)
end
end
end

def delete(key)
rescuing do
with do |client|
store.with do |client|
client.delete(key)
end
end
Expand All @@ -58,10 +58,10 @@ def delete(key)
private

def stub_with_if_missing
unless __getobj__.respond_to?(:with)
class << self
unless store.respond_to?(:with)
class << store
def with
yield __getobj__
yield store
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
# frozen_string_literal: true

require 'rack/attack/base_proxy'
require 'forwardable'

module Rack
class Attack
module StoreProxy
class MemCacheStoreProxy < BaseProxy
module StoreAdapters
class MemCacheStoreAdapter < StoreAdapter
def self.handle?(store)
defined?(::Dalli) &&
defined?(::ActiveSupport::Cache::MemCacheStore) &&
store.is_a?(::ActiveSupport::Cache::MemCacheStore)
end

def write(name, value, options = {})
super(name, value, options.merge!(raw: true))
extend Forwardable
def_delegators :@store, :read, :increment, :delete

def write(key, value, options = {})
store.write(key, value, options.merge!(raw: true))
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
# frozen_string_literal: true

require 'rack/attack/base_proxy'
require 'rack/attack/store_adapter'

module Rack
class Attack
module StoreProxy
class RedisProxy < BaseProxy
def initialize(*args)
module StoreAdapters
class RedisAdapter < StoreAdapter
def initialize(store)
if Gem::Version.new(Redis::VERSION) < Gem::Version.new("3")
warn 'RackAttack requires Redis gem >= 3.0.0.'
end

super(*args)
super
end

def self.handle?(store)
defined?(::Redis) && store.class == ::Redis
end

def read(key)
rescuing { get(key) }
rescuing { store.get(key) }
end

def write(key, value, options = {})
if (expires_in = options[:expires_in])
rescuing { setex(key, expires_in, value) }
rescuing { store.setex(key, expires_in, value) }
else
rescuing { set(key, value) }
rescuing { store.set(key, value) }
end
end

def increment(key, amount, options = {})
rescuing do
pipelined do
incrby(key, amount)
expire(key, options[:expires_in]) if options[:expires_in]
store.pipelined do
store.incrby(key, amount)
store.expire(key, options[:expires_in]) if options[:expires_in]
end.first
end
end

def delete(key, _options = {})
rescuing { del(key) }
rescuing { store.del(key) }
end

def delete_matched(matcher, _options = nil)
Expand All @@ -49,8 +49,8 @@ def delete_matched(matcher, _options = nil)
rescuing do
# Fetch keys in batches using SCAN to avoid blocking the Redis server.
loop do
cursor, keys = scan(cursor, match: matcher, count: 1000)
del(*keys) unless keys.empty?
cursor, keys = store.scan(cursor, match: matcher, count: 1000)
store.del(*keys) unless keys.empty?
break if cursor == "0"
end
end
Expand Down
Loading