rack-attack 2.0.0 → 2.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 +26 -9
- data/lib/rack/attack.rb +5 -7
- data/lib/rack/attack/cache.rb +29 -3
- data/lib/rack/attack/version.rb +1 -1
- data/spec/rack_attack_cache_spec.rb +61 -0
- data/spec/rack_attack_throttle_spec.rb +0 -1
- data/spec/spec_helper.rb +1 -0
- metadata +36 -2
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
Rack::Attack is a rack middleware to protect your web app from bad clients.
|
5
5
|
It allows *whitelisting*, *blacklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
|
6
6
|
|
7
|
-
Throttle state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached.
|
7
|
+
Throttle state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached or redis.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -34,10 +34,27 @@ Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & w
|
|
34
34
|
|
35
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.
|
38
|
-
*
|
39
|
-
*
|
40
|
-
*
|
37
|
+
* If the request matches any **whitelist**, it is allowed.
|
38
|
+
* Otherwise, if the request matches any **blacklist**, it is blocked.
|
39
|
+
* Otherwise, 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.
|
40
|
+
* Otherwise, all **tracks** are checked, and the request is allowed.
|
41
|
+
|
42
|
+
The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
|
43
|
+
|
44
|
+
def call(env)
|
45
|
+
req = Rack::Request.new(env)
|
46
|
+
|
47
|
+
if whitelisted?(req)
|
48
|
+
@app.call(env)
|
49
|
+
elsif blacklisted?(req)
|
50
|
+
blacklisted_response[env]
|
51
|
+
elsif throttled?(req)
|
52
|
+
throttled_response[env]
|
53
|
+
else
|
54
|
+
tracked?(req)
|
55
|
+
@app.call(env)
|
56
|
+
end
|
57
|
+
end
|
41
58
|
|
42
59
|
## About Tracks
|
43
60
|
|
@@ -108,11 +125,11 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
|
|
108
125
|
|
109
126
|
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).
|
110
127
|
|
111
|
-
Rack
|
128
|
+
Rack::Attack.blacklisted_response = lambda do |env|
|
112
129
|
[ 503, {}, ['Blocked']]
|
113
130
|
end
|
114
131
|
|
115
|
-
Rack
|
132
|
+
Rack::Attack.throttled_response = lambda do |env|
|
116
133
|
# name and other data about the matched throttle
|
117
134
|
body = [
|
118
135
|
env['rack.attack.matched'],
|
@@ -149,8 +166,8 @@ less on short-term, one-off hacks to block a particular attack.
|
|
149
166
|
|
150
167
|
Rack::Attack complements tools like iptables and nginx's [limit_zone module](http://wiki.nginx.org/HttpLimitZoneModule).
|
151
168
|
|
152
|
-
[](https://travis-ci.org/kickstarter/rack-attack)
|
170
|
+
[](https://codeclimate.com/github/kickstarter/rack-attack)
|
154
171
|
|
155
172
|
## License
|
156
173
|
|
data/lib/rack/attack.rb
CHANGED
@@ -37,10 +37,10 @@ module Rack::Attack
|
|
37
37
|
|
38
38
|
# Set defaults
|
39
39
|
@notifier ||= ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
40
|
-
@blacklisted_response ||= lambda {|env| [503, {}, [
|
40
|
+
@blacklisted_response ||= lambda {|env| [503, {}, ["Blocked\n"]] }
|
41
41
|
@throttled_response ||= lambda {|env|
|
42
42
|
retry_after = env['rack.attack.match_data'][:period] rescue nil
|
43
|
-
[503, {'Retry-After' => retry_after.to_s}, [
|
43
|
+
[503, {'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
|
44
44
|
}
|
45
45
|
|
46
46
|
self
|
@@ -50,10 +50,8 @@ module Rack::Attack
|
|
50
50
|
req = Rack::Request.new(env)
|
51
51
|
|
52
52
|
if whitelisted?(req)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if blacklisted?(req)
|
53
|
+
@app.call(env)
|
54
|
+
elsif blacklisted?(req)
|
57
55
|
blacklisted_response[env]
|
58
56
|
elsif throttled?(req)
|
59
57
|
throttled_response[env]
|
@@ -95,7 +93,7 @@ module Rack::Attack
|
|
95
93
|
@cache ||= Cache.new
|
96
94
|
end
|
97
95
|
|
98
|
-
|
96
|
+
def clear!
|
99
97
|
@whitelists, @blacklists, @throttles = {}, {}, {}
|
100
98
|
end
|
101
99
|
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -2,17 +2,43 @@ module Rack
|
|
2
2
|
module Attack
|
3
3
|
class Cache
|
4
4
|
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :prefix
|
6
|
+
|
6
7
|
def initialize
|
7
|
-
|
8
|
+
self.store = ::Rails.cache if defined?(::Rails.cache)
|
8
9
|
@prefix = 'rack::attack'
|
9
10
|
end
|
10
11
|
|
12
|
+
attr_reader :store
|
13
|
+
def store=(store)
|
14
|
+
# RedisStore#increment needs different behavior, so detect that
|
15
|
+
# (method has an arity of 2; must call #expire seperately
|
16
|
+
if defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
|
17
|
+
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
18
|
+
# so use the raw Redis::Store instead
|
19
|
+
@store = store.instance_variable_get(:@data)
|
20
|
+
else
|
21
|
+
@redis_store = false
|
22
|
+
@store = store
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
11
26
|
def count(unprefixed_key, period)
|
12
27
|
epoch_time = Time.now.to_i
|
13
28
|
expires_in = period - (epoch_time % period)
|
14
29
|
key = "#{prefix}:#{(epoch_time/period).to_i}:#{unprefixed_key}"
|
15
|
-
|
30
|
+
do_count(key, expires_in)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def do_count(key, expires_in)
|
35
|
+
# Workaround Redis::Store's interface
|
36
|
+
if defined?(::Redis::Store) && store.is_a?(::Redis::Store)
|
37
|
+
result = store.incr(key)
|
38
|
+
store.expire(key, expires_in)
|
39
|
+
else
|
40
|
+
result = store.increment(key, 1, :expires_in => expires_in)
|
41
|
+
end
|
16
42
|
# NB: Some stores return nil when incrementing uninitialized values
|
17
43
|
if result.nil?
|
18
44
|
store.write(key, 1, :expires_in => expires_in)
|
data/lib/rack/attack/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
if ENV['TEST_INTEGRATION']
|
4
|
+
describe Rack::Attack::Cache do
|
5
|
+
def delete(key)
|
6
|
+
if @cache.store.respond_to?(:delete)
|
7
|
+
@cache.store.delete(key)
|
8
|
+
else
|
9
|
+
@cache.store.del(key)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'active_support/cache/dalli_store'
|
14
|
+
require 'active_support/cache/redis_store'
|
15
|
+
cache_stores = [
|
16
|
+
ActiveSupport::Cache::MemoryStore.new,
|
17
|
+
ActiveSupport::Cache::DalliStore.new("localhost"),
|
18
|
+
ActiveSupport::Cache::RedisStore.new("localhost"),
|
19
|
+
Redis::Store.new
|
20
|
+
]
|
21
|
+
|
22
|
+
cache_stores.each do |store|
|
23
|
+
describe "with #{store.class}" do
|
24
|
+
|
25
|
+
before {
|
26
|
+
@cache ||= Rack::Attack::Cache.new
|
27
|
+
@key = "rack::attack:cache-test-key"
|
28
|
+
@expires_in = 1
|
29
|
+
@cache.store = store
|
30
|
+
delete(@key)
|
31
|
+
}
|
32
|
+
|
33
|
+
after { delete(@key) }
|
34
|
+
|
35
|
+
describe "do_count once" do
|
36
|
+
it "should be 1" do
|
37
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "do_count twice" do
|
42
|
+
it "must be 2" do
|
43
|
+
@cache.send(:do_count, @key, @expires_in)
|
44
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 2
|
45
|
+
end
|
46
|
+
end
|
47
|
+
describe "do_count after expires_in" do
|
48
|
+
it "must be 1" do
|
49
|
+
@cache.send(:do_count, @key, @expires_in)
|
50
|
+
sleep @expires_in # sigh
|
51
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
else
|
60
|
+
puts 'Skipping cache store integration tests (set ENV["TEST_INTEGRATION"] to enable)'
|
61
|
+
end
|
data/spec/spec_helper.rb
CHANGED
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: 2.
|
4
|
+
version: 2.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: 2013-
|
12
|
+
date: 2013-03-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -107,6 +107,38 @@ dependencies:
|
|
107
107
|
- - ~>
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: 1.1.3
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: redis-activesupport
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: dalli
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
110
142
|
description: A rack middleware for throttling and blocking abusive requests
|
111
143
|
email: aaron@ktheory.com
|
112
144
|
executables: []
|
@@ -123,6 +155,7 @@ files:
|
|
123
155
|
- lib/rack/attack.rb
|
124
156
|
- Rakefile
|
125
157
|
- README.md
|
158
|
+
- spec/rack_attack_cache_spec.rb
|
126
159
|
- spec/rack_attack_spec.rb
|
127
160
|
- spec/rack_attack_throttle_spec.rb
|
128
161
|
- spec/rack_attack_track_spec.rb
|
@@ -153,6 +186,7 @@ signing_key:
|
|
153
186
|
specification_version: 3
|
154
187
|
summary: Block & throttle abusive requests
|
155
188
|
test_files:
|
189
|
+
- spec/rack_attack_cache_spec.rb
|
156
190
|
- spec/rack_attack_spec.rb
|
157
191
|
- spec/rack_attack_throttle_spec.rb
|
158
192
|
- spec/rack_attack_track_spec.rb
|