Initial working version

This commit is contained in:
Aaron Suggs 2012-07-27 17:40:11 -04:00
parent e166e87fb9
commit dc2e402812
9 changed files with 66 additions and 39 deletions

View file

@ -1 +1,6 @@
# Rack::Attack - middleware for throttling & blocking abusive clients # 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)

View file

@ -15,17 +15,11 @@ Rack::Attack.throttle "logins/email", :limit => 2, :period => 60 do |req|
req.params['email'] unless req.params['email'].blank? req.params['email'] unless req.params['email'].blank?
end end
# Blacklist cloud IPs from accessing PATH regexp
# Block cloud IPs from accessing PATH regexp Rack::Attack.blacklist "bad_ips from logging in" do |req|
Rack::Attack.block "bad_ips from logging in" do |req|
req.path =~ /^login/ && bad_ips.include?(req.ip) req.path =~ /^login/ && bad_ips.include?(req.ip)
end 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 # Whitelist a User-Agent
Rack::Attack.whitelist 'internal user agent' do |req| Rack::Attack.whitelist 'internal user agent' do |req|
req.user_agent =~ 'InternalUserAgent' req.user_agent =~ 'InternalUserAgent'

View file

@ -2,17 +2,19 @@ require 'rack'
module Rack::Attack module Rack::Attack
require 'rack/attack/cache' require 'rack/attack/cache'
require 'rack/attack/throttle' require 'rack/attack/throttle'
require 'rack/attack/whitelist'
require 'rack/attack/blacklist'
class << self class << self
attr_reader :cache, :notifier attr_reader :cache, :notifier
def whitelist(name, &block) def whitelist(name, &block)
(@whitelists ||= {})[name] = block (@whitelists ||= {})[name] = Whitelist.new(name, block)
end end
def block(name, &block) def blacklist(name, &block)
(@blocks ||= {})[name] = block (@blacklists ||= {})[name] = Blacklist.new(name, block)
end end
def throttle(name, options, &block) def throttle(name, options, &block)
@ -20,7 +22,7 @@ module Rack::Attack
end end
def whitelists; @whitelists ||= {}; end def whitelists; @whitelists ||= {}; end
def blocks; @blocks ||= {}; end def blacklists; @blacklists ||= {}; end
def throttles; @throttles ||= {}; end def throttles; @throttles ||= {}; end
def new(app) def new(app)
@ -38,8 +40,8 @@ module Rack::Attack
return @app.call(env) return @app.call(env)
end end
if blocked?(req) if blacklisted?(req)
blocked_response blacklisted_response
elsif throttled?(req) elsif throttled?(req)
throttled_response throttled_response
else else
@ -48,18 +50,14 @@ module Rack::Attack
end end
def whitelisted?(req) def whitelisted?(req)
whitelists.any? do |name, block| whitelists.any? do |name, whitelist|
block[req].tap{ |match| whitelist[req]
instrument(:type => :whitelist, :name => name, :request => req) if match
}
end end
end end
def blocked?(req) def blacklisted?(req)
blocks.any? do |name, block| blacklists.any? do |name, blacklist|
block[req].tap { |match| blacklist[req]
instrument(:type => :block, :name => name, :request => req) if match
}
end end
end end
@ -73,7 +71,7 @@ module Rack::Attack
notifier.instrument('rack.attack', payload) if notifier notifier.instrument('rack.attack', payload) if notifier
end end
def blocked_response def blacklisted_response
[503, {}, ['Blocked']] [503, {}, ['Blocked']]
end end
@ -82,7 +80,7 @@ module Rack::Attack
end end
def clear! def clear!
@whitelists, @blocks, @throttles = {}, {}, {} @whitelists, @blacklists, @throttles = {}, {}, {}
end end
end end

View file

@ -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

19
lib/rack/attack/check.rb Normal file
View file

@ -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

View file

@ -16,7 +16,7 @@ module Rack
end end
def [](req) def [](req)
discriminator = @block[req] discriminator = block[req]
return false unless discriminator return false unless discriminator
key = "#{name}:#{discriminator}" key = "#{name}:#{discriminator}"

View file

@ -1,5 +1,5 @@
module Rack module Rack
module Attack module Attack
VERSION = '0.0.2' VERSION = '0.0.3'
end end
end end

View file

@ -1,12 +1,10 @@
require_relative 'check'
module Rack module Rack
module Attack module Attack
class Whitelist class Whitelist < Check
attr_reader :name, :block def initialize(name, block)
def initialize(name, &block) super
@name, @block = name, block @type = :whitelist
end
def [](req)
end end
end end

View file

@ -22,15 +22,15 @@ describe 'Rack::Attack' do
allow_ok_requests allow_ok_requests
describe 'with a block' do describe 'with a blacklist' do
before do before do
@bad_ip = '1.2.3.4' @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 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 get '/', {}, 'REMOTE_ADDR' => @bad_ip
last_response.status.must_equal 503 last_response.status.must_equal 503
end end
@ -44,7 +44,7 @@ describe 'Rack::Attack' do
end end
it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") } 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 get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
last_response.status.must_equal 200 last_response.status.must_equal 200
end end