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 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