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 +35 -13
- data/lib/rack/attack.rb +18 -4
- data/lib/rack/attack/blacklist.rb +0 -1
- data/lib/rack/attack/track.rb +10 -0
- data/lib/rack/attack/version.rb +1 -1
- data/lib/rack/attack/whitelist.rb +0 -1
- data/spec/rack_attack_spec.rb +2 -61
- data/spec/rack_attack_throttle_spec.rb +44 -0
- data/spec/rack_attack_track_spec.rb +44 -0
- data/spec/spec_helper.rb +22 -0
- metadata +8 -3
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 *
|
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 *
|
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
|
38
|
-
* If the request matches any blacklist
|
39
|
-
* If the request matches any throttle
|
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
|
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
|
-
|
88
|
+
req.path == '/login' && req.post? && req.params['email']
|
75
89
|
end
|
76
90
|
|
77
|
-
###
|
91
|
+
### Tracks
|
78
92
|
|
79
|
-
#
|
80
|
-
|
81
|
-
|
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
|
[](http://travis-ci.org/ktheory/rack-attack)
|
153
|
+
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
132
154
|
|
133
155
|
## License
|
134
156
|
|
data/lib/rack/attack.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'rack'
|
2
2
|
module Rack::Attack
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
data/lib/rack/attack/version.rb
CHANGED
data/spec/rack_attack_spec.rb
CHANGED
@@ -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 '
|
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
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -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:
|
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:
|
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.
|
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
|