mirror of
https://github.com/samsonjs/rack-attack.git
synced 2026-04-26 14:57:47 +00:00
Add throttle support
This commit is contained in:
parent
c22c33f9ec
commit
e166e87fb9
7 changed files with 114 additions and 6 deletions
32
examples/rack_attack.rb
Normal file
32
examples/rack_attack.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# NB: `req` is a Rack::Request object (basically an env hash with friendly accessor methods)
|
||||||
|
|
||||||
|
# Throttle 10 requests/ip/second
|
||||||
|
# NB: return value of block is key name for counter
|
||||||
|
# falsy values bypass throttling
|
||||||
|
Rack::Attack.throttle("req/ip", :limit => 10, :period => 1) { |req| req.ip }
|
||||||
|
|
||||||
|
# Throttle attempts to a particular path. 2 POSTs to /login per second per IP
|
||||||
|
Rack::Attack.throttle "logins/ip", :limit => 2, :period => 1 do |req|
|
||||||
|
req.ip if req.post? && req.path_info =~ /^login/
|
||||||
|
end
|
||||||
|
|
||||||
|
# Throttle login attempts per email, 10/minute/email
|
||||||
|
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|
|
||||||
|
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'
|
||||||
|
end
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
require 'rack'
|
require 'rack'
|
||||||
module Rack::Attack
|
module Rack::Attack
|
||||||
require 'rack/attack/cache'
|
require 'rack/attack/cache'
|
||||||
|
require 'rack/attack/throttle'
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
|
||||||
|
|
@ -14,7 +15,8 @@ module Rack::Attack
|
||||||
(@blocks ||= {})[name] = block
|
(@blocks ||= {})[name] = block
|
||||||
end
|
end
|
||||||
|
|
||||||
def throttle
|
def throttle(name, options, &block)
|
||||||
|
(@throttles ||= {})[name] = Throttle.new(name, options, block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def whitelists; @whitelists ||= {}; end
|
def whitelists; @whitelists ||= {}; end
|
||||||
|
|
@ -22,7 +24,7 @@ module Rack::Attack
|
||||||
def throttles; @throttles ||= {}; end
|
def throttles; @throttles ||= {}; end
|
||||||
|
|
||||||
def new(app)
|
def new(app)
|
||||||
@cache = Cache.new
|
@cache ||= Cache.new
|
||||||
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
||||||
@app = app
|
@app = app
|
||||||
self
|
self
|
||||||
|
|
@ -39,6 +41,7 @@ module Rack::Attack
|
||||||
if blocked?(req)
|
if blocked?(req)
|
||||||
blocked_response
|
blocked_response
|
||||||
elsif throttled?(req)
|
elsif throttled?(req)
|
||||||
|
throttled_response
|
||||||
else
|
else
|
||||||
@app.call(env)
|
@app.call(env)
|
||||||
end
|
end
|
||||||
|
|
@ -61,7 +64,9 @@ module Rack::Attack
|
||||||
end
|
end
|
||||||
|
|
||||||
def throttled?(req)
|
def throttled?(req)
|
||||||
false
|
throttles.any? do |name, throttle|
|
||||||
|
throttle[req]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def instrument(payload)
|
def instrument(payload)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@ module Rack
|
||||||
module Attack
|
module Attack
|
||||||
class Cache
|
class Cache
|
||||||
|
|
||||||
attr_accessor :store
|
attr_accessor :store, :prefix
|
||||||
def initialize
|
def initialize
|
||||||
@store = ::Rails.cache if defined?(::Rails.cache)
|
@store = ::Rails.cache if defined?(::Rails.cache)
|
||||||
|
@prefix = 'rack::attack'
|
||||||
end
|
end
|
||||||
|
|
||||||
def count(key, expires_in)
|
def count(unprefixed_key, expires_in)
|
||||||
result = store.increment(1, :expires_in => expires_in)
|
key = "#{prefix}:#{unprefixed_key}"
|
||||||
|
result = store.increment(key, 1, :expires_in => expires_in)
|
||||||
# NB: Some stores return nil when incrementing uninitialized values
|
# NB: Some stores return nil when incrementing uninitialized values
|
||||||
if result.nil?
|
if result.nil?
|
||||||
store.write(key, 1, :expires_in => expires_in)
|
store.write(key, 1, :expires_in => expires_in)
|
||||||
|
|
|
||||||
31
lib/rack/attack/throttle.rb
Normal file
31
lib/rack/attack/throttle.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
module Rack
|
||||||
|
module Attack
|
||||||
|
class Throttle
|
||||||
|
attr_reader :name, :limit, :period, :block
|
||||||
|
def initialize(name, options, block)
|
||||||
|
@name, @block = name, block
|
||||||
|
[:limit, :period].each do |opt|
|
||||||
|
raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt]
|
||||||
|
end
|
||||||
|
@limit = options[:limit]
|
||||||
|
@period = options[:period]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache
|
||||||
|
Rack::Attack.cache
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](req)
|
||||||
|
discriminator = @block[req]
|
||||||
|
return false unless discriminator
|
||||||
|
|
||||||
|
key = "#{name}:#{discriminator}"
|
||||||
|
count = cache.count(key, period)
|
||||||
|
throttled = count > limit
|
||||||
|
Rack::Attack.instrument(:type => :throttle, :name => name, :request => req, :count => count, :throttled => throttled)
|
||||||
|
throttled
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
14
lib/rack/attack/whitelist.rb
Normal file
14
lib/rack/attack/whitelist.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
module Rack
|
||||||
|
module Attack
|
||||||
|
class Whitelist
|
||||||
|
attr_reader :name, :block
|
||||||
|
def initialize(name, &block)
|
||||||
|
@name, @block = name, block
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](req)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -51,5 +51,27 @@ describe 'Rack::Attack' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'with a throttle' do
|
||||||
|
before do
|
||||||
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
||||||
|
Rack::Attack.throttle('ip/sec', :limit => 1, :period => 1) { |req| req.ip }
|
||||||
|
end
|
||||||
|
|
||||||
|
it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
|
||||||
|
allow_ok_requests
|
||||||
|
|
||||||
|
it 'should set the counter for one request' do
|
||||||
|
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
||||||
|
Rack::Attack.cache.store.read('rack::attack:ip/sec:1.2.3.4').must_equal 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should block 2 requests' do
|
||||||
|
2.times do
|
||||||
|
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
||||||
|
end
|
||||||
|
last_response.status.must_equal 503
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,7 @@ require "bundler/setup"
|
||||||
|
|
||||||
require "minitest/autorun"
|
require "minitest/autorun"
|
||||||
require "rack/test"
|
require "rack/test"
|
||||||
|
require 'debugger'
|
||||||
|
require 'active_support'
|
||||||
|
|
||||||
require "rack/attack"
|
require "rack/attack"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue