rack-attack 1.3.2 → 2.0.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
@@ -2,7 +2,7 @@
2
2
  *A DSL for blocking & throttling abusive clients*
3
3
 
4
4
  Rack::Attack is a rack middleware to protect your web app from bad clients.
5
- It allows *whitelisting*, *blacklisting*, and *throttling* based on arbitrary properties of the request.
5
+ It allows *whitelisting*, *blacklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
6
6
 
7
7
  Throttle state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached.
8
8
 
@@ -32,17 +32,31 @@ Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & w
32
32
 
33
33
  ## How it works
34
34
 
35
- The Rack::Attack middleware compares each request against *whitelists*, *blacklists*, and *throttles* that you define. There are none by default.
35
+ The Rack::Attack middleware compares each request against *whitelists*, *blacklists*, *throttles*, and *tracks* that you define. There are none by default.
36
36
 
37
- * If the request matches any whitelist, it is allowed. Blacklists and throttles are not checked.
38
- * If the request matches any blacklist, it is blocked. Throttles are not checked.
39
- * 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.
37
+ * If the request matches any **whitelist**, it is allowed. Blacklists and throttles are not checked.
38
+ * If the request matches any **blacklist**, it is blocked. Throttles are not checked.
39
+ * 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.
40
+ * If the request was not whitelisted, blacklisted, or throttled; all **tracks** are checked.
41
+
42
+ ## About Tracks
43
+
44
+ `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
40
45
 
41
46
  ## Usage
42
47
 
43
- Define blacklists, throttles, and whitelists as blocks that return truthy of falsy values.
48
+ Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise.
44
49
  A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object is passed to the block (named 'req' in the examples).
45
50
 
51
+ ### Whitelists
52
+
53
+ # Always allow requests from localhost
54
+ # (blacklist & throttles are skipped)
55
+ Rack::Attack.whitelist('allow from localhost') do |req|
56
+ # Requests are allowed if the return value is truthy
57
+ '127.0.0.1' == req.ip
58
+ end
59
+
46
60
  ### Blacklists
47
61
 
48
62
  # Block requests from 1.2.3.4
@@ -71,18 +85,25 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
71
85
 
72
86
  # Throttle login attempts for a given email parameter to 6 reqs/minute
73
87
  Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
74
- request.path == '/login' && req.post? && req.params['email']
88
+ req.path == '/login' && req.post? && req.params['email']
75
89
  end
76
90
 
77
- ### Whitelists
91
+ ### Tracks
78
92
 
79
- # Always allow requests from localhost
80
- # (blacklist & throttles are skipped)
81
- Rack::Attack.whitelist('allow from localhost') do |req|
82
- # Requests are allowed if the return value is truthy
83
- '127.0.0.1' == req.ip
93
+ # Track requests from a special user agent
94
+ Rack::Attack.track("special_agent") do |req|
95
+ req.user_agent == "SpecialAgent"
84
96
  end
85
97
 
98
+ # Track it using ActiveSupport::Notification
99
+ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req|
100
+ if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track
101
+ Rails.logger.info "special_agent: #{req.path}"
102
+ STATSD.increment("special_agent")
103
+ end
104
+ end
105
+
106
+
86
107
  ## Responses
87
108
 
88
109
  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).
@@ -129,6 +150,7 @@ less on short-term, one-off hacks to block a particular attack.
129
150
  Rack::Attack complements tools like iptables and nginx's [limit_zone module](http://wiki.nginx.org/HttpLimitZoneModule).
130
151
 
131
152
  [![Travis CI](https://secure.travis-ci.org/ktheory/rack-attack.png)](http://travis-ci.org/ktheory/rack-attack)
153
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/kickstarter/rack-attack)
132
154
 
133
155
  ## License
134
156
 
@@ -1,9 +1,11 @@
1
1
  require 'rack'
2
2
  module Rack::Attack
3
- require 'rack/attack/cache'
4
- require 'rack/attack/throttle'
5
- require 'rack/attack/whitelist'
6
- require 'rack/attack/blacklist'
3
+ autoload :Cache, 'rack/attack/cache'
4
+ autoload :Check, 'rack/attack/check'
5
+ autoload :Throttle, 'rack/attack/throttle'
6
+ autoload :Whitelist, 'rack/attack/whitelist'
7
+ autoload :Blacklist, 'rack/attack/blacklist'
8
+ autoload :Track, 'rack/attack/track'
7
9
 
8
10
  class << self
9
11
 
@@ -21,9 +23,14 @@ module Rack::Attack
21
23
  self.throttles[name] = Throttle.new(name, options, block)
22
24
  end
23
25
 
26
+ def track(name, &block)
27
+ self.tracks[name] = Track.new(name, block)
28
+ end
29
+
24
30
  def whitelists; @whitelists ||= {}; end
25
31
  def blacklists; @blacklists ||= {}; end
26
32
  def throttles; @throttles ||= {}; end
33
+ def tracks; @tracks ||= {}; end
27
34
 
28
35
  def new(app)
29
36
  @app = app
@@ -51,6 +58,7 @@ module Rack::Attack
51
58
  elsif throttled?(req)
52
59
  throttled_response[env]
53
60
  else
61
+ tracked?(req)
54
62
  @app.call(env)
55
63
  end
56
64
  end
@@ -73,6 +81,12 @@ module Rack::Attack
73
81
  end
74
82
  end
75
83
 
84
+ def tracked?(req)
85
+ tracks.each_value do |tracker|
86
+ tracker[req]
87
+ end
88
+ end
89
+
76
90
  def instrument(req)
77
91
  notifier.instrument('rack.attack', req) if notifier
78
92
  end
@@ -1,4 +1,3 @@
1
- require_relative 'check'
2
1
  module Rack
3
2
  module Attack
4
3
  class Blacklist < Check
@@ -0,0 +1,10 @@
1
+ module Rack
2
+ module Attack
3
+ class Track < Check
4
+ def initialize(name, block)
5
+ super
6
+ @type = :track
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Attack
3
- VERSION = '1.3.2'
3
+ VERSION = '2.0.0'
4
4
  end
5
5
  end
@@ -1,4 +1,3 @@
1
- require_relative 'check'
2
1
  module Rack
3
2
  module Attack
4
3
  class Whitelist < Check
@@ -1,28 +1,9 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  describe 'Rack::Attack' do
4
- include Rack::Test::Methods
5
-
6
- def app
7
- Rack::Builder.new {
8
- use Rack::Attack
9
- run lambda {|env| [200, {}, ['Hello World']]}
10
- }.to_app
11
- end
12
-
13
- def self.allow_ok_requests
14
- it "must allow ok requests" do
15
- get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
16
- last_response.status.must_equal 200
17
- last_response.body.must_equal 'Hello World'
18
- end
19
- end
20
-
21
- after { Rack::Attack.clear! }
22
-
23
4
  allow_ok_requests
24
5
 
25
- describe 'with a blacklist' do
6
+ describe 'blacklist' do
26
7
  before do
27
8
  @bad_ip = '1.2.3.4'
28
9
  Rack::Attack.blacklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip }
@@ -44,7 +25,7 @@ describe 'Rack::Attack' do
44
25
  allow_ok_requests
45
26
  end
46
27
 
47
- describe "and with a whitelist" do
28
+ describe "and whitelist" do
48
29
  before do
49
30
  @good_ua = 'GoodUA'
50
31
  Rack::Attack.whitelist("good ua") {|req| req.user_agent == @good_ua }
@@ -65,44 +46,4 @@ describe 'Rack::Attack' do
65
46
  end
66
47
  end
67
48
 
68
- describe 'with a throttle' do
69
- before do
70
- @period = 60 # Use a long period; failures due to cache key rotation less likely
71
- Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
72
- Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
73
- end
74
-
75
- it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
76
- allow_ok_requests
77
-
78
- describe 'a single request' do
79
- before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
80
- it 'should set the counter for one request' do
81
- key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
82
- Rack::Attack.cache.store.read(key).must_equal 1
83
- end
84
-
85
- it 'should populate throttle data' do
86
- data = { :count => 1, :limit => 1, :period => @period }
87
- last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
88
- end
89
- end
90
- describe "with 2 requests" do
91
- before do
92
- 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
93
- end
94
- it 'should block the last request' do
95
- last_response.status.must_equal 503
96
- end
97
- it 'should tag the env' do
98
- last_request.env['rack.attack.matched'].must_equal 'ip/sec'
99
- last_request.env['rack.attack.match_type'].must_equal :throttle
100
- last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period})
101
- end
102
- it 'should set a Retry-After header' do
103
- last_response.headers['Retry-After'].must_equal @period.to_s
104
- end
105
- end
106
-
107
- end
108
49
  end
@@ -0,0 +1,44 @@
1
+
2
+ require_relative 'spec_helper'
3
+ describe 'Rack::Attack.throttle' do
4
+ before do
5
+ @period = 60 # Use a long period; failures due to cache key rotation less likely
6
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
7
+ Rack::Attack.throttle('ip/sec', :limit => 1, :period => @period) { |req| req.ip }
8
+ end
9
+
10
+ it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
11
+ allow_ok_requests
12
+
13
+ describe 'a single request' do
14
+ before { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
15
+ it 'should set the counter for one request' do
16
+ key = "rack::attack:#{Time.now.to_i/@period}:ip/sec:1.2.3.4"
17
+ Rack::Attack.cache.store.read(key).must_equal 1
18
+ end
19
+
20
+ it 'should populate throttle data' do
21
+ data = { :count => 1, :limit => 1, :period => @period }
22
+ last_request.env['rack.attack.throttle_data']['ip/sec'].must_equal data
23
+ end
24
+ end
25
+ describe "with 2 requests" do
26
+ before do
27
+ 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
28
+ end
29
+ it 'should block the last request' do
30
+ last_response.status.must_equal 503
31
+ end
32
+ it 'should tag the env' do
33
+ last_request.env['rack.attack.matched'].must_equal 'ip/sec'
34
+ last_request.env['rack.attack.match_type'].must_equal :throttle
35
+ last_request.env['rack.attack.match_data'].must_equal({:count => 2, :limit => 1, :period => @period})
36
+ end
37
+ it 'should set a Retry-After header' do
38
+ last_response.headers['Retry-After'].must_equal @period.to_s
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+
@@ -0,0 +1,44 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'Rack::Attack.track' do
4
+ class Counter
5
+ def self.incr
6
+ @counter += 1
7
+ end
8
+
9
+ def self.reset
10
+ @counter = 0
11
+ end
12
+
13
+ def self.check
14
+ @counter
15
+ end
16
+ end
17
+
18
+ before do
19
+ Rack::Attack.track("everything"){ |req| true }
20
+ end
21
+ allow_ok_requests
22
+ it "should tag the env" do
23
+ get '/'
24
+ last_request.env['rack.attack.matched'].must_equal 'everything'
25
+ last_request.env['rack.attack.match_type'].must_equal :track
26
+ end
27
+
28
+ describe "with a notification subscriber and two tracks" do
29
+ before do
30
+ Counter.reset
31
+ # A second track
32
+ Rack::Attack.track("homepage"){ |req| req.path == "/"}
33
+
34
+ ActiveSupport::Notifications.subscribe("rack.attack") do |*args|
35
+ Counter.incr
36
+ end
37
+ get "/"
38
+ end
39
+
40
+ it "should notify twice" do
41
+ Counter.check.must_equal 2
42
+ end
43
+ end
44
+ end
@@ -7,3 +7,25 @@ require 'debugger'
7
7
  require 'active_support'
8
8
 
9
9
  require "rack/attack"
10
+
11
+ class Minitest::Spec
12
+
13
+ include Rack::Test::Methods
14
+
15
+ after { Rack::Attack.clear! }
16
+
17
+ def app
18
+ Rack::Builder.new {
19
+ use Rack::Attack
20
+ run lambda {|env| [200, {}, ['Hello World']]}
21
+ }.to_app
22
+ end
23
+
24
+ def self.allow_ok_requests
25
+ it "must allow ok requests" do
26
+ get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
27
+ last_response.status.must_equal 200
28
+ last_response.body.must_equal 'Hello World'
29
+ end
30
+ end
31
+ 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: 1.3.2
4
+ version: 2.0.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-08-15 00:00:00.000000000 Z
12
+ date: 2013-01-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -117,12 +117,15 @@ files:
117
117
  - lib/rack/attack/cache.rb
118
118
  - lib/rack/attack/check.rb
119
119
  - lib/rack/attack/throttle.rb
120
+ - lib/rack/attack/track.rb
120
121
  - lib/rack/attack/version.rb
121
122
  - lib/rack/attack/whitelist.rb
122
123
  - lib/rack/attack.rb
123
124
  - Rakefile
124
125
  - README.md
125
126
  - spec/rack_attack_spec.rb
127
+ - spec/rack_attack_throttle_spec.rb
128
+ - spec/rack_attack_track_spec.rb
126
129
  - spec/spec_helper.rb
127
130
  homepage: http://github.com/kickstarter/rack-attack
128
131
  licenses: []
@@ -145,10 +148,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
148
  version: '0'
146
149
  requirements: []
147
150
  rubyforge_project:
148
- rubygems_version: 1.8.23
151
+ rubygems_version: 1.8.24
149
152
  signing_key:
150
153
  specification_version: 3
151
154
  summary: Block & throttle abusive requests
152
155
  test_files:
153
156
  - spec/rack_attack_spec.rb
157
+ - spec/rack_attack_throttle_spec.rb
158
+ - spec/rack_attack_track_spec.rb
154
159
  - spec/spec_helper.rb