diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index 1a5c50d..a2342f5 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -31,7 +31,7 @@ module Rack autoload :Allow2Ban, 'rack/attack/allow2ban' class << self - attr_accessor :enabled, :notifier + attr_accessor :enabled, :notifier, :discriminator_normalizer attr_reader :configuration def instrument(request) @@ -79,6 +79,9 @@ module Rack # Set defaults @enabled = true @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications) + @discriminator_normalizer = lambda do |discriminator| + discriminator.to_s.strip.downcase + end @configuration = Configuration.new attr_reader :configuration diff --git a/lib/rack/attack/throttle.rb b/lib/rack/attack/throttle.rb index 3b80d9e..a1e21a7 100644 --- a/lib/rack/attack/throttle.rb +++ b/lib/rack/attack/throttle.rb @@ -22,8 +22,7 @@ module Rack end def matched_by?(request) - discriminator = block.call(request) - + discriminator = discriminator_for(request) return false unless discriminator current_period = period_for(request) @@ -49,6 +48,14 @@ module Rack private + def discriminator_for(request) + discriminator = block.call(request) + if discriminator && Rack::Attack.discriminator_normalizer + discriminator = Rack::Attack.discriminator_normalizer.call(discriminator) + end + discriminator + end + def period_for(request) period.respond_to?(:call) ? period.call(request) : period end diff --git a/spec/rack_attack_throttle_spec.rb b/spec/rack_attack_throttle_spec.rb index dcb1e41..feb599c 100644 --- a/spec/rack_attack_throttle_spec.rb +++ b/spec/rack_attack_throttle_spec.rb @@ -144,3 +144,47 @@ describe 'Rack::Attack.throttle with block retuning nil' do end end end + +describe 'Rack::Attack.throttle with discriminator_normalizer' do + before do + @period = 60 + @emails = [ + "person@example.com", + "PERSON@example.com ", + " person@example.com\r\n ", + ] + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + Rack::Attack.throttle('logins/email', limit: 4, period: @period) do |req| + if req.path == '/login' && req.post? + req.params['email'] + end + end + end + + it 'should not differentiate requests when discriminator_normalizer is enabled' do + post_logins + key = "rack::attack:#{Time.now.to_i / @period}:logins/email:person@example.com" + _(Rack::Attack.cache.store.read(key)).must_equal 3 + end + + it 'should differentiate requests when discriminator_normalizer is disabled' do + begin + prev = Rack::Attack.discriminator_normalizer + Rack::Attack.discriminator_normalizer = nil + + post_logins + @emails.each do |email| + key = "rack::attack:#{Time.now.to_i / @period}:logins/email:#{email}" + _(Rack::Attack.cache.store.read(key)).must_equal 1 + end + ensure + Rack::Attack.discriminator_normalizer = prev + end + end + + def post_logins + @emails.each do |email| + post '/login', email: email + end + end +end