rack-attack 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack-attack might be problematic. Click here for more details.

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. 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.
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:Attack.blacklisted_response = lambda do |env|
128
+ Rack::Attack.blacklisted_response = lambda do |env|
112
129
  [ 503, {}, ['Blocked']]
113
130
  end
114
131
 
115
- Rack:Attack.throttled_response = lambda do |env|
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
- [![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)
169
+ [![Build Status](https://travis-ci.org/kickstarter/rack-attack.png?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
170
+ [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](https://codeclimate.com/github/kickstarter/rack-attack)
154
171
 
155
172
  ## License
156
173
 
@@ -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, {}, ['Blocked']] }
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}, ['Retry later']]
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
- return @app.call(env)
54
- end
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
- def clear!
96
+ def clear!
99
97
  @whitelists, @blacklists, @throttles = {}, {}, {}
100
98
  end
101
99
 
@@ -2,17 +2,43 @@ module Rack
2
2
  module Attack
3
3
  class Cache
4
4
 
5
- attr_accessor :store, :prefix
5
+ attr_accessor :prefix
6
+
6
7
  def initialize
7
- @store = ::Rails.cache if defined?(::Rails.cache)
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
- result = store.increment(key, 1, :expires_in => expires_in)
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)
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Attack
3
- VERSION = '2.0.0'
3
+ VERSION = '2.1.0'
4
4
  end
5
5
  end
@@ -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
@@ -1,4 +1,3 @@
1
-
2
1
  require_relative 'spec_helper'
3
2
  describe 'Rack::Attack.throttle' do
4
3
  before do
@@ -2,6 +2,7 @@ require "rubygems"
2
2
  require "bundler/setup"
3
3
 
4
4
  require "minitest/autorun"
5
+ require "minitest/pride"
5
6
  require "rack/test"
6
7
  require 'debugger'
7
8
  require 'active_support'
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.0.0
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-01-11 00:00:00.000000000 Z
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