rack-attack-4.4.1/0000755000004100000410000000000012673001123013730 5ustar www-datawww-datarack-attack-4.4.1/Rakefile0000644000004100000410000000057012673001122015376 0ustar www-datawww-datarequire "rubygems" require "bundler/setup" require 'bundler/gem_tasks' require 'rake/testtask' namespace :test do Rake::TestTask.new(:units) do |t| t.pattern = "spec/*_spec.rb" end Rake::TestTask.new(:integration) do |t| t.pattern = "spec/integration/*_spec.rb" end end desc 'Run tests' task :test => %w[test:units test:integration] task :default => :test rack-attack-4.4.1/spec/0000755000004100000410000000000012673001122014661 5ustar www-datawww-datarack-attack-4.4.1/spec/spec_helper.rb0000644000004100000410000000152412673001122017501 0ustar www-datawww-datarequire "rubygems" require "bundler/setup" require "minitest/autorun" require "minitest/pride" require "rack/test" require 'active_support' require 'action_dispatch' # Load Journey for Rails 3.2 require 'journey' if ActionPack::VERSION::MAJOR == 3 require "rack/attack" begin require 'pry' rescue LoadError #nothing to do here end class MiniTest::Spec include Rack::Test::Methods after { Rack::Attack.clear! } def app Rack::Builder.new { use Rack::Attack run lambda {|env| [200, {}, ['Hello World']]} }.to_app end def self.allow_ok_requests it "must allow ok requests" do get '/', {}, 'REMOTE_ADDR' => '127.0.0.1' last_response.status.must_equal 200 last_response.body.must_equal 'Hello World' end end end class Minitest::SharedExamples < Module include Minitest::Spec::DSL end rack-attack-4.4.1/spec/rack_attack_spec.rb0000644000004100000410000000356112673001122020474 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack' do allow_ok_requests describe 'normalizing paths' do before do Rack::Attack.blacklist("banned_path") {|req| req.path == '/foo' } end it 'blocks requests with trailing slash' do get '/foo/' last_response.status.must_equal 403 end end describe 'blacklist' do before do @bad_ip = '1.2.3.4' Rack::Attack.blacklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip } end it('has a blacklist') { Rack::Attack.blacklists.key?("ip #{@bad_ip}").must_equal true } describe "a bad request" do before { get '/', {}, 'REMOTE_ADDR' => @bad_ip } it "should return a blacklist response" do get '/', {}, 'REMOTE_ADDR' => @bad_ip last_response.status.must_equal 403 last_response.body.must_equal "Forbidden\n" end it "should tag the env" do last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}" last_request.env['rack.attack.match_type'].must_equal :blacklist end allow_ok_requests end describe "and whitelist" do before do @good_ua = 'GoodUA' Rack::Attack.whitelist("good ua") {|req| req.user_agent == @good_ua } end it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") } describe "with a request match both whitelist & blacklist" do before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua } it "should allow whitelists before blacklists" do get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua last_response.status.must_equal 200 end it "should tag the env" do last_request.env['rack.attack.matched'].must_equal 'good ua' last_request.env['rack.attack.match_type'].must_equal :whitelist end end end end end rack-attack-4.4.1/spec/rack_attack_throttle_spec.rb0000644000004100000410000000625212673001122022421 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack.throttle' do before do @period = 60 # Use a long period; failures due to cache key rotation less likely Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip } end it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') } allow_ok_requests describe 'a single request' do before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } it 'should set the counter for one request' do key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4" Rack::Attack.cache.store.read(key).must_equal 1 end it 'should populate throttle data' do data = { :count => 1, :limit => 1, :period => @period } last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data end end describe "with 2 requests" do before do 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } end it 'should block the last request' do last_response.status.must_equal 429 end it 'should tag the env' do last_request.env['rack.attack.matched'].must_equal 'ip/sec' last_request.env['rack.attack.match_type'].must_equal :throttle last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period}) last_request.env['rack.attack.match_discriminator'].must_equal('1.2.3.4') end it 'should set a Retry-After header' do last_response.headers['Retry-After'].must_equal @period.to_s end end end describe 'Rack::Attack.throttle with limit as proc' do before do @period = 60 # Use a long period; failures due to cache key rotation less likely Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => @period) { |req| req.ip } end allow_ok_requests describe 'a single request' do before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } it 'should set the counter for one request' do key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4" Rack::Attack.cache.store.read(key).must_equal 1 end it 'should populate throttle data' do data = { :count => 1, :limit => 1, :period => @period } last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data end end end describe 'Rack::Attack.throttle with period as proc' do before do @period = 60 # Use a long period; failures due to cache key rotation less likely Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new Rack::Attack.throttle('ip/sec', :limit => lambda { |req| 1 }, :period => lambda { |req| @period }) { |req| req.ip } end allow_ok_requests describe 'a single request' do before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } it 'should set the counter for one request' do key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4" Rack::Attack.cache.store.read(key).must_equal 1 end it 'should populate throttle data' do data = { :count => 1, :limit => 1, :period => @period } last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data end end end rack-attack-4.4.1/spec/allow2ban_spec.rb0000644000004100000410000000661712673001122020113 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack.Allow2Ban' do before do # Use a long findtime; failures due to cache key rotation less likely @cache = Rack::Attack.cache @findtime = 60 @bantime = 60 Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new @f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2} Rack::Attack.blacklist('pentest') do |req| Rack::Attack::Allow2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/} end end describe 'discriminator has not been banned' do describe 'making ok request' do it 'succeeds' do get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' last_response.status.must_equal 200 end end describe 'making qualifying request' do describe 'when not at maxretry' do before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' } it 'succeeds' do last_response.status.must_equal 200 end it 'increases fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:allow2ban:count:1.2.3.4" @cache.store.read(key).must_equal 1 end it 'is not banned' do key = "rack::attack:allow2ban:1.2.3.4" @cache.store.read(key).must_be_nil end end describe 'when at maxretry' do before do # maxretry is 2 - so hit with an extra failed request first get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'succeeds' do last_response.status.must_equal 200 end it 'increases fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:allow2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is banned' do key = "rack::attack:allow2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end end end describe 'discriminator has been banned' do before do # maxretry is 2 - so hit enough times to get banned get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end describe 'making request for other discriminator' do it 'succeeds' do get '/', {}, 'REMOTE_ADDR' => '2.2.3.4' last_response.status.must_equal 200 end end describe 'making ok request' do before do get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'fails' do last_response.status.must_equal 403 end it 'does not increase fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:allow2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is still banned' do key = "rack::attack:allow2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end describe 'making failing request' do before do get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'fails' do last_response.status.must_equal 403 end it 'does not increase fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:allow2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is still banned' do key = "rack::attack:allow2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end end end rack-attack-4.4.1/spec/rack_attack_path_normalizer_spec.rb0000644000004100000410000000065612673001122023754 0ustar www-datawww-datarequire_relative 'spec_helper' describe Rack::Attack::PathNormalizer do subject { Rack::Attack::PathNormalizer } it 'should have a normalize_path method' do subject.normalize_path('/foo').must_equal '/foo' end describe 'FallbackNormalizer' do subject { Rack::Attack::FallbackPathNormalizer } it '#normalize_path does not change the path' do subject.normalize_path('').must_equal '' end end end rack-attack-4.4.1/spec/integration/0000755000004100000410000000000012673001122017204 5ustar www-datawww-datarack-attack-4.4.1/spec/integration/offline_spec.rb0000644000004100000410000000166712673001122022177 0ustar www-datawww-datarequire 'active_support/cache' require 'active_support/cache/redis_store' require 'dalli' require_relative '../spec_helper' OfflineExamples = Minitest::SharedExamples.new do it 'should write' do @cache.write('cache-test-key', 'foobar', 1) end it 'should read' do @cache.read('cache-test-key') end it 'should count' do @cache.send(:do_count, 'rack::attack::cache-test-key', 1) end end describe 'when Redis is offline' do include OfflineExamples before { @cache = Rack::Attack::Cache.new # Use presumably unused port for Redis client @cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333) } end describe 'when Memcached is offline' do include OfflineExamples before { Dalli.logger.level = Logger::FATAL @cache = Rack::Attack::Cache.new @cache.store = Dalli::Client.new('127.0.0.1:22122') } after { Dalli.logger.level = Logger::INFO } end rack-attack-4.4.1/spec/integration/rack_attack_cache_spec.rb0000644000004100000410000000673412673001122024147 0ustar www-datawww-datarequire_relative '../spec_helper' describe Rack::Attack::Cache do # A convenience method for deleting a key from cache. # Slightly differnet than @cache.delete, which adds a prefix. def delete(key) if @cache.store.respond_to?(:delete) @cache.store.delete(key) else @cache.store.del(key) end end def sleep_until_expired sleep(@expires_in * 1.1) # Add 10% to reduce errors end require 'active_support/cache/dalli_store' require 'active_support/cache/mem_cache_store' require 'active_support/cache/redis_store' require 'connection_pool' cache_stores = [ ActiveSupport::Cache::MemoryStore.new, ActiveSupport::Cache::DalliStore.new("127.0.0.1"), ActiveSupport::Cache::RedisStore.new("127.0.0.1"), ActiveSupport::Cache::MemCacheStore.new("127.0.0.1"), Dalli::Client.new, ConnectionPool.new { Dalli::Client.new }, Redis::Store.new ] cache_stores.each do |store| store = Rack::Attack::StoreProxy.build(store) describe "with #{store.class}" do before { @cache = Rack::Attack::Cache.new @key = "rack::attack:cache-test-key" @expires_in = 1 @cache.store = store delete(@key) } after { delete(@key) } describe "do_count once" do it "should be 1" do @cache.send(:do_count, @key, @expires_in).must_equal 1 end end describe "do_count twice" do it "must be 2" do @cache.send(:do_count, @key, @expires_in) @cache.send(:do_count, @key, @expires_in).must_equal 2 end end describe "do_count after expires_in" do it "must be 1" do @cache.send(:do_count, @key, @expires_in) sleep_until_expired @cache.send(:do_count, @key, @expires_in).must_equal 1 end end describe "write" do it "should write a value to the store with prefix" do @cache.write("cache-test-key", "foobar", 1) store.read(@key).must_equal "foobar" end end describe "write after expiry" do it "must not have a value" do @cache.write("cache-test-key", "foobar", @expires_in) sleep_until_expired store.read(@key).must_be :nil? end end describe "read" do it "must read the value with a prefix" do store.write(@key, "foobar", :expires_in => @expires_in) @cache.read("cache-test-key").must_equal "foobar" end end describe "delete" do it "must delete the value" do store.write(@key, "foobar", :expires_in => @expires_in) @cache.read('cache-test-key').must_equal "foobar" store.delete(@key) @cache.read('cache-test-key').must_equal nil end end describe "cache#delete" do it "must delete the value" do @cache.write("cache-test-key", "foobar", 1) store.read(@key).must_equal "foobar" @cache.delete('cache-test-key') store.read(@key).must_be :nil? end end describe "reset_count" do it "must delete the value" do period = 1.minute unprefixed_key = 'cache-test-key' @cache.count(unprefixed_key, period) period_key, _ = @cache.send(:key_and_expiry, 'cache-test-key', period) store.read(period_key).to_i.must_equal 1 @cache.reset_count(unprefixed_key, period) store.read(period_key).must_equal nil end end end end end rack-attack-4.4.1/spec/rack_attack_request_spec.rb0000644000004100000410000000050712673001122022241 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack' do describe 'helpers' do before do class Rack::Attack::Request def remote_ip ip end end Rack::Attack.whitelist('valid IP') do |req| req.remote_ip == "127.0.0.1" end end allow_ok_requests end end rack-attack-4.4.1/spec/fail2ban_spec.rb0000644000004100000410000000776312673001122017713 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack.Fail2Ban' do before do # Use a long findtime; failures due to cache key rotation less likely @cache = Rack::Attack.cache @findtime = 60 @bantime = 60 Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new @f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2} Rack::Attack.blacklist('pentest') do |req| Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/} end end describe 'discriminator has not been banned' do describe 'making ok request' do it 'succeeds' do get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' last_response.status.must_equal 200 end end describe 'making failing request' do describe 'when not at maxretry' do before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' } it 'fails' do last_response.status.must_equal 403 end it 'increases fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4" @cache.store.read(key).must_equal 1 end it 'is not banned' do key = "rack::attack:fail2ban:1.2.3.4" @cache.store.read(key).must_be_nil end end describe 'when at maxretry' do before do # maxretry is 2 - so hit with an extra failed request first get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'fails' do last_response.status.must_equal 403 end it 'increases fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is banned' do key = "rack::attack:fail2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end describe 'reset after success' do before do get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' Rack::Attack::Fail2Ban.reset('1.2.3.4', @f2b_options) get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'succeeds' do last_response.status.must_equal 200 end it 'resets fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4" @cache.store.read(key).must_equal nil end it 'IP is not banned' do Rack::Attack::Fail2Ban.banned?('1.2.3.4').must_equal false end end end end describe 'discriminator has been banned' do before do # maxretry is 2 - so hit enough times to get banned get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end describe 'making request for other discriminator' do it 'succeeds' do get '/', {}, 'REMOTE_ADDR' => '2.2.3.4' last_response.status.must_equal 200 end end describe 'making ok request' do before do get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'fails' do last_response.status.must_equal 403 end it 'does not increase fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is still banned' do key = "rack::attack:fail2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end describe 'making failing request' do before do get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' end it 'fails' do last_response.status.must_equal 403 end it 'does not increase fail count' do key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4" @cache.store.read(key).must_equal 2 end it 'is still banned' do key = "rack::attack:fail2ban:ban:1.2.3.4" @cache.store.read(key).must_equal 1 end end end end rack-attack-4.4.1/spec/rack_attack_dalli_proxy_spec.rb0000644000004100000410000000040312673001122023072 0ustar www-datawww-datarequire_relative 'spec_helper' describe Rack::Attack::StoreProxy::DalliProxy do it 'should stub Dalli::Client#with on older clients' do proxy = Rack::Attack::StoreProxy::DalliProxy.new(Class.new) proxy.with {} # will not raise an error end end rack-attack-4.4.1/spec/rack_attack_track_spec.rb0000644000004100000410000000262512673001122021660 0ustar www-datawww-datarequire_relative 'spec_helper' describe 'Rack::Attack.track' do class Counter def self.incr @counter += 1 end def self.reset @counter = 0 end def self.check @counter end end before do Rack::Attack.track("everything"){ |req| true } end allow_ok_requests it "should tag the env" do get '/' last_request.env['rack.attack.matched'].must_equal 'everything' last_request.env['rack.attack.match_type'].must_equal :track end describe "with a notification subscriber and two tracks" do before do Counter.reset # A second track Rack::Attack.track("homepage"){ |req| req.path == "/"} ActiveSupport::Notifications.subscribe("rack.attack") do |*args| Counter.incr end get "/" end it "should notify twice" do Counter.check.must_equal 2 end end describe "without limit and period options" do it "should assign the track filter to a Check instance" do tracker = Rack::Attack.track("homepage") { |req| req.path == "/"} tracker.filter.class.must_equal Rack::Attack::Check end end describe "with limit and period options" do it "should assign the track filter to a Throttle instance" do tracker = Rack::Attack.track("homepage", :limit => 10, :period => 10) { |req| req.path == "/"} tracker.filter.class.must_equal Rack::Attack::Throttle end end end rack-attack-4.4.1/lib/0000755000004100000410000000000012673001122014475 5ustar www-datawww-datarack-attack-4.4.1/lib/rack/0000755000004100000410000000000012673001122015415 5ustar www-datawww-datarack-attack-4.4.1/lib/rack/attack.rb0000644000004100000410000000622412673001122017215 0ustar www-datawww-datarequire 'rack' require 'forwardable' class Rack::Attack autoload :Cache, 'rack/attack/cache' autoload :PathNormalizer, 'rack/attack/path_normalizer' autoload :Check, 'rack/attack/check' autoload :Throttle, 'rack/attack/throttle' autoload :Whitelist, 'rack/attack/whitelist' autoload :Blacklist, 'rack/attack/blacklist' autoload :Track, 'rack/attack/track' autoload :StoreProxy, 'rack/attack/store_proxy' autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy' autoload :MemCacheProxy, 'rack/attack/store_proxy/mem_cache_proxy' autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy' autoload :Fail2Ban, 'rack/attack/fail2ban' autoload :Allow2Ban, 'rack/attack/allow2ban' autoload :Request, 'rack/attack/request' class << self attr_accessor :notifier, :blacklisted_response, :throttled_response def whitelist(name, &block) self.whitelists[name] = Whitelist.new(name, block) end def blacklist(name, &block) self.blacklists[name] = Blacklist.new(name, block) end def throttle(name, options, &block) self.throttles[name] = Throttle.new(name, options, block) end def track(name, options = {}, &block) self.tracks[name] = Track.new(name, options, block) end def whitelists; @whitelists ||= {}; end def blacklists; @blacklists ||= {}; end def throttles; @throttles ||= {}; end def tracks; @tracks ||= {}; end def whitelisted?(req) whitelists.any? do |name, whitelist| whitelist[req] end end def blacklisted?(req) blacklists.any? do |name, blacklist| blacklist[req] end end def throttled?(req) throttles.any? do |name, throttle| throttle[req] end end def tracked?(req) tracks.each_value do |tracker| tracker[req] end end def instrument(req) notifier.instrument('rack.attack', req) if notifier end def cache @cache ||= Cache.new end def clear! @whitelists, @blacklists, @throttles, @tracks = {}, {}, {}, {} end end # Set defaults @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications) @blacklisted_response = lambda {|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"]] } def initialize(app) @app = app end def call(env) env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO']) req = Rack::Attack::Request.new(env) if whitelisted?(req) @app.call(env) elsif blacklisted?(req) self.class.blacklisted_response.call(env) elsif throttled?(req) self.class.throttled_response.call(env) else tracked?(req) @app.call(env) end end extend Forwardable def_delegators self, :whitelisted?, :blacklisted?, :throttled?, :tracked? end rack-attack-4.4.1/lib/rack/attack/0000755000004100000410000000000012673001122016664 5ustar www-datawww-datarack-attack-4.4.1/lib/rack/attack/cache.rb0000644000004100000410000000305012673001122020252 0ustar www-datawww-datamodule Rack class Attack class Cache attr_accessor :prefix def initialize self.store = ::Rails.cache if defined?(::Rails.cache) @prefix = 'rack::attack' end attr_reader :store def store=(store) @store = StoreProxy.build(store) end def count(unprefixed_key, period) key, expires_in = key_and_expiry(unprefixed_key, period) do_count(key, expires_in) end def read(unprefixed_key) store.read("#{prefix}:#{unprefixed_key}") end def write(unprefixed_key, value, 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) store.delete(key) end def delete(unprefixed_key) store.delete("#{prefix}:#{unprefixed_key}") end private def key_and_expiry(unprefixed_key, period) epoch_time = Time.now.to_i # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA expires_in = (period - (epoch_time % period) + 1).to_i ["#{prefix}:#{(epoch_time / period).to_i}:#{unprefixed_key}", expires_in] end def do_count(key, expires_in) result = store.increment(key, 1, :expires_in => expires_in) # NB: Some stores return nil when incrementing uninitialized values if result.nil? store.write(key, 1, :expires_in => expires_in) end result || 1 end end end end rack-attack-4.4.1/lib/rack/attack/track.rb0000644000004100000410000000064312673001122020320 0ustar www-datawww-datamodule Rack class Attack class Track extend Forwardable attr_reader :filter def initialize(name, options = {}, block) options[:type] = :track if options[:limit] && options[:period] @filter = Throttle.new(name, options, block) else @filter = Check.new(name, options, block) end end def_delegator :@filter, :[] end end end rack-attack-4.4.1/lib/rack/attack/check.rb0000644000004100000410000000074612673001122020275 0ustar www-datawww-datamodule Rack class Attack class Check attr_reader :name, :block, :type def initialize(name, options = {}, block) @name, @block = name, block @type = options.fetch(:type, nil) end def [](req) block[req].tap {|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 rack-attack-4.4.1/lib/rack/attack/allow2ban.rb0000644000004100000410000000121212673001122021066 0ustar www-datawww-datamodule Rack class Attack class Allow2Ban < Fail2Ban class << self protected def key_prefix 'allow2ban' end # everything the same here except we return only return true # (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 # we may not block them this time, but they're banned for next time false end end end end end rack-attack-4.4.1/lib/rack/attack/path_normalizer.rb0000644000004100000410000000163412673001122022413 0ustar www-datawww-dataclass Rack::Attack # When using Rack::Attack with a Rails app, developers expect the request path # to be normalized. In particular, trailing slashes are stripped. # (See http://git.io/v0rrR for implementation.) # # Look for an ActionDispatch utility class that Rails folks would expect # to normalize request paths. If unavailable, use a fallback class that # doesn't normalize the path (as a non-Rails rack app developer expects). module FallbackPathNormalizer def self.normalize_path(path) path end end PathNormalizer = if defined?(::ActionDispatch::Journey::Router::Utils) # For Rails 4+ apps ::ActionDispatch::Journey::Router::Utils elsif defined?(::Journey::Router::Utils) # for Rails 3.2 ::Journey::Router::Utils else FallbackPathNormalizer end end rack-attack-4.4.1/lib/rack/attack/request.rb0000644000004100000410000000066412673001122020707 0ustar www-datawww-data# Rack::Attack::Request is the same as ::Rack::Request by default. # # This is a safe place to add custom helper methods to the request object # through monkey patching: # # class Rack::Attack::Request < ::Rack::Request # def localhost? # ip == "127.0.0.1" # end # end # # Rack::Attack.whitelist("localhost") {|req| req.localhost? } # module Rack class Attack class Request < ::Rack::Request end end end rack-attack-4.4.1/lib/rack/attack/store_proxy/0000755000004100000410000000000012673001122021261 5ustar www-datawww-datarack-attack-4.4.1/lib/rack/attack/store_proxy/redis_store_proxy.rb0000644000004100000410000000211412673001122025367 0ustar www-datawww-datarequire 'delegate' module Rack class Attack module StoreProxy class RedisStoreProxy < SimpleDelegator def self.handle?(store) defined?(::Redis::Store) && store.is_a?(::Redis::Store) end def initialize(store) super(store) end def read(key) self.get(key, raw: true) rescue Redis::BaseError end def write(key, value, options={}) if (expires_in = options[:expires_in]) self.setex(key, expires_in, value, raw: true) else self.set(key, value, raw: true) end rescue Redis::BaseError end def increment(key, amount, options={}) count = nil self.pipelined do count = self.incrby(key, amount) self.expire(key, options[:expires_in]) if options[:expires_in] end count.value if count rescue Redis::BaseError end def delete(key, options={}) self.del(key) rescue Redis::BaseError end end end end end rack-attack-4.4.1/lib/rack/attack/store_proxy/mem_cache_proxy.rb0000644000004100000410000000222312673001122024747 0ustar www-datawww-datamodule Rack class Attack module StoreProxy class MemCacheProxy < SimpleDelegator def self.handle?(store) defined?(::MemCache) && store.is_a?(::MemCache) end def initialize(store) super(store) stub_with_if_missing end def read(key) # Second argument: reading raw value get(key, true) rescue MemCache::MemCacheError end 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={}) incr(key, amount) rescue MemCache::MemCacheError end def delete(key, options={}) with do |client| client.delete(key) end rescue MemCache::MemCacheError end private def stub_with_if_missing unless __getobj__.respond_to?(:with) class << self def with; yield __getobj__; end end end end end end end end rack-attack-4.4.1/lib/rack/attack/store_proxy/dalli_proxy.rb0000644000004100000410000000305412673001122024136 0ustar www-datawww-datarequire 'delegate' module Rack class Attack module StoreProxy class DalliProxy < SimpleDelegator def self.handle?(store) return false unless defined?(::Dalli) # Consider extracting to a separate Connection Pool proxy to reduce # code here and handle clients other than Dalli. if defined?(::ConnectionPool) && store.is_a?(::ConnectionPool) store.with { |conn| conn.is_a?(::Dalli::Client) } else store.is_a?(::Dalli::Client) end end def initialize(client) super(client) stub_with_if_missing end def read(key) with do |client| client.get(key) end rescue Dalli::DalliError end 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={}) with do |client| client.incr(key, amount, options.fetch(:expires_in, 0), amount) end rescue Dalli::DalliError end def delete(key) with do |client| client.delete(key) end rescue Dalli::DalliError end private def stub_with_if_missing unless __getobj__.respond_to?(:with) class << self def with; yield __getobj__; end end end end end end end end rack-attack-4.4.1/lib/rack/attack/version.rb0000644000004100000410000000007312673001122020676 0ustar www-datawww-datamodule Rack class Attack VERSION = '4.4.1' end end rack-attack-4.4.1/lib/rack/attack/blacklist.rb0000644000004100000410000000024012673001122021155 0ustar www-datawww-datamodule Rack class Attack class Blacklist < Check def initialize(name, block) super @type = :blacklist end end end end rack-attack-4.4.1/lib/rack/attack/whitelist.rb0000644000004100000410000000023712673001122021227 0ustar www-datawww-datamodule Rack class Attack class Whitelist < Check def initialize(name, block) super @type = :whitelist end end end end rack-attack-4.4.1/lib/rack/attack/throttle.rb0000644000004100000410000000307112673001122021057 0ustar www-datawww-datamodule Rack class Attack class Throttle MANDATORY_OPTIONS = [:limit, :period] attr_reader :name, :limit, :period, :block, :type def initialize(name, options, block) @name, @block = name, block MANDATORY_OPTIONS.each do |opt| raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt] end @limit = options[:limit] @period = options[:period].respond_to?(:call) ? options[:period] : options[:period].to_i @type = options.fetch(:type, :throttle) end def cache Rack::Attack.cache end def [](req) discriminator = block[req] return false unless discriminator current_period = period.respond_to?(:call) ? period.call(req) : period current_limit = limit.respond_to?(:call) ? limit.call(req) : limit key = "#{name}:#{discriminator}" count = cache.count(key, current_period) data = { :count => count, :period => current_period, :limit => current_limit } (req.env['rack.attack.throttle_data'] ||= {})[name] = data (count > current_limit).tap do |throttled| if throttled req.env['rack.attack.matched'] = name req.env['rack.attack.match_discriminator'] = discriminator req.env['rack.attack.match_type'] = type req.env['rack.attack.match_data'] = data Rack::Attack.instrument(req) end end end end end end rack-attack-4.4.1/lib/rack/attack/store_proxy.rb0000644000004100000410000000220312673001122021603 0ustar www-datawww-datamodule Rack class Attack module StoreProxy PROXIES = [DalliProxy, MemCacheProxy, RedisStoreProxy] ACTIVE_SUPPORT_WRAPPER_CLASSES = Set.new(['ActiveSupport::Cache::MemCacheStore', 'ActiveSupport::Cache::RedisStore']).freeze ACTIVE_SUPPORT_CLIENTS = Set.new(['Redis::Store', 'Dalli::Client', 'MemCache']).freeze def self.build(store) client = unwrap_active_support_stores(store) klass = PROXIES.find { |proxy| proxy.handle?(client) } 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. # We also want to use the underlying Dalli client instead of ::ActiveSupport::Cache::MemCacheStore, # and the MemCache client if using Rails 3.x client = store.instance_variable_get(:@data) if ACTIVE_SUPPORT_WRAPPER_CLASSES.include?(store.class.to_s) && ACTIVE_SUPPORT_CLIENTS.include?(client.class.to_s) client else store end end end end end rack-attack-4.4.1/lib/rack/attack/fail2ban.rb0000644000004100000410000000312012673001122020663 0ustar www-datawww-datamodule Rack 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" if banned?(discriminator) # Return true for blacklist true elsif yield fail!(discriminator, bantime, findtime, maxretry) end end def reset(discriminator, options) findtime = options[:findtime] or 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}") end def banned?(discriminator) cache.read("#{key_prefix}:ban:#{discriminator}") ? true : false 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 true end private def ban!(discriminator, bantime) cache.write("#{key_prefix}:ban:#{discriminator}", 1, bantime) end def cache Rack::Attack.cache end end end end end rack-attack-4.4.1/rack-attack.gemspec0000644000004100000410000001004512673001123017462 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "rack-attack" s.version = "4.4.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Aaron Suggs"] s.date = "2016-02-17" s.description = "A rack middleware for throttling and blocking abusive requests" s.email = "aaron@ktheory.com" s.files = ["README.md", "Rakefile", "lib/rack/attack.rb", "lib/rack/attack/allow2ban.rb", "lib/rack/attack/blacklist.rb", "lib/rack/attack/cache.rb", "lib/rack/attack/check.rb", "lib/rack/attack/fail2ban.rb", "lib/rack/attack/path_normalizer.rb", "lib/rack/attack/request.rb", "lib/rack/attack/store_proxy.rb", "lib/rack/attack/store_proxy/dalli_proxy.rb", "lib/rack/attack/store_proxy/mem_cache_proxy.rb", "lib/rack/attack/store_proxy/redis_store_proxy.rb", "lib/rack/attack/throttle.rb", "lib/rack/attack/track.rb", "lib/rack/attack/version.rb", "lib/rack/attack/whitelist.rb", "spec/allow2ban_spec.rb", "spec/fail2ban_spec.rb", "spec/integration/offline_spec.rb", "spec/integration/rack_attack_cache_spec.rb", "spec/rack_attack_dalli_proxy_spec.rb", "spec/rack_attack_path_normalizer_spec.rb", "spec/rack_attack_request_spec.rb", "spec/rack_attack_spec.rb", "spec/rack_attack_throttle_spec.rb", "spec/rack_attack_track_spec.rb", "spec/spec_helper.rb"] s.homepage = "http://github.com/kickstarter/rack-attack" s.licenses = ["MIT"] s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new(">= 2.0.0") s.rubygems_version = "1.8.23" s.summary = "Block & throttle abusive requests" s.test_files = ["spec/allow2ban_spec.rb", "spec/fail2ban_spec.rb", "spec/integration/offline_spec.rb", "spec/integration/rack_attack_cache_spec.rb", "spec/rack_attack_dalli_proxy_spec.rb", "spec/rack_attack_path_normalizer_spec.rb", "spec/rack_attack_request_spec.rb", "spec/rack_attack_spec.rb", "spec/rack_attack_throttle_spec.rb", "spec/rack_attack_track_spec.rb", "spec/spec_helper.rb"] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, [">= 3.0.0"]) s.add_development_dependency(%q, [">= 3.0.0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_runtime_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, [">= 0"]) else s.add_dependency(%q, [">= 3.0.0"]) s.add_dependency(%q, [">= 3.0.0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) end else s.add_dependency(%q, [">= 3.0.0"]) s.add_dependency(%q, [">= 3.0.0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, [">= 0"]) end end rack-attack-4.4.1/README.md0000644000004100000410000002715612673001122015221 0ustar www-datawww-data# Rack::Attack!!! *Rack middleware for blocking & throttling abusive requests* Rack::Attack is a rack middleware to protect your web app from bad clients. It allows *whitelisting*, *blacklisting*, *throttling*, and *tracking* based on arbitrary properties of the request. Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)). See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack. [![Gem Version](https://badge.fury.io/rb/rack-attack.png)](http://badge.fury.io/rb/rack-attack) [![Build Status](https://travis-ci.org/kickstarter/rack-attack.png?branch=master)](https://travis-ci.org/kickstarter/rack-attack) [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](https://codeclimate.com/github/kickstarter/rack-attack) ## Getting started Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to your Gemfile with bundler: ```ruby # In your Gemfile gem 'rack-attack' ``` Tell your app to use the Rack::Attack middleware. For Rails 3+ apps: ```ruby # In config/application.rb config.middleware.use Rack::Attack ``` Or for Rackup files: ```ruby # In config.ru use Rack::Attack ``` Add a `rack-attack.rb` file to `config/initializers/`: ```ruby # In config/initializers/rack-attack.rb class Rack::Attack # your custom configuration... end ``` *Tip:* The example in the wiki is a great way to get started: [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration) Optionally configure the cache store for throttling or fail2ban filtering: ```ruby Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache ``` Note that `Rack::Attack.cache` is only used for throttling and fail2ban filtering; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). ## How it works The Rack::Attack middleware compares each request against *whitelists*, *blacklists*, *throttles*, and *tracks* that you define. There are none by default. * If the request matches any **whitelist**, it is allowed. * Otherwise, if the request matches any **blacklist**, it is blocked. * Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked. * Otherwise, all **tracks** are checked, and the request is allowed. The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb): ```ruby def call(env) req = Rack::Attack::Request.new(env) if whitelisted?(req) @app.call(env) elsif blacklisted?(req) self.class.blacklisted_response.call(env) elsif throttled?(req) self.class.throttled_response.call(env) else tracked?(req) @app.call(env) end end ``` Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you can cleanly monkey patch helper methods onto the [request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb). ## About Tracks `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes. ## Usage Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app these go in an initializer in `config/initializers/`. A [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request) object is passed to the block (named 'req' in the examples). ### Whitelists ```ruby # Always allow requests from localhost # (blacklist & throttles are skipped) Rack::Attack.whitelist('allow from localhost') do |req| # Requests are allowed if the return value is truthy '127.0.0.1' == req.ip || '::1' == req.ip end ``` ### Blacklists ```ruby # Block requests from 1.2.3.4 Rack::Attack.blacklist('block 1.2.3.4') do |req| # Requests are blocked if the return value is truthy '1.2.3.4' == req.ip end # Block logins from a bad user agent Rack::Attack.blacklist('block bad UA logins') do |req| req.path == '/login' && req.post? && req.user_agent == 'BadUA' end ``` #### Fail2Ban `Fail2Ban.filter` can be used within a blacklist to block all requests from misbehaving clients. This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page). See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on how the parameters work. For multiple filters, be sure to put each filter in a separate blacklist and use a unique discriminator for each fail2ban filter. ```ruby # Block suspicious requests for '/etc/password' or wordpress specific paths. # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes. Rack::Attack.blacklist('fail2ban pentesters') do |req| # `filter` returns truthy value if request fails, or if it's from a previously banned IP # so the request is blocked Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do # The count for the IP is incremented if the return value is truthy CGI.unescape(req.query_string) =~ %r{/etc/passwd} || req.path.include?('/etc/passwd') || req.path.include?('wp-admin') || req.path.include?('wp-login') end end ``` Note that `Fail2Ban` filters are not automatically scoped to the blacklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `"pentest:#{req.ip}"`. #### Allow2Ban `Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving clients until such time as they reach maxretry at which they are cut off as per normal. ```ruby # Lockout IP addresses that are hammering your login page. # After 20 requests in 1 minute, block all requests from that IP for 1 hour. Rack::Attack.blacklist('allow2ban login scrapers') do |req| # `filter` returns false value if request is to your login page (but still # increments the count) so request below the limit are not blocked until # they hit the limit. At that point, filter will return true and block. Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do # The count for the IP is incremented if the return value is truthy. req.path == '/login' and req.post? end end ``` ### Throttles ```ruby # Throttle requests to 5 requests per second per ip Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req| # If the return value is truthy, the cache key for the return value # is incremented and compared with the limit. In this case: # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}" # # If falsy, the cache key is neither incremented nor checked. req.ip end # Throttle login attempts for a given email parameter to 6 reqs/minute # Return the email as a discriminator on POST /login requests Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req| req.params['email'] if req.path == '/login' && req.post? end # You can also set a limit and period using a proc. For instance, after # Rack::Auth::Basic has authenticated the user: limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1} period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute} Rack::Attack.throttle('req/ip', :limit => limit_proc, :period => period_proc) do |req| req.ip end ``` ### Tracks ```ruby # Track requests from a special user agent. Rack::Attack.track("special_agent") do |req| req.user_agent == "SpecialAgent" end # Supports optional limit and period, triggers the notification only when the limit is reached. Rack::Attack.track("special_agent", :limit => 6, :period => 60.seconds) do |req| req.user_agent == "SpecialAgent" end # Track it using ActiveSupport::Notification ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req| if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track Rails.logger.info "special_agent: #{req.path}" STATSD.increment("special_agent") end end ``` ## Responses Customize the response of blacklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html). ```ruby Rack::Attack.blacklisted_response = lambda do |env| # Using 503 because it may make attacker think that they have successfully # DOSed the site. Rack::Attack returns 403 for blacklists by default [ 503, {}, ['Blocked']] end Rack::Attack.throttled_response = lambda do |env| # name and other data about the matched throttle body = [ env['rack.attack.matched'], env['rack.attack.match_type'], env['rack.attack.match_data'] ].inspect # Using 503 because it may make attacker think that they have successfully # DOSed the site. Rack::Attack returns 429 for throttling by default [ 503, {}, [body]] end ``` For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data: ```ruby request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l } ``` ## Logging & Instrumentation Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available. You can subscribe to 'rack.attack' events and log it, graph it, etc: ```ruby ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req| puts req.inspect end ``` ## Testing A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html) for more on how to do this. ## Performance The overhead of running Rack::Attack is typically negligible (a few milliseconds per request), but it depends on how many checks you've configured, and how long they take. Throttles usually require a network roundtrip to your cache server(s), so try to keep the number of throttle checks per request low. If a request is blacklisted or throttled, the response is a very simple Rack response. A single typical ruby web server thread can block several hundred requests per second. Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone). ## Motivation Abusive clients range from malicious login crackers to naively-written scrapers. They hinder the security, performance, & availability of web applications. It is impractical if not impossible to block abusive clients completely. Rack::Attack aims to let developers quickly mitigate abusive requests and rely less on short-term, one-off hacks to block a particular attack. ## Contributing Pull requests and issues are greatly appreciated. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md). ## Mailing list New releases of Rack::Attack are announced on . To subscribe, just send an email to . See the [archives](http://librelist.com/browser/rack.attack.announce/). ## License Copyright Kickstarter, Inc. Released under an [MIT License](http://opensource.org/licenses/MIT).