rack-attack 2.1.1 → 2.2.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.
- checksums.yaml +6 -14
- data/README.md +54 -4
- data/lib/rack/attack.rb +2 -0
- data/lib/rack/attack/cache.rb +11 -16
- data/lib/rack/attack/fail2ban.rb +43 -0
- data/lib/rack/attack/store_proxy.rb +51 -0
- data/lib/rack/attack/version.rb +1 -1
- data/spec/fail2ban_spec.rb +121 -0
- data/spec/rack_attack_cache_spec.rb +23 -0
- metadata +23 -20
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
ZDJmYWJjMzhhYTdkZTYxMjM0ZDY2OGQ0ZjA2NzQ2ZGU2YTBiOWY2N2EwYTA2
|
10
|
-
MGIzNTBiMjAwMDhhYWE2YzMyZTI0OTk3MGM5MjU4NmQzYTc4YzdmNDYxNzcx
|
11
|
-
OTljOTVjNGE4YTQ1ZDQyMzY2MDdiMjUyZGFhNjFjNzc0NDBiZWQ=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
Y2E5NGViNGQxMGFmMTM0ZWM2N2YzYWQ4Yjg4ZTkyMDdkNTY5NGVjOWUwNzEx
|
14
|
-
M2ViYWJlZWFiNTcxNGIwODk0Nzk5MDY5YjA5N2Y2MjE5ZjYxYTkzNzcyMWFk
|
15
|
-
NjQ4YzVhZGY0N2U2NzE1NjQwODkyYzIzMDFmZDMxYmM0ZmVlM2U=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 30436667301e528e76d76b7454ad73ca9dd08a08
|
4
|
+
data.tar.gz: 72e275cb98d8e38b478c4db4d7fa5bdb4759c7ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2a8d0690b58f15a6f512077408fa40cec5c7e23b39ebc470a53a753273f1497e3605930106d2c16dd41a8b3edef39a3e9b66b0fd4778b871c5ab0017e2bd4ac
|
7
|
+
data.tar.gz: 2a097e071cafcdb11cf36d4150c459723877a852b7920fdf3ed5fa74cedf1ea3d19b25357d0bd31c3b47c48d3a938575e877aade034e8fc7f5eba882101385b5
|
data/README.md
CHANGED
@@ -10,23 +10,30 @@ Throttle state is stored in a configurable cache (e.g. `Rails.cache`), presumabl
|
|
10
10
|
|
11
11
|
Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
|
12
12
|
|
13
|
+
```ruby
|
13
14
|
# In your Gemfile
|
14
15
|
gem 'rack-attack'
|
15
|
-
|
16
|
+
```
|
16
17
|
Tell your app to use the Rack::Attack middleware.
|
17
18
|
For Rails 3 apps:
|
18
19
|
|
20
|
+
```ruby
|
19
21
|
# In config/application.rb
|
20
22
|
config.middleware.use Rack::Attack
|
23
|
+
```
|
21
24
|
|
22
25
|
Or for Rackup files:
|
23
26
|
|
27
|
+
```ruby
|
24
28
|
# In config.ru
|
25
29
|
use Rack::Attack
|
30
|
+
```
|
26
31
|
|
27
32
|
Optionally configure the cache store for throttling:
|
28
33
|
|
34
|
+
```ruby
|
29
35
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
|
36
|
+
```
|
30
37
|
|
31
38
|
Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
|
32
39
|
|
@@ -41,6 +48,7 @@ The Rack::Attack middleware compares each request against *whitelists*, *blackli
|
|
41
48
|
|
42
49
|
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
50
|
|
51
|
+
```ruby
|
44
52
|
def call(env)
|
45
53
|
req = Rack::Request.new(env)
|
46
54
|
|
@@ -55,6 +63,7 @@ The algorithm is actually more concise in code: See [Rack::Attack.call](https://
|
|
55
63
|
@app.call(env)
|
56
64
|
end
|
57
65
|
end
|
66
|
+
```
|
58
67
|
|
59
68
|
## About Tracks
|
60
69
|
|
@@ -62,20 +71,24 @@ The algorithm is actually more concise in code: See [Rack::Attack.call](https://
|
|
62
71
|
|
63
72
|
## Usage
|
64
73
|
|
65
|
-
Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise.
|
74
|
+
Define whitelists, blacklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
|
75
|
+
these go in an initializer in `config/initializers/`.
|
66
76
|
A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) object is passed to the block (named 'req' in the examples).
|
67
77
|
|
68
78
|
### Whitelists
|
69
79
|
|
80
|
+
```ruby
|
70
81
|
# Always allow requests from localhost
|
71
82
|
# (blacklist & throttles are skipped)
|
72
83
|
Rack::Attack.whitelist('allow from localhost') do |req|
|
73
84
|
# Requests are allowed if the return value is truthy
|
74
85
|
'127.0.0.1' == req.ip
|
75
86
|
end
|
87
|
+
```
|
76
88
|
|
77
89
|
### Blacklists
|
78
90
|
|
91
|
+
```ruby
|
79
92
|
# Block requests from 1.2.3.4
|
80
93
|
Rack::Attack.blacklist('block 1.2.3.4') do |req|
|
81
94
|
# Request are blocked if the return value is truthy
|
@@ -86,9 +99,31 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
|
|
86
99
|
Rack::Attack.blacklist('block bad UA logins') do |req|
|
87
100
|
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
|
88
101
|
end
|
102
|
+
```
|
103
|
+
|
104
|
+
#### Fail2Ban
|
105
|
+
|
106
|
+
`Fail2Ban.filter` can be used within a blacklist to block all requests from misbehaving clients.
|
107
|
+
This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
|
108
|
+
See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
|
109
|
+
how the parameters work.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# Block requests containing '/etc/password' in the params.
|
113
|
+
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
|
114
|
+
Rack::Attack.blacklist('fail2ban pentesters') do |req|
|
115
|
+
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
|
116
|
+
# so the request is blocked
|
117
|
+
Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
|
118
|
+
# The count for the IP is incremented if the return value is truthy.
|
119
|
+
CGI.unescape(req.query_string) =~ %r{/etc/passwd}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
89
123
|
|
90
124
|
### Throttles
|
91
125
|
|
126
|
+
```ruby
|
92
127
|
# Throttle requests to 5 requests per second per ip
|
93
128
|
Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
|
94
129
|
# If the return value is truthy, the cache key for the return value
|
@@ -101,12 +136,15 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
|
|
101
136
|
end
|
102
137
|
|
103
138
|
# Throttle login attempts for a given email parameter to 6 reqs/minute
|
139
|
+
# Return the email as a discriminator on POST /login requests
|
104
140
|
Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
|
105
|
-
req.path == '/login' && req.post?
|
141
|
+
req.params['email'] if req.path == '/login' && req.post?
|
106
142
|
end
|
143
|
+
```
|
107
144
|
|
108
145
|
### Tracks
|
109
146
|
|
147
|
+
```ruby
|
110
148
|
# Track requests from a special user agent
|
111
149
|
Rack::Attack.track("special_agent") do |req|
|
112
150
|
req.user_agent == "SpecialAgent"
|
@@ -119,12 +157,13 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
|
|
119
157
|
STATSD.increment("special_agent")
|
120
158
|
end
|
121
159
|
end
|
122
|
-
|
160
|
+
```
|
123
161
|
|
124
162
|
## Responses
|
125
163
|
|
126
164
|
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).
|
127
165
|
|
166
|
+
```ruby
|
128
167
|
Rack::Attack.blacklisted_response = lambda do |env|
|
129
168
|
[ 503, {}, ['Blocked']]
|
130
169
|
end
|
@@ -139,10 +178,13 @@ Customize the response of blacklisted and throttled requests using an object tha
|
|
139
178
|
|
140
179
|
[ 503, {}, [body]]
|
141
180
|
end
|
181
|
+
```
|
142
182
|
|
143
183
|
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
|
144
184
|
|
185
|
+
```ruby
|
145
186
|
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
|
187
|
+
```
|
146
188
|
|
147
189
|
## Logging & Instrumentation
|
148
190
|
|
@@ -150,9 +192,17 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
|
|
150
192
|
|
151
193
|
You can subscribe to 'rack.attack' events and log it, graph it, etc:
|
152
194
|
|
195
|
+
```ruby
|
153
196
|
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
|
154
197
|
puts req.inspect
|
155
198
|
end
|
199
|
+
```
|
200
|
+
|
201
|
+
## Testing
|
202
|
+
|
203
|
+
A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
|
204
|
+
need to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)
|
205
|
+
for more on how to do this.
|
156
206
|
|
157
207
|
## Performance
|
158
208
|
|
data/lib/rack/attack.rb
CHANGED
@@ -6,6 +6,8 @@ module Rack::Attack
|
|
6
6
|
autoload :Whitelist, 'rack/attack/whitelist'
|
7
7
|
autoload :Blacklist, 'rack/attack/blacklist'
|
8
8
|
autoload :Track, 'rack/attack/track'
|
9
|
+
autoload :StoreProxy,'rack/attack/store_proxy'
|
10
|
+
autoload :Fail2Ban, 'rack/attack/fail2ban'
|
9
11
|
|
10
12
|
class << self
|
11
13
|
|
data/lib/rack/attack/cache.rb
CHANGED
@@ -11,15 +11,7 @@ module Rack
|
|
11
11
|
|
12
12
|
attr_reader :store
|
13
13
|
def store=(store)
|
14
|
-
|
15
|
-
# (method has an arity of 2; must call #expire separately
|
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
|
-
@store = store
|
22
|
-
end
|
14
|
+
@store = StoreProxy.build(store)
|
23
15
|
end
|
24
16
|
|
25
17
|
def count(unprefixed_key, period)
|
@@ -29,15 +21,18 @@ module Rack
|
|
29
21
|
do_count(key, expires_in)
|
30
22
|
end
|
31
23
|
|
24
|
+
def read(unprefixed_key)
|
25
|
+
store.read("#{prefix}:#{unprefixed_key}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(unprefixed_key, value, expires_in)
|
29
|
+
store.write("#{prefix}:#{unprefixed_key}", value, :expires_in => expires_in)
|
30
|
+
end
|
31
|
+
|
32
32
|
private
|
33
33
|
def do_count(key, expires_in)
|
34
|
-
|
35
|
-
|
36
|
-
result = store.incr(key)
|
37
|
-
store.expire(key, expires_in)
|
38
|
-
else
|
39
|
-
result = store.increment(key, 1, :expires_in => expires_in)
|
40
|
-
end
|
34
|
+
result = store.increment(key, 1, :expires_in => expires_in)
|
35
|
+
|
41
36
|
# NB: Some stores return nil when incrementing uninitialized values
|
42
37
|
if result.nil?
|
43
38
|
store.write(key, 1, :expires_in => expires_in)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rack
|
2
|
+
module Attack
|
3
|
+
class Fail2Ban
|
4
|
+
class << self
|
5
|
+
def filter(discriminator, options)
|
6
|
+
bantime = options[:bantime] or raise ArgumentError, "Must pass bantime option"
|
7
|
+
findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
|
8
|
+
maxretry = options[:maxretry] or raise ArgumentError, "Must pass maxretry option"
|
9
|
+
|
10
|
+
if banned?(discriminator)
|
11
|
+
# Return true for blacklist
|
12
|
+
true
|
13
|
+
elsif yield
|
14
|
+
fail!(discriminator, bantime, findtime, maxretry)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def fail!(discriminator, bantime, findtime, maxretry)
|
20
|
+
count = cache.count("fail2ban:count:#{discriminator}", findtime)
|
21
|
+
if count >= maxretry
|
22
|
+
ban!(discriminator, bantime)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return true for blacklist
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def ban!(discriminator, bantime)
|
30
|
+
cache.write("fail2ban:ban:#{discriminator}", 1, bantime)
|
31
|
+
end
|
32
|
+
|
33
|
+
def banned?(discriminator)
|
34
|
+
cache.read("fail2ban:ban:#{discriminator}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache
|
38
|
+
Rack::Attack.cache
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Attack
|
5
|
+
class StoreProxy
|
6
|
+
def self.build(store)
|
7
|
+
# RedisStore#increment needs different behavior, so detect that
|
8
|
+
# (method has an arity of 2; must call #expire separately
|
9
|
+
if defined?(::ActiveSupport::Cache::RedisStore) && store.is_a?(::ActiveSupport::Cache::RedisStore)
|
10
|
+
# ActiveSupport::Cache::RedisStore doesn't expose any way to set an expiry,
|
11
|
+
# so use the raw Redis::Store instead
|
12
|
+
store = store.instance_variable_get(:@data)
|
13
|
+
end
|
14
|
+
|
15
|
+
if defined?(::Redis::Store) && store.is_a?(::Redis::Store)
|
16
|
+
RedisStoreProxy.new(store)
|
17
|
+
else
|
18
|
+
store
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class RedisStoreProxy < SimpleDelegator
|
23
|
+
def initialize(store)
|
24
|
+
super(store)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(key)
|
28
|
+
self.get(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(key, value, options={})
|
32
|
+
if (expires_in = options[:expires_in])
|
33
|
+
self.setex(key, expires_in, value)
|
34
|
+
else
|
35
|
+
self.set(key, value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def increment(key, amount, options={})
|
40
|
+
count = nil
|
41
|
+
self.pipelined do
|
42
|
+
count = self.incrby(key, amount)
|
43
|
+
self.expire(key, options[:expires_in]) if options[:expires_in]
|
44
|
+
end
|
45
|
+
count.value if count
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/rack/attack/version.rb
CHANGED
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
describe 'Rack::Attack.Fail2Ban' do
|
3
|
+
before do
|
4
|
+
# Use a long findtime; failures due to cache key rotation less likely
|
5
|
+
@cache = Rack::Attack.cache
|
6
|
+
@findtime = 60
|
7
|
+
@bantime = 60
|
8
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
|
+
@f2b_options = {:bantime => @bantime, :findtime => @findtime, :maxretry => 2}
|
10
|
+
Rack::Attack.blacklist('pentest') do |req|
|
11
|
+
Rack::Attack::Fail2Ban.filter(req.ip, @f2b_options){req.query_string =~ /OMGHAX/}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'discriminator has not been banned' do
|
16
|
+
describe 'making ok request' do
|
17
|
+
it 'succeeds' do
|
18
|
+
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
19
|
+
last_response.status.must_equal 200
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'making failing request' do
|
24
|
+
describe 'when not at maxretry' do
|
25
|
+
before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
26
|
+
it 'fails' do
|
27
|
+
last_response.status.must_equal 503
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'increases fail count' do
|
31
|
+
key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
|
32
|
+
@cache.store.read(key).must_equal 1
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is not banned' do
|
36
|
+
key = "rack::attack:fail2ban:1.2.3.4"
|
37
|
+
@cache.store.read(key).must_be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'when at maxretry' do
|
42
|
+
before do
|
43
|
+
# maxretry is 2 - so hit with an extra failed request first
|
44
|
+
get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
45
|
+
get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'fails' do
|
49
|
+
last_response.status.must_equal 503
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'increases fail count' do
|
53
|
+
key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
|
54
|
+
@cache.store.read(key).must_equal 2
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'is banned' do
|
58
|
+
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
59
|
+
@cache.store.read(key).must_equal 1
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'discriminator has been banned' do
|
67
|
+
before do
|
68
|
+
# maxretry is 2 - so hit enough times to get banned
|
69
|
+
get '/?test=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
70
|
+
get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'making request for other discriminator' do
|
74
|
+
it 'succeeds' do
|
75
|
+
get '/', {}, 'REMOTE_ADDR' => '2.2.3.4'
|
76
|
+
last_response.status.must_equal 200
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'making ok request' do
|
81
|
+
before do
|
82
|
+
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'fails' do
|
86
|
+
last_response.status.must_equal 503
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'does not increase fail count' do
|
90
|
+
key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
|
91
|
+
@cache.store.read(key).must_equal 2
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is still banned' do
|
95
|
+
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
96
|
+
@cache.store.read(key).must_equal 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'making failing request' do
|
101
|
+
before do
|
102
|
+
get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'fails' do
|
106
|
+
last_response.status.must_equal 503
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'does not increase fail count' do
|
110
|
+
key = "rack::attack:#{Time.now.to_i/@findtime}:fail2ban:count:1.2.3.4"
|
111
|
+
@cache.store.read(key).must_equal 2
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'is still banned' do
|
115
|
+
key = "rack::attack:fail2ban:ban:1.2.3.4"
|
116
|
+
@cache.store.read(key).must_equal 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -20,6 +20,7 @@ if ENV['TEST_INTEGRATION']
|
|
20
20
|
]
|
21
21
|
|
22
22
|
cache_stores.each do |store|
|
23
|
+
store = Rack::Attack::StoreProxy.build(store)
|
23
24
|
describe "with #{store.class}" do
|
24
25
|
|
25
26
|
before {
|
@@ -51,6 +52,28 @@ if ENV['TEST_INTEGRATION']
|
|
51
52
|
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
52
53
|
end
|
53
54
|
end
|
55
|
+
|
56
|
+
describe "write" do
|
57
|
+
it "should write a value to the store with prefix" do
|
58
|
+
@cache.write("cache-test-key", "foobar", 1)
|
59
|
+
store.read(@key).must_equal "foobar"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "write after expiry" do
|
64
|
+
it "must not have a value" do
|
65
|
+
@cache.write("cache-test-key", "foobar", @expires_in)
|
66
|
+
sleep @expires_in # tick... tick... tick...
|
67
|
+
store.read(@key).must_be :nil?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "read" do
|
72
|
+
it "must read the value with a prefix" do
|
73
|
+
store.write(@key, "foobar", :expires_in => @expires_in)
|
74
|
+
@cache.read("cache-test-key").must_equal "foobar"
|
75
|
+
end
|
76
|
+
end
|
54
77
|
end
|
55
78
|
|
56
79
|
end
|
metadata
CHANGED
@@ -1,83 +1,83 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: minitest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '>='
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rack-test
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - '>='
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - '>='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: activesupport
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - '>='
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: 3.0.0
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 3.0.0
|
83
83
|
- !ruby/object:Gem::Dependency
|
@@ -98,28 +98,28 @@ dependencies:
|
|
98
98
|
name: redis-activesupport
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - '>='
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - '>='
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: dalli
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
|
-
- -
|
115
|
+
- - '>='
|
116
116
|
- !ruby/object:Gem::Version
|
117
117
|
version: '0'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
|
-
- -
|
122
|
+
- - '>='
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
description: A rack middleware for throttling and blocking abusive requests
|
@@ -131,6 +131,8 @@ files:
|
|
131
131
|
- lib/rack/attack/blacklist.rb
|
132
132
|
- lib/rack/attack/cache.rb
|
133
133
|
- lib/rack/attack/check.rb
|
134
|
+
- lib/rack/attack/fail2ban.rb
|
135
|
+
- lib/rack/attack/store_proxy.rb
|
134
136
|
- lib/rack/attack/throttle.rb
|
135
137
|
- lib/rack/attack/track.rb
|
136
138
|
- lib/rack/attack/version.rb
|
@@ -138,6 +140,7 @@ files:
|
|
138
140
|
- lib/rack/attack.rb
|
139
141
|
- Rakefile
|
140
142
|
- README.md
|
143
|
+
- spec/fail2ban_spec.rb
|
141
144
|
- spec/rack_attack_cache_spec.rb
|
142
145
|
- spec/rack_attack_spec.rb
|
143
146
|
- spec/rack_attack_throttle_spec.rb
|
@@ -153,24 +156,24 @@ require_paths:
|
|
153
156
|
- lib
|
154
157
|
required_ruby_version: !ruby/object:Gem::Requirement
|
155
158
|
requirements:
|
156
|
-
- -
|
159
|
+
- - '>='
|
157
160
|
- !ruby/object:Gem::Version
|
158
161
|
version: 1.9.3
|
159
162
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
163
|
requirements:
|
161
|
-
- -
|
164
|
+
- - '>='
|
162
165
|
- !ruby/object:Gem::Version
|
163
166
|
version: '0'
|
164
167
|
requirements: []
|
165
168
|
rubyforge_project:
|
166
|
-
rubygems_version: 2.0.
|
169
|
+
rubygems_version: 2.0.2
|
167
170
|
signing_key:
|
168
171
|
specification_version: 4
|
169
172
|
summary: Block & throttle abusive requests
|
170
173
|
test_files:
|
174
|
+
- spec/fail2ban_spec.rb
|
171
175
|
- spec/rack_attack_cache_spec.rb
|
172
176
|
- spec/rack_attack_spec.rb
|
173
177
|
- spec/rack_attack_throttle_spec.rb
|
174
178
|
- spec/rack_attack_track_spec.rb
|
175
179
|
- spec/spec_helper.rb
|
176
|
-
has_rdoc:
|