rack-attack 1.3.2 → 2.0.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
@@ -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