diff --git a/README.md b/README.md index 66aa310..3cb88ef 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ # Rack::Attack - middleware for throttling & blocking abusive clients + +## Processing order + * If any whitelist matches, the request is allowed + * If any blacklist matches, the request is blocked (unless a whitelist matched) + * If any throttle matches, the request is throttled (unless a whitelist or blacklist matched) diff --git a/examples/rack_attack.rb b/examples/rack_attack.rb index 0ebabd5..81d3d0b 100644 --- a/examples/rack_attack.rb +++ b/examples/rack_attack.rb @@ -15,17 +15,11 @@ Rack::Attack.throttle "logins/email", :limit => 2, :period => 60 do |req| req.params['email'] unless req.params['email'].blank? end - -# Block cloud IPs from accessing PATH regexp -Rack::Attack.block "bad_ips from logging in" do |req| +# Blacklist cloud IPs from accessing PATH regexp +Rack::Attack.blacklist "bad_ips from logging in" do |req| req.path =~ /^login/ && bad_ips.include?(req.ip) end -# Throttle login attempts -Rack::Attack.throttle "logins/ip", :limit => 2, :period => 1.second do | req| - req.ip if req.post? && req.path_info =~ /^login/ -end - # Whitelist a User-Agent Rack::Attack.whitelist 'internal user agent' do |req| req.user_agent =~ 'InternalUserAgent' diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index c863843..ccb69af 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -2,17 +2,19 @@ require 'rack' module Rack::Attack require 'rack/attack/cache' require 'rack/attack/throttle' + require 'rack/attack/whitelist' + require 'rack/attack/blacklist' class << self attr_reader :cache, :notifier def whitelist(name, &block) - (@whitelists ||= {})[name] = block + (@whitelists ||= {})[name] = Whitelist.new(name, block) end - def block(name, &block) - (@blocks ||= {})[name] = block + def blacklist(name, &block) + (@blacklists ||= {})[name] = Blacklist.new(name, block) end def throttle(name, options, &block) @@ -20,7 +22,7 @@ module Rack::Attack end def whitelists; @whitelists ||= {}; end - def blocks; @blocks ||= {}; end + def blacklists; @blacklists ||= {}; end def throttles; @throttles ||= {}; end def new(app) @@ -38,8 +40,8 @@ module Rack::Attack return @app.call(env) end - if blocked?(req) - blocked_response + if blacklisted?(req) + blacklisted_response elsif throttled?(req) throttled_response else @@ -48,18 +50,14 @@ module Rack::Attack end def whitelisted?(req) - whitelists.any? do |name, block| - block[req].tap{ |match| - instrument(:type => :whitelist, :name => name, :request => req) if match - } + whitelists.any? do |name, whitelist| + whitelist[req] end end - def blocked?(req) - blocks.any? do |name, block| - block[req].tap { |match| - instrument(:type => :block, :name => name, :request => req) if match - } + def blacklisted?(req) + blacklists.any? do |name, blacklist| + blacklist[req] end end @@ -73,7 +71,7 @@ module Rack::Attack notifier.instrument('rack.attack', payload) if notifier end - def blocked_response + def blacklisted_response [503, {}, ['Blocked']] end @@ -82,7 +80,7 @@ module Rack::Attack end def clear! - @whitelists, @blocks, @throttles = {}, {}, {} + @whitelists, @blacklists, @throttles = {}, {}, {} end end diff --git a/lib/rack/attack/blacklist.rb b/lib/rack/attack/blacklist.rb new file mode 100644 index 0000000..452d51d --- /dev/null +++ b/lib/rack/attack/blacklist.rb @@ -0,0 +1,13 @@ +require_relative 'check' +module Rack + module Attack + class Blacklist < Check + def initialize(name, block) + super + @type = :blacklist + end + + end + end +end + diff --git a/lib/rack/attack/check.rb b/lib/rack/attack/check.rb new file mode 100644 index 0000000..f7aaee3 --- /dev/null +++ b/lib/rack/attack/check.rb @@ -0,0 +1,19 @@ +module Rack + module Attack + class Check + attr_reader :name, :block, :type + def initialize(name, block) + @name, @block = name, block + @type = nil + end + + def [](req) + block[req].tap {|match| + Rack::Attack.instrument(:type => type, :name => name, :request => req) if match + } + end + + end + end +end + diff --git a/lib/rack/attack/throttle.rb b/lib/rack/attack/throttle.rb index f31d2ad..020439d 100644 --- a/lib/rack/attack/throttle.rb +++ b/lib/rack/attack/throttle.rb @@ -16,7 +16,7 @@ module Rack end def [](req) - discriminator = @block[req] + discriminator = block[req] return false unless discriminator key = "#{name}:#{discriminator}" diff --git a/lib/rack/attack/version.rb b/lib/rack/attack/version.rb index 2a9e639..3d4b612 100644 --- a/lib/rack/attack/version.rb +++ b/lib/rack/attack/version.rb @@ -1,5 +1,5 @@ module Rack module Attack - VERSION = '0.0.2' + VERSION = '0.0.3' end end diff --git a/lib/rack/attack/whitelist.rb b/lib/rack/attack/whitelist.rb index 93b7e8f..0d5f3b1 100644 --- a/lib/rack/attack/whitelist.rb +++ b/lib/rack/attack/whitelist.rb @@ -1,12 +1,10 @@ +require_relative 'check' module Rack module Attack - class Whitelist - attr_reader :name, :block - def initialize(name, &block) - @name, @block = name, block - end - - def [](req) + class Whitelist < Check + def initialize(name, block) + super + @type = :whitelist end end diff --git a/spec/rack_attack_spec.rb b/spec/rack_attack_spec.rb index 469a748..e8fc78d 100644 --- a/spec/rack_attack_spec.rb +++ b/spec/rack_attack_spec.rb @@ -22,15 +22,15 @@ describe 'Rack::Attack' do allow_ok_requests - describe 'with a block' do + describe 'with a blacklist' do before do @bad_ip = '1.2.3.4' - Rack::Attack.block("ip #{@bad_ip}") {|req| req.ip == @bad_ip } + Rack::Attack.blacklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip } end - it('has a block') { Rack::Attack.blocks.key?("ip #{@bad_ip}") } + it('has a blacklist') { Rack::Attack.blacklists.key?("ip #{@bad_ip}") } - it "should block bad requests" do + it "should blacklist bad requests" do get '/', {}, 'REMOTE_ADDR' => @bad_ip last_response.status.must_equal 503 end @@ -44,7 +44,7 @@ describe 'Rack::Attack' do end it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") } - it "should allow whitelists before blocks" do + it "should allow whitelists before blacklists" do get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua last_response.status.must_equal 200 end