rack-attack 2.3.0 → 3.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.
- checksums.yaml +4 -4
- data/README.md +116 -116
- data/Rakefile +11 -2
- data/lib/rack/attack.rb +1 -1
- data/lib/rack/attack/store_proxy.rb +6 -0
- data/lib/rack/attack/version.rb +1 -1
- data/spec/allow2ban_spec.rb +2 -2
- data/spec/fail2ban_spec.rb +4 -4
- data/spec/integration/rack_attack_cache_spec.rb +111 -0
- data/spec/rack_attack_spec.rb +2 -1
- metadata +25 -39
- data/spec/rack_attack_cache_spec.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea9a8662af6ca32da955a04479518cefb8d391f1
|
4
|
+
data.tar.gz: a81aaf8306c6652e177d06ae0c6fd1ecc538e8fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 400df3037af9a335d07edd5968782300711c018ea01890e90a8802dc2c347a347c75b64c13433b81b8bb7345a87399a4765828ed1206c7c7ac4728c0b41fbb49
|
7
|
+
data.tar.gz: 1a37808e78845769b371341e20bd1c3f018d206b36c64e1b2569aa753375b2aa01460324e5a507579bb621b76ed7fd4e957c13c1809a9c7ac6d9c2d5bbecd097
|
data/README.md
CHANGED
@@ -18,28 +18,28 @@ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hac
|
|
18
18
|
Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
|
19
19
|
|
20
20
|
```ruby
|
21
|
-
|
22
|
-
|
21
|
+
# In your Gemfile
|
22
|
+
gem 'rack-attack'
|
23
23
|
```
|
24
24
|
Tell your app to use the Rack::Attack middleware.
|
25
25
|
For Rails 3+ apps:
|
26
26
|
|
27
27
|
```ruby
|
28
|
-
|
29
|
-
|
28
|
+
# In config/application.rb
|
29
|
+
config.middleware.use Rack::Attack
|
30
30
|
```
|
31
31
|
|
32
32
|
Or for Rackup files:
|
33
33
|
|
34
34
|
```ruby
|
35
|
-
|
36
|
-
|
35
|
+
# In config.ru
|
36
|
+
use Rack::Attack
|
37
37
|
```
|
38
38
|
|
39
39
|
Optionally configure the cache store for throttling:
|
40
40
|
|
41
41
|
```ruby
|
42
|
-
|
42
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
|
43
43
|
```
|
44
44
|
|
45
45
|
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).
|
@@ -56,20 +56,20 @@ The Rack::Attack middleware compares each request against *whitelists*, *blackli
|
|
56
56
|
The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
|
57
57
|
|
58
58
|
```ruby
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
59
|
+
def call(env)
|
60
|
+
req = Rack::Request.new(env)
|
61
|
+
|
62
|
+
if whitelisted?(req)
|
63
|
+
@app.call(env)
|
64
|
+
elsif blacklisted?(req)
|
65
|
+
blacklisted_response[env]
|
66
|
+
elsif throttled?(req)
|
67
|
+
throttled_response[env]
|
68
|
+
else
|
69
|
+
tracked?(req)
|
70
|
+
@app.call(env)
|
71
|
+
end
|
72
|
+
end
|
73
73
|
```
|
74
74
|
|
75
75
|
## About Tracks
|
@@ -85,27 +85,27 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
|
|
85
85
|
### Whitelists
|
86
86
|
|
87
87
|
```ruby
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
88
|
+
# Always allow requests from localhost
|
89
|
+
# (blacklist & throttles are skipped)
|
90
|
+
Rack::Attack.whitelist('allow from localhost') do |req|
|
91
|
+
# Requests are allowed if the return value is truthy
|
92
|
+
'127.0.0.1' == req.ip
|
93
|
+
end
|
94
94
|
```
|
95
95
|
|
96
96
|
### Blacklists
|
97
97
|
|
98
98
|
```ruby
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
99
|
+
# Block requests from 1.2.3.4
|
100
|
+
Rack::Attack.blacklist('block 1.2.3.4') do |req|
|
101
|
+
# Request are blocked if the return value is truthy
|
102
|
+
'1.2.3.4' == req.ip
|
103
|
+
end
|
104
|
+
|
105
|
+
# Block logins from a bad user agent
|
106
|
+
Rack::Attack.blacklist('block bad UA logins') do |req|
|
107
|
+
req.path == '/login' && req.post? && req.user_agent == 'BadUA'
|
108
|
+
end
|
109
109
|
```
|
110
110
|
|
111
111
|
#### Fail2Ban
|
@@ -116,79 +116,79 @@ See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0
|
|
116
116
|
how the parameters work.
|
117
117
|
|
118
118
|
```ruby
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
119
|
+
# Block requests containing '/etc/password' in the params.
|
120
|
+
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
|
121
|
+
Rack::Attack.blacklist('fail2ban pentesters') do |req|
|
122
|
+
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
|
123
|
+
# so the request is blocked
|
124
|
+
Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
|
125
|
+
# The count for the IP is incremented if the return value is truthy.
|
126
|
+
CGI.unescape(req.query_string) =~ %r{/etc/passwd}
|
127
|
+
end
|
128
|
+
end
|
129
129
|
```
|
130
130
|
|
131
131
|
#### Allow2Ban
|
132
132
|
`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
|
133
133
|
clients until such time as they reach maxretry at which they are cut off as per normal.
|
134
134
|
```ruby
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
135
|
+
# Lockout IP addresses that are hammering your login page.
|
136
|
+
# After 20 requests in 1 minute, block all requests from that IP for 1 hour.
|
137
|
+
Rack::Attack.blacklist('allow2ban login scrapers') do |req|
|
138
|
+
# `filter` returns false value if request is to your login page (but still
|
139
|
+
# increments the count) so request below the limit are not blocked until
|
140
|
+
# they hit the limit. At that point, filter will return true and block.
|
141
|
+
Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do
|
142
|
+
# The count for the IP is incremented if the return value is truthy.
|
143
|
+
req.path == '/login' and req.post?
|
144
|
+
end
|
145
|
+
end
|
146
146
|
```
|
147
147
|
|
148
148
|
|
149
149
|
### Throttles
|
150
150
|
|
151
151
|
```ruby
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
152
|
+
# Throttle requests to 5 requests per second per ip
|
153
|
+
Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
|
154
|
+
# If the return value is truthy, the cache key for the return value
|
155
|
+
# is incremented and compared with the limit. In this case:
|
156
|
+
# "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
|
157
|
+
#
|
158
|
+
# If falsy, the cache key is neither incremented nor checked.
|
159
|
+
|
160
|
+
req.ip
|
161
|
+
end
|
162
|
+
|
163
|
+
# Throttle login attempts for a given email parameter to 6 reqs/minute
|
164
|
+
# Return the email as a discriminator on POST /login requests
|
165
|
+
Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
|
166
|
+
req.params['email'] if req.path == '/login' && req.post?
|
167
|
+
end
|
168
|
+
|
169
|
+
# You can also set a limit using a proc instead of a number. For
|
170
|
+
# instance, after Rack::Auth::Basic has authenticated the user:
|
171
|
+
limit_based_on_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
|
172
|
+
Rack::Attack.throttle('req/ip', :limit => limit_based_on_proc, :period => 1.second) do |req|
|
173
|
+
req.ip
|
174
|
+
end
|
175
175
|
```
|
176
176
|
|
177
177
|
### Tracks
|
178
178
|
|
179
179
|
```ruby
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
180
|
+
# Track requests from a special user agent
|
181
|
+
Rack::Attack.track("special_agent") do |req|
|
182
|
+
req.user_agent == "SpecialAgent"
|
183
|
+
end
|
184
|
+
|
185
|
+
# Track it using ActiveSupport::Notification
|
186
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req|
|
187
|
+
if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track
|
188
|
+
Rails.logger.info "special_agent: #{req.path}"
|
189
|
+
STATSD.increment("special_agent")
|
190
|
+
end
|
191
|
+
end
|
192
192
|
```
|
193
193
|
|
194
194
|
## Responses
|
@@ -196,30 +196,30 @@ clients until such time as they reach maxretry at which they are cut off as per
|
|
196
196
|
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).
|
197
197
|
|
198
198
|
```ruby
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
199
|
+
Rack::Attack.blacklisted_response = lambda do |env|
|
200
|
+
# Using 503 because it may make attacker think that they have successfully
|
201
|
+
# DOSed the site. Rack::Attack returns 403 for blacklists by default
|
202
|
+
[ 503, {}, ['Blocked']]
|
203
|
+
end
|
204
|
+
|
205
|
+
Rack::Attack.throttled_response = lambda do |env|
|
206
|
+
# name and other data about the matched throttle
|
207
|
+
body = [
|
208
|
+
env['rack.attack.matched'],
|
209
|
+
env['rack.attack.match_type'],
|
210
|
+
env['rack.attack.match_data']
|
211
|
+
].inspect
|
212
|
+
|
213
|
+
# Using 503 because it may make attacker think that they have successfully
|
214
|
+
# DOSed the site. Rack::Attack returns 429 for throttling by default
|
215
|
+
[ 503, {}, [body]]
|
216
|
+
end
|
217
217
|
```
|
218
218
|
|
219
219
|
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
|
220
220
|
|
221
221
|
```ruby
|
222
|
-
|
222
|
+
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
|
223
223
|
```
|
224
224
|
|
225
225
|
## Logging & Instrumentation
|
@@ -229,9 +229,9 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
|
|
229
229
|
You can subscribe to 'rack.attack' events and log it, graph it, etc:
|
230
230
|
|
231
231
|
```ruby
|
232
|
-
|
233
|
-
|
234
|
-
|
232
|
+
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
|
233
|
+
puts req.inspect
|
234
|
+
end
|
235
235
|
```
|
236
236
|
|
237
237
|
## Testing
|
@@ -250,7 +250,7 @@ so try to keep the number of throttle checks per request low.
|
|
250
250
|
If a request is blacklisted or throttled, the response is a very simple Rack response.
|
251
251
|
A single typical ruby web server thread can block several hundred requests per second.
|
252
252
|
|
253
|
-
Rack::Attack complements tools like `iptables` and nginx's [
|
253
|
+
Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
|
254
254
|
|
255
255
|
## Motivation
|
256
256
|
|
data/Rakefile
CHANGED
@@ -2,8 +2,17 @@ require "rubygems"
|
|
2
2
|
require "bundler/setup"
|
3
3
|
require 'rake/testtask'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
namespace :test do
|
6
|
+
Rake::TestTask.new(:units) do |t|
|
7
|
+
t.pattern = "spec/*_spec.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
Rake::TestTask.new(:integration) do |t|
|
11
|
+
t.pattern = "spec/integration/*_spec.rb"
|
12
|
+
end
|
7
13
|
end
|
8
14
|
|
15
|
+
desc 'Run tests'
|
16
|
+
task :test => %w[test:units test:integration]
|
17
|
+
|
9
18
|
task :default => :test
|
data/lib/rack/attack.rb
CHANGED
@@ -40,7 +40,7 @@ module Rack::Attack
|
|
40
40
|
|
41
41
|
# Set defaults
|
42
42
|
@notifier ||= ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
43
|
-
@blacklisted_response ||= lambda {|env| [
|
43
|
+
@blacklisted_response ||= lambda {|env| [403, {}, ["Forbidden\n"]] }
|
44
44
|
@throttled_response ||= lambda {|env|
|
45
45
|
retry_after = env['rack.attack.match_data'][:period] rescue nil
|
46
46
|
[429, {'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
|
@@ -26,6 +26,8 @@ module Rack
|
|
26
26
|
|
27
27
|
def read(key)
|
28
28
|
self.get(key)
|
29
|
+
rescue Redis::BaseError
|
30
|
+
nil
|
29
31
|
end
|
30
32
|
|
31
33
|
def write(key, value, options={})
|
@@ -34,6 +36,8 @@ module Rack
|
|
34
36
|
else
|
35
37
|
self.set(key, value)
|
36
38
|
end
|
39
|
+
rescue Redis::BaseError
|
40
|
+
nil
|
37
41
|
end
|
38
42
|
|
39
43
|
def increment(key, amount, options={})
|
@@ -43,6 +47,8 @@ module Rack
|
|
43
47
|
self.expire(key, options[:expires_in]) if options[:expires_in]
|
44
48
|
end
|
45
49
|
count.value if count
|
50
|
+
rescue Redis::BaseError
|
51
|
+
nil
|
46
52
|
end
|
47
53
|
|
48
54
|
end
|
data/lib/rack/attack/version.rb
CHANGED
data/spec/allow2ban_spec.rb
CHANGED
@@ -83,7 +83,7 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
83
83
|
end
|
84
84
|
|
85
85
|
it 'fails' do
|
86
|
-
last_response.status.must_equal
|
86
|
+
last_response.status.must_equal 403
|
87
87
|
end
|
88
88
|
|
89
89
|
it 'does not increase fail count' do
|
@@ -103,7 +103,7 @@ describe 'Rack::Attack.Allow2Ban' do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
it 'fails' do
|
106
|
-
last_response.status.must_equal
|
106
|
+
last_response.status.must_equal 403
|
107
107
|
end
|
108
108
|
|
109
109
|
it 'does not increase fail count' do
|
data/spec/fail2ban_spec.rb
CHANGED
@@ -24,7 +24,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
24
24
|
describe 'when not at maxretry' do
|
25
25
|
before { get '/?foo=OMGHAX', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
26
26
|
it 'fails' do
|
27
|
-
last_response.status.must_equal
|
27
|
+
last_response.status.must_equal 403
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'increases fail count' do
|
@@ -46,7 +46,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'fails' do
|
49
|
-
last_response.status.must_equal
|
49
|
+
last_response.status.must_equal 403
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'increases fail count' do
|
@@ -83,7 +83,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
83
83
|
end
|
84
84
|
|
85
85
|
it 'fails' do
|
86
|
-
last_response.status.must_equal
|
86
|
+
last_response.status.must_equal 403
|
87
87
|
end
|
88
88
|
|
89
89
|
it 'does not increase fail count' do
|
@@ -103,7 +103,7 @@ describe 'Rack::Attack.Fail2Ban' do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
it 'fails' do
|
106
|
-
last_response.status.must_equal
|
106
|
+
last_response.status.must_equal 403
|
107
107
|
end
|
108
108
|
|
109
109
|
it 'does not increase fail count' do
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Attack::Cache do
|
4
|
+
def delete(key)
|
5
|
+
if @cache.store.respond_to?(:delete)
|
6
|
+
@cache.store.delete(key)
|
7
|
+
else
|
8
|
+
@cache.store.del(key)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def sleep_until_expired
|
13
|
+
sleep(@expires_in * 1.1) # Add 10% to reduce errors
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'active_support/cache/dalli_store'
|
17
|
+
require 'active_support/cache/redis_store'
|
18
|
+
cache_stores = [
|
19
|
+
ActiveSupport::Cache::MemoryStore.new,
|
20
|
+
ActiveSupport::Cache::DalliStore.new("localhost"),
|
21
|
+
ActiveSupport::Cache::RedisStore.new("localhost"),
|
22
|
+
Redis::Store.new
|
23
|
+
]
|
24
|
+
|
25
|
+
cache_stores.each do |store|
|
26
|
+
store = Rack::Attack::StoreProxy.build(store)
|
27
|
+
describe "with #{store.class}" do
|
28
|
+
|
29
|
+
before {
|
30
|
+
@cache ||= Rack::Attack::Cache.new
|
31
|
+
@key = "rack::attack:cache-test-key"
|
32
|
+
@expires_in = 1
|
33
|
+
@cache.store = store
|
34
|
+
delete(@key)
|
35
|
+
}
|
36
|
+
|
37
|
+
after { delete(@key) }
|
38
|
+
|
39
|
+
describe "do_count once" do
|
40
|
+
it "should be 1" do
|
41
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "do_count twice" do
|
46
|
+
it "must be 2" do
|
47
|
+
@cache.send(:do_count, @key, @expires_in)
|
48
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 2
|
49
|
+
end
|
50
|
+
end
|
51
|
+
describe "do_count after expires_in" do
|
52
|
+
it "must be 1" do
|
53
|
+
@cache.send(:do_count, @key, @expires_in)
|
54
|
+
sleep_until_expired
|
55
|
+
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "write" do
|
60
|
+
it "should write a value to the store with prefix" do
|
61
|
+
@cache.write("cache-test-key", "foobar", 1)
|
62
|
+
store.read(@key).must_equal "foobar"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "write after expiry" do
|
67
|
+
it "must not have a value" do
|
68
|
+
@cache.write("cache-test-key", "foobar", @expires_in)
|
69
|
+
sleep_until_expired
|
70
|
+
store.read(@key).must_be :nil?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "read" do
|
75
|
+
it "must read the value with a prefix" do
|
76
|
+
store.write(@key, "foobar", :expires_in => @expires_in)
|
77
|
+
@cache.read("cache-test-key").must_equal "foobar"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "should not error if redis is not running" do
|
85
|
+
before {
|
86
|
+
@cache = Rack::Attack::Cache.new
|
87
|
+
@key = "rack::attack:cache-test-key"
|
88
|
+
@expires_in = 1
|
89
|
+
# Use presumably unused port for Redis client
|
90
|
+
@cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333)
|
91
|
+
}
|
92
|
+
describe "write" do
|
93
|
+
it "should not raise exception" do
|
94
|
+
@cache.write("cache-test-key", "foobar", 1)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "read" do
|
99
|
+
it "should not raise exception" do
|
100
|
+
@cache.read("cache-test-key")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "do_count" do
|
105
|
+
it "should not raise exception" do
|
106
|
+
@cache.send(:do_count, @key, @expires_in)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -15,7 +15,8 @@ describe 'Rack::Attack' do
|
|
15
15
|
before { get '/', {}, 'REMOTE_ADDR' => @bad_ip }
|
16
16
|
it "should return a blacklist response" do
|
17
17
|
get '/', {}, 'REMOTE_ADDR' => @bad_ip
|
18
|
-
last_response.status.must_equal
|
18
|
+
last_response.status.must_equal 403
|
19
|
+
last_response.body.must_equal "Forbidden\n"
|
19
20
|
end
|
20
21
|
it "should tag the env" do
|
21
22
|
last_request.env['rack.attack.matched'].must_equal "ip #{@bad_ip}"
|
metadata
CHANGED
@@ -1,125 +1,111 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.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:
|
11
|
+
date: 2014-03-15 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
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: debugger
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ~>
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '1.5'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ~>
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '1.5'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: redis-activesupport
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
|
-
- -
|
87
|
+
- - ">="
|
102
88
|
- !ruby/object:Gem::Version
|
103
89
|
version: '0'
|
104
90
|
type: :development
|
105
91
|
prerelease: false
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
107
93
|
requirements:
|
108
|
-
- -
|
94
|
+
- - ">="
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: '0'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: dalli
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
|
-
- -
|
101
|
+
- - ">="
|
116
102
|
- !ruby/object:Gem::Version
|
117
103
|
version: '0'
|
118
104
|
type: :development
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
107
|
requirements:
|
122
|
-
- -
|
108
|
+
- - ">="
|
123
109
|
- !ruby/object:Gem::Version
|
124
110
|
version: '0'
|
125
111
|
description: A rack middleware for throttling and blocking abusive requests
|
@@ -128,6 +114,9 @@ executables: []
|
|
128
114
|
extensions: []
|
129
115
|
extra_rdoc_files: []
|
130
116
|
files:
|
117
|
+
- README.md
|
118
|
+
- Rakefile
|
119
|
+
- lib/rack/attack.rb
|
131
120
|
- lib/rack/attack/allow2ban.rb
|
132
121
|
- lib/rack/attack/blacklist.rb
|
133
122
|
- lib/rack/attack/cache.rb
|
@@ -138,12 +127,9 @@ files:
|
|
138
127
|
- lib/rack/attack/track.rb
|
139
128
|
- lib/rack/attack/version.rb
|
140
129
|
- lib/rack/attack/whitelist.rb
|
141
|
-
- lib/rack/attack.rb
|
142
|
-
- Rakefile
|
143
|
-
- README.md
|
144
130
|
- spec/allow2ban_spec.rb
|
145
131
|
- spec/fail2ban_spec.rb
|
146
|
-
- spec/rack_attack_cache_spec.rb
|
132
|
+
- spec/integration/rack_attack_cache_spec.rb
|
147
133
|
- spec/rack_attack_spec.rb
|
148
134
|
- spec/rack_attack_throttle_spec.rb
|
149
135
|
- spec/rack_attack_track_spec.rb
|
@@ -154,29 +140,29 @@ licenses:
|
|
154
140
|
metadata: {}
|
155
141
|
post_install_message:
|
156
142
|
rdoc_options:
|
157
|
-
- --charset=UTF-8
|
143
|
+
- "--charset=UTF-8"
|
158
144
|
require_paths:
|
159
145
|
- lib
|
160
146
|
required_ruby_version: !ruby/object:Gem::Requirement
|
161
147
|
requirements:
|
162
|
-
- -
|
148
|
+
- - ">="
|
163
149
|
- !ruby/object:Gem::Version
|
164
150
|
version: 1.9.2
|
165
151
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
152
|
requirements:
|
167
|
-
- -
|
153
|
+
- - ">="
|
168
154
|
- !ruby/object:Gem::Version
|
169
155
|
version: '0'
|
170
156
|
requirements: []
|
171
157
|
rubyforge_project:
|
172
|
-
rubygems_version: 2.
|
158
|
+
rubygems_version: 2.2.2
|
173
159
|
signing_key:
|
174
160
|
specification_version: 4
|
175
161
|
summary: Block & throttle abusive requests
|
176
162
|
test_files:
|
177
163
|
- spec/allow2ban_spec.rb
|
178
164
|
- spec/fail2ban_spec.rb
|
179
|
-
- spec/rack_attack_cache_spec.rb
|
165
|
+
- spec/integration/rack_attack_cache_spec.rb
|
180
166
|
- spec/rack_attack_spec.rb
|
181
167
|
- spec/rack_attack_throttle_spec.rb
|
182
168
|
- spec/rack_attack_track_spec.rb
|
@@ -1,84 +0,0 @@
|
|
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
|
-
store = Rack::Attack::StoreProxy.build(store)
|
24
|
-
describe "with #{store.class}" do
|
25
|
-
|
26
|
-
before {
|
27
|
-
@cache ||= Rack::Attack::Cache.new
|
28
|
-
@key = "rack::attack:cache-test-key"
|
29
|
-
@expires_in = 1
|
30
|
-
@cache.store = store
|
31
|
-
delete(@key)
|
32
|
-
}
|
33
|
-
|
34
|
-
after { delete(@key) }
|
35
|
-
|
36
|
-
describe "do_count once" do
|
37
|
-
it "should be 1" do
|
38
|
-
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
describe "do_count twice" do
|
43
|
-
it "must be 2" do
|
44
|
-
@cache.send(:do_count, @key, @expires_in)
|
45
|
-
@cache.send(:do_count, @key, @expires_in).must_equal 2
|
46
|
-
end
|
47
|
-
end
|
48
|
-
describe "do_count after expires_in" do
|
49
|
-
it "must be 1" do
|
50
|
-
@cache.send(:do_count, @key, @expires_in)
|
51
|
-
sleep @expires_in # sigh
|
52
|
-
@cache.send(:do_count, @key, @expires_in).must_equal 1
|
53
|
-
end
|
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
|
77
|
-
end
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
|
-
end
|
82
|
-
else
|
83
|
-
puts 'Skipping cache store integration tests (set ENV["TEST_INTEGRATION"] to enable)'
|
84
|
-
end
|