rack-attack 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-attack might be problematic. Click here for more details.

data/README.md CHANGED
@@ -1,9 +1,122 @@
1
- # Rack::Attack - middleware for throttling & blocking abusive clients
1
+ # Rack::Attack
2
+ A DSL for blocking & thottling abusive clients
2
3
 
3
- ## Processing order
4
- * If any whitelist matches, the request is allowed
5
- * If any blacklist matches, the request is blocked (unless a whitelist matched)
6
- * If any throttle matches, the request is throttled (unless a whitelist or blacklist matched)
4
+ Rack::Attack is a rack middleware to protect your web app from bad clients.
5
+ It allows *whitelisting*, *blacklisting*, and *thottling* based on arbitrary properties of the request.
7
6
 
8
- [![Travis CI](https://secure.travis-ci.org/ktheory/rack-attack.png)](http://travis-ci.org/ktheory/rack-attack)
7
+ Thottle state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached.
8
+
9
+ ## Installation
10
+
11
+ Add the [rack-attack](http://rubygems.org/gems/rack-attack) gem to your Gemfile or run
12
+
13
+ gem install rack-attack
14
+
15
+ Tell your app to use the Rack::Attack middleware.
16
+ For Rails 3 apps:
17
+
18
+ # In config/application.rb
19
+ config.middleware.use Rack::Attack
20
+
21
+ Or in your `config.ru`:
22
+
23
+ use Rack::Attack
24
+
25
+ Optionally configure the cache store for throttling:
26
+
27
+ Rack::Attack.cache.store = my_cache_store # defaults to Rails.cache
28
+
29
+ Note that `Rack::Attack.cache` is only used for throttling, not blacklisting & whitelisting.
30
+
31
+ ## How it works
32
+
33
+ The Rack::Attack middleware examines each request against *whitelists*, *blacklists*, and *throttles* that you define. There are none by default.
34
+
35
+ * If the request matches any whitelist, the request is allowed. Blacklists and throttles are not checked.
36
+ * If the request matches any blacklist, the request is blocked. Throttles are not checked.
37
+ * If the request matches any throttle, a counter is incremented in the Rack::Attack.cache. If the throttle limit is exceeded, the request is blocked and further throttles are not checked.
38
+
39
+ ## Usage
40
+
41
+ Define blacklists, throttles, and whitelists.
42
+ Note that `req` is a [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object.
43
+
44
+ ### Blacklists
45
+
46
+ # Block requests from 1.2.3.4
47
+ Rack::Attack.blacklist('block 1.2.3.4') do |req|
48
+ # Request are blocked if the return value is truthy
49
+ '1.2.3.4' == req.ip
50
+ end
51
+
52
+ # Block logins from a bad user agent
53
+ Rack::Attack.blacklist('block bad UA logins') do |req|
54
+ req.post? && request.path == '/login' && req.user_agent == 'BadUA'
55
+ end
56
+
57
+ ### Throttles
58
+
59
+ # Throttle requests to 5 requests per second per ip
60
+ Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
61
+ # If the return value is truthy, the cache key for "rack::attack:req/ip:#{req.ip}" is incremented and checked.
62
+ # If falsy, the cache key is neither incremented or checked.
63
+ req.ip
64
+ end
9
65
 
66
+ # Throttle login attempts for a given email parameter to 6 reqs/minute
67
+ Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
68
+ req.post? && request.path == '/login' && req.params['email']
69
+ end
70
+
71
+ ### Whitelists
72
+
73
+ # Always allow requests from localhost
74
+ # (blacklist & throttles are skipped)
75
+ Rack::Attack.whitelist('allow from localhost') do |req|
76
+ # Requests are allowed if the return value is truthy
77
+ '127.0.0.1' == req.ip
78
+ end
79
+
80
+ ## Responses
81
+
82
+ Customize the response of throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
83
+
84
+ Rack:Attack.throttled_response = lambda do |env|
85
+ env['rack.attack.throttled'] # name and other data about the matched throttle
86
+ [ 503, {}, ['Throttled']]
87
+ end
88
+
89
+ Similarly for blacklisted responses:
90
+
91
+ Rack:Attack.blacklisted_response = lambda do |env|
92
+ env['rack.attack.blacklisted'] # name of the matched blacklist
93
+ [ 503, {}, ['Blocked']]
94
+ end
95
+
96
+ ## Logging & Instrumentation
97
+
98
+ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available.
99
+
100
+ You can subscribe to 'rack.attack.{blacklist,throttle,whitelist}' events and log it, graph it, etc:
101
+
102
+ ActiveSupport::Notifications.subscribe('rack.attack.blacklist') do |name, start, finish, request_id, req|
103
+ puts req.inspect
104
+ end
105
+
106
+ ## Motivation
107
+
108
+ Abusive clients range from malicious login crackers to naively-written scrapers.
109
+ They hinder the security, performance, & availability of web applications.
110
+
111
+ It is impractical if not impossible to block abusive clients completely.
112
+
113
+ Rack::Attack aims to let developers quickly mitigate abusive requests and rely
114
+ less on short-term, one-off hacks to block a particular attack.
115
+
116
+ Rack::Attack complements `iptables` and nginx's [limit_zone module](http://wiki.nginx.org/HttpLimitZoneModule).
117
+
118
+ ## Thanks
119
+
120
+ Thanks to [Kickstarter](https://github.com/kickstarter) for sponsoring Rack::Attack development
121
+
122
+ [![Travis CI](https://secure.travis-ci.org/ktheory/rack-attack.png)](http://travis-ci.org/ktheory/rack-attack)
@@ -8,17 +8,18 @@ module Rack::Attack
8
8
  class << self
9
9
 
10
10
  attr_reader :cache, :notifier
11
+ attr_accessor :blacklisted_response, :throttled_response
11
12
 
12
13
  def whitelist(name, &block)
13
- (@whitelists ||= {})[name] = Whitelist.new(name, block)
14
+ self.whitelists[name] = Whitelist.new(name, block)
14
15
  end
15
16
 
16
17
  def blacklist(name, &block)
17
- (@blacklists ||= {})[name] = Blacklist.new(name, block)
18
+ self.blacklists[name] = Blacklist.new(name, block)
18
19
  end
19
20
 
20
21
  def throttle(name, options, &block)
21
- (@throttles ||= {})[name] = Throttle.new(name, options, block)
22
+ self.throttles[name] = Throttle.new(name, options, block)
22
23
  end
23
24
 
24
25
  def whitelists; @whitelists ||= {}; end
@@ -26,13 +27,20 @@ module Rack::Attack
26
27
  def throttles; @throttles ||= {}; end
27
28
 
28
29
  def new(app)
29
- @cache ||= Cache.new
30
- @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
31
30
  @app = app
31
+
32
+ # Set defaults
33
+ @cache ||= Cache.new
34
+ @notifier ||= ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
35
+ @blacklisted_response ||= lambda {|env| [503, {}, ['Blocked']] }
36
+ @throttled_response ||= lambda {|env|
37
+ retry_after = env['rack.attack.matched'][:period] rescue nil
38
+ [503, {'Retry-After' => retry_after}, ['Retry later']]
39
+ }
40
+
32
41
  self
33
42
  end
34
43
 
35
-
36
44
  def call(env)
37
45
  req = Rack::Request.new(env)
38
46
 
@@ -41,9 +49,9 @@ module Rack::Attack
41
49
  end
42
50
 
43
51
  if blacklisted?(req)
44
- blacklisted_response
52
+ blacklisted_response[env]
45
53
  elsif throttled?(req)
46
- throttled_response
54
+ throttled_response[env]
47
55
  else
48
56
  @app.call(env)
49
57
  end
@@ -67,19 +75,11 @@ module Rack::Attack
67
75
  end
68
76
  end
69
77
 
70
- def instrument(payload)
71
- notifier.instrument('rack.attack', payload) if notifier
72
- end
73
-
74
- def blacklisted_response
75
- [503, {}, ['Blocked']]
76
- end
77
-
78
- def throttled_response
79
- [503, {}, ['Throttled']]
78
+ def instrument(type, payload)
79
+ notifier.instrument("rack.attack.#{type}", payload) if notifier
80
80
  end
81
81
 
82
- def clear!
82
+ def clear!
83
83
  @whitelists, @blacklists, @throttles = {}, {}, {}
84
84
  end
85
85
 
@@ -9,7 +9,10 @@ module Rack
9
9
 
10
10
  def [](req)
11
11
  block[req].tap {|match|
12
- Rack::Attack.instrument(:type => type, :name => name, :request => req) if match
12
+ if match
13
+ req.env["rack.attack.matched"] = {type => name}
14
+ Rack::Attack.instrument(type, req)
15
+ end
13
16
  }
14
17
  end
15
18
 
@@ -21,9 +21,12 @@ module Rack
21
21
 
22
22
  key = "#{name}:#{discriminator}"
23
23
  count = cache.count(key, period)
24
- throttled = count > limit
25
- Rack::Attack.instrument(:type => :throttle, :name => name, :request => req, :count => count, :throttled => throttled)
26
- throttled
24
+ (count > limit).tap do |throttled|
25
+ if throttled
26
+ req.env['rack.attack.matched'] = {:throttle => name, :count => count, :period => period, :limit => limit}
27
+ Rack::Attack.instrument(:throttle, req)
28
+ end
29
+ end
27
30
  end
28
31
 
29
32
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Attack
3
- VERSION = '0.0.3'
3
+ VERSION = '0.1.0'
4
4
  end
5
5
  end
@@ -30,12 +30,18 @@ describe 'Rack::Attack' do
30
30
 
31
31
  it('has a blacklist') { Rack::Attack.blacklists.key?("ip #{@bad_ip}") }
32
32
 
33
- it "should blacklist bad requests" do
34
- get '/', {}, 'REMOTE_ADDR' => @bad_ip
35
- last_response.status.must_equal 503
36
- end
33
+ describe "a bad request" do
34
+ before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
35
+ it "should return a blacklist response" do
36
+ get '/', {}, 'REMOTE_ADDR' => @bad_ip
37
+ last_response.status.must_equal 503
38
+ end
39
+ it "should tag the env" do
40
+ last_request.env['rack.attack.matched'].must_equal({:blacklist => "ip #{@bad_ip}"})
41
+ end
37
42
 
38
- allow_ok_requests
43
+ allow_ok_requests
44
+ end
39
45
 
40
46
  describe "and with a whitelist" do
41
47
  before do
@@ -44,9 +50,15 @@ describe 'Rack::Attack' do
44
50
  end
45
51
 
46
52
  it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") }
47
- it "should allow whitelists before blacklists" do
48
- get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
49
- last_response.status.must_equal 200
53
+ describe "with a request match both whitelist & blacklist" do
54
+ before { get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua }
55
+ it "should allow whitelists before blacklists" do
56
+ get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
57
+ last_response.status.must_equal 200
58
+ end
59
+ it "should tag the env" do
60
+ last_request.env['rack.attack.matched'].must_equal({:whitelist => 'good ua'})
61
+ end
50
62
  end
51
63
  end
52
64
  end
@@ -60,18 +72,26 @@ describe 'Rack::Attack' do
60
72
  it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
61
73
  allow_ok_requests
62
74
 
63
- it 'should set the counter for one request' do
64
- get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
65
- Rack::Attack.cache.store.read('rack::attack:ip/sec:1.2.3.4').must_equal 1
75
+ describe 'a single request' do
76
+ before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
77
+ it 'should set the counter for one request' do
78
+ Rack::Attack.cache.store.read('rack::attack:ip/sec:1.2.3.4').must_equal 1
79
+ end
66
80
  end
67
-
68
- it 'should block 2 requests' do
69
- 2.times do
70
- get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
81
+ describe "with 2 requests" do
82
+ before do
83
+ 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
84
+ end
85
+ it 'should block the last request' do
86
+ last_response.status.must_equal 503
87
+ end
88
+ it 'should tag the env' do
89
+ last_request.env['rack.attack.matched'].must_equal({:throttle => 'ip/sec', :count => 2, :limit => 1, :period => 1})
90
+ end
91
+ it 'should set a Retry-After header' do
92
+ last_response.headers['Retry-After'].must_equal 1
71
93
  end
72
- last_response.status.must_equal 503
73
94
  end
74
- end
75
-
76
95
 
96
+ end
77
97
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-attack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-27 00:00:00.000000000 Z
12
+ date: 2012-07-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -107,7 +107,7 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: 1.1.3
110
- description: A flexible rack middleware for throttling and blocking requests
110
+ description: A rack middleware for throttling and blocking abusive requests
111
111
  email: aaron@ktheory.com
112
112
  executables: []
113
113
  extensions: []