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
38 changes: 20 additions & 18 deletions lib/rack/attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ class MissingStoreError < StandardError; end
autoload :Request, 'rack/attack/request'

class << self

attr_accessor :notifier, :blocklisted_response, :throttled_response

def safelist(name, &block)
self.safelists[name] = Safelist.new(name, block)
safelists[name] = Safelist.new(name, block)
end

def whitelist(name, &block)
Expand All @@ -34,7 +33,7 @@ def whitelist(name, &block)
end

def blocklist(name, &block)
self.blocklists[name] = Blocklist.new(name, block)
blocklists[name] = Blocklist.new(name, block)
end

def blacklist(name, &block)
Expand All @@ -43,11 +42,11 @@ def blacklist(name, &block)
end

def throttle(name, options, &block)
self.throttles[name] = Throttle.new(name, options, block)
throttles[name] = Throttle.new(name, options, block)
end

def track(name, options = {}, &block)
self.tracks[name] = Track.new(name, options, block)
tracks[name] = Track.new(name, options, block)
end

def safelists; @safelists ||= {}; end
Expand All @@ -66,7 +65,7 @@ def blacklists
end

def safelisted?(req)
safelists.any? do |name, safelist|
safelists.any? do |_name, safelist|
safelist[req]
end
end
Expand All @@ -77,7 +76,7 @@ def whitelisted?(req)
end

def blocklisted?(req)
blocklists.any? do |name, blocklist|
blocklists.any? do |_name, blocklist|
blocklist[req]
end
end
Expand All @@ -88,7 +87,7 @@ def blacklisted?(req)
end

def throttled?(req)
throttles.any? do |name, throttle|
throttles.any? do |_name, throttle|
throttle[req]
end
end
Expand All @@ -108,27 +107,29 @@ def cache
end

def clear!
@safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {}
@safelists = {}
@blocklists = {}
@throttles = {}
@tracks = {}
end

def blacklisted_response=(res)
warn "[DEPRECATION] 'Rack::Attack.blacklisted_response=' is deprecated. Please use 'blocklisted_response=' instead."
self.blocklisted_response=(res)
self.blocklisted_response = res
end

def blacklisted_response
warn "[DEPRECATION] 'Rack::Attack.blacklisted_response' is deprecated. Please use 'blocklisted_response' instead."
blocklisted_response
end

end

# Set defaults
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
@blocklisted_response = lambda {|env| [403, {'Content-Type' => 'text/plain'}, ["Forbidden\n"]] }
@throttled_response = lambda {|env|
@blocklisted_response = ->(_env) { [403, { 'Content-Type' => 'text/plain' }, ["Forbidden\n"]] }
@throttled_response = lambda { |env|
retry_after = (env['rack.attack.match_data'] || {})[:period]
[429, {'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
[429, { 'Content-Type' => 'text/plain', 'Retry-After' => retry_after.to_s }, ["Retry later\n"]]
}

def initialize(app)
Expand All @@ -152,8 +153,9 @@ def call(env)
end

extend Forwardable
def_delegators self, :safelisted?,
:blocklisted?,
:throttled?,
:tracked?
def_delegators self,
:safelisted?,
:blocklisted?,
:throttled?,
:tracked?
end
5 changes: 2 additions & 3 deletions lib/rack/attack/allow2ban.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Attack
class Allow2Ban < Fail2Ban
class << self
protected

def key_prefix
'allow2ban'
end
Expand All @@ -11,9 +12,7 @@ def key_prefix
# (blocking the request) if they have tripped the limit.
def fail!(discriminator, bantime, findtime, maxretry)
count = cache.count("#{key_prefix}:count:#{discriminator}", findtime)
if count >= maxretry
ban!(discriminator, bantime)
end
ban!(discriminator, bantime) if count >= maxretry
# we may not block them this time, but they're banned for next time
false
end
Expand Down
1 change: 0 additions & 1 deletion lib/rack/attack/blocklist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ def initialize(name, block)
super
@type = :blocklist
end

end
end
end
5 changes: 2 additions & 3 deletions lib/rack/attack/cache.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module Rack
class Attack
class Cache

attr_accessor :prefix

def initialize
Expand All @@ -24,11 +23,11 @@ def read(unprefixed_key)
end

def write(unprefixed_key, value, expires_in)
store.write("#{prefix}:#{unprefixed_key}", value, :expires_in => expires_in)
store.write("#{prefix}:#{unprefixed_key}", value, expires_in: expires_in)
end

def reset_count(unprefixed_key, period)
key, _ = key_and_expiry(unprefixed_key, period)
key, _expires_in = key_and_expiry(unprefixed_key, period)
store.delete(key)
end

Expand Down
10 changes: 5 additions & 5 deletions lib/rack/attack/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ module Rack
class Attack
class Check
attr_reader :name, :block, :type

def initialize(name, options = {}, block)
@name, @block = name, block
@name = name
@block = block
@type = options.fetch(:type, nil)
end

def [](req)
block[req].tap {|match|
block[req].tap do |match|
if match
req.env["rack.attack.matched"] = name
req.env["rack.attack.match_type"] = type
Rack::Attack.instrument(req)
end
}
end
end

end
end
end

15 changes: 7 additions & 8 deletions lib/rack/attack/fail2ban.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ class Attack
class Fail2Ban
class << self
def filter(discriminator, options)
bantime = options[:bantime] or raise ArgumentError, "Must pass bantime option"
findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
maxretry = options[:maxretry] or raise ArgumentError, "Must pass maxretry option"
bantime = options[:bantime] || raise(ArgumentError, "Must pass bantime option")
findtime = options[:findtime] || raise(ArgumentError, "Must pass findtime option")
maxretry = options[:maxretry] || raise(ArgumentError, "Must pass maxretry option")

if banned?(discriminator)
# Return true for blocklist
Expand All @@ -16,7 +16,7 @@ def filter(discriminator, options)
end

def reset(discriminator, options)
findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
findtime = options[:findtime] || raise(ArgumentError, "Must pass findtime option")
cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime)
# Clear ban flag just in case it's there
cache.delete("#{key_prefix}:ban:#{discriminator}")
Expand All @@ -27,21 +27,20 @@ def banned?(discriminator)
end

protected

def key_prefix
'fail2ban'
end

def fail!(discriminator, bantime, findtime, maxretry)
count = cache.count("#{key_prefix}:count:#{discriminator}", findtime)
if count >= maxretry
ban!(discriminator, bantime)
end
ban!(discriminator, bantime) if count >= maxretry

true
end


private

def ban!(discriminator, bantime)
cache.write("#{key_prefix}:ban:#{discriminator}", 1, bantime)
end
Expand Down
1 change: 0 additions & 1 deletion lib/rack/attack/safelist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ def initialize(name, block)
super
@type = :safelist
end

end
end
end
2 changes: 1 addition & 1 deletion lib/rack/attack/store_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ def self.build(store)
klass ? klass.new(client) : client
end


private

def self.unwrap_active_support_stores(store)
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
# so use the raw Redis::Store instead.
Expand Down
12 changes: 5 additions & 7 deletions lib/rack/attack/store_proxy/dalli_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ def read(key)
rescue Dalli::DalliError
end

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

def increment(key, amount, options={})
def increment(key, amount, options = {})
with do |client|
client.incr(key, amount, options.fetch(:expires_in, 0), amount)
end
Expand All @@ -52,13 +52,11 @@ def delete(key)
private

def stub_with_if_missing
unless __getobj__.respond_to?(:with)
class << self
def with; yield __getobj__; end
end
return if __getobj__.respond_to?(:with)
class << self
def with; yield __getobj__; end
end
end

end
end
end
Expand Down
14 changes: 6 additions & 8 deletions lib/rack/attack/store_proxy/mem_cache_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ def read(key)
rescue MemCache::MemCacheError
end

def write(key, value, options={})
def write(key, value, options = {})
# Third argument: writing raw value
set(key, value, options.fetch(:expires_in, 0), true)
rescue MemCache::MemCacheError
end

def increment(key, amount, options={})
def increment(key, amount, _options = {})
incr(key, amount)
rescue MemCache::MemCacheError
end

def delete(key, options={})
def delete(key, _options = {})
with do |client|
client.delete(key)
end
Expand All @@ -38,13 +38,11 @@ def delete(key, options={})
private

def stub_with_if_missing
unless __getobj__.respond_to?(:with)
class << self
def with; yield __getobj__; end
end
return if __getobj__.respond_to?(:with)
class << self
def with; yield __getobj__; end
end
end

end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/rack/attack/store_proxy/redis_store_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def read(key)
rescue Redis::BaseError
end

def write(key, value, options={})
def write(key, value, options = {})
if (expires_in = options[:expires_in])
setex(key, expires_in, value, raw: true)
else
Expand All @@ -33,7 +33,7 @@ def write(key, value, options={})
rescue Redis::BaseError
end

def increment(key, amount, options={})
def increment(key, amount, options = {})
count = nil

pipelined do
Expand All @@ -45,7 +45,7 @@ def increment(key, amount, options={})
rescue Redis::BaseError
end

def delete(key, options={})
def delete(key, _options = {})
del(key)
rescue Redis::BaseError
end
Expand Down
14 changes: 8 additions & 6 deletions lib/rack/attack/throttle.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
module Rack
class Attack
class Throttle
MANDATORY_OPTIONS = [:limit, :period].freeze
MANDATORY_OPTIONS = %i[limit period].freeze

attr_reader :name, :limit, :period, :block, :type

def initialize(name, options, block)
@name, @block = name, block
@name = name
@block = block
MANDATORY_OPTIONS.each do |opt|
raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt]
raise ArgumentError, "Must pass #{opt.inspect} option" unless options[opt]
end
@limit = options[:limit]
@period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i
Expand All @@ -28,9 +30,9 @@ def [](req)
count = cache.count(key, current_period)

data = {
:count => count,
:period => current_period,
:limit => current_limit
count: count,
period: current_period,
limit: current_limit
}
(req.env['rack.attack.throttle_data'] ||= {})[name] = data

Expand Down
Loading