rack-attack 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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: []