rack-attack 3.0.0 → 4.0.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea9a8662af6ca32da955a04479518cefb8d391f1
4
- data.tar.gz: a81aaf8306c6652e177d06ae0c6fd1ecc538e8fb
3
+ metadata.gz: ff9b10f54b7093a546fea83332521abbd15b50fc
4
+ data.tar.gz: 766a4136c3895f45c09288e9fc97fe4a5e8e596c
5
5
  SHA512:
6
- metadata.gz: 400df3037af9a335d07edd5968782300711c018ea01890e90a8802dc2c347a347c75b64c13433b81b8bb7345a87399a4765828ed1206c7c7ac4728c0b41fbb49
7
- data.tar.gz: 1a37808e78845769b371341e20bd1c3f018d206b36c64e1b2569aa753375b2aa01460324e5a507579bb621b76ed7fd4e957c13c1809a9c7ac6d9c2d5bbecd097
6
+ metadata.gz: d5891f076087208013f1df9942977692aec45692ac6ec6fea1d143731033bebeed08d955a977ac7322aed8c9390383557214891ea61344351b1d1ec04c5f897e
7
+ data.tar.gz: d6e5098b06db042fb7e863b7d34668d4f5bff12812d7ee1d7493455ca8546606605431ebd403041442ee82f47509ad8c2365a2a2bcd130706c3a328a74dc94ed
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Rack::Attack!!!
2
- *A DSL for blocking & throttling abusive clients*
2
+ *Rack middleware for blocking & throttling abusive requests*
3
3
 
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.
@@ -13,7 +13,7 @@ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hac
13
13
  [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](https://codeclimate.com/github/kickstarter/rack-attack)
14
14
 
15
15
 
16
- ## Installation
16
+ ## Getting started
17
17
 
18
18
  Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
19
19
 
@@ -36,6 +36,17 @@ Or for Rackup files:
36
36
  use Rack::Attack
37
37
  ```
38
38
 
39
+ Add a `rack-attack.rb` file to `config/initalizers/`:
40
+ ```ruby
41
+ # In config/initializers/rack-attack.rb
42
+ module Rack::Attack
43
+ # your custom configuration...
44
+ end
45
+ ```
46
+
47
+ *Tip:* The example in the wiki is a great way to get started:
48
+ [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
49
+
39
50
  Optionally configure the cache store for throttling:
40
51
 
41
52
  ```ruby
@@ -271,6 +282,6 @@ New releases of Rack::Attack are announced on
271
282
 
272
283
  ## License
273
284
 
274
- Copyright (c) 2012 Kickstarter, Inc
285
+ Copyright Kickstarter, Inc.
275
286
 
276
- Released under an [MIT License](http://opensource.org/licenses/MIT)
287
+ Released under an [MIT License](http://opensource.org/licenses/MIT).
@@ -1,14 +1,19 @@
1
1
  require 'rack'
2
- module Rack::Attack
3
- autoload :Cache, 'rack/attack/cache'
4
- autoload :Check, 'rack/attack/check'
5
- autoload :Throttle, 'rack/attack/throttle'
6
- autoload :Whitelist, 'rack/attack/whitelist'
7
- autoload :Blacklist, 'rack/attack/blacklist'
8
- autoload :Track, 'rack/attack/track'
9
- autoload :StoreProxy,'rack/attack/store_proxy'
10
- autoload :Fail2Ban, 'rack/attack/fail2ban'
11
- autoload :Allow2Ban, 'rack/attack/allow2ban'
2
+ require 'forwardable'
3
+
4
+ class Rack::Attack
5
+ autoload :Cache, 'rack/attack/cache'
6
+ autoload :Check, 'rack/attack/check'
7
+ autoload :Throttle, 'rack/attack/throttle'
8
+ autoload :Whitelist, 'rack/attack/whitelist'
9
+ autoload :Blacklist, 'rack/attack/blacklist'
10
+ autoload :Track, 'rack/attack/track'
11
+ autoload :StoreProxy, 'rack/attack/store_proxy'
12
+ autoload :DalliProxy, 'rack/attack/store_proxy/dalli_proxy'
13
+ autoload :RedisStoreProxy, 'rack/attack/store_proxy/redis_store_proxy'
14
+ autoload :Fail2Ban, 'rack/attack/fail2ban'
15
+ autoload :Allow2Ban, 'rack/attack/allow2ban'
16
+ autoload :Request, 'rack/attack/request'
12
17
 
13
18
  class << self
14
19
 
@@ -35,35 +40,6 @@ module Rack::Attack
35
40
  def throttles; @throttles ||= {}; end
36
41
  def tracks; @tracks ||= {}; end
37
42
 
38
- def new(app)
39
- @app = app
40
-
41
- # Set defaults
42
- @notifier ||= ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
43
- @blacklisted_response ||= lambda {|env| [403, {}, ["Forbidden\n"]] }
44
- @throttled_response ||= lambda {|env|
45
- retry_after = env['rack.attack.match_data'][:period] rescue nil
46
- [429, {'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
47
- }
48
-
49
- self
50
- end
51
-
52
- def call(env)
53
- req = Rack::Request.new(env)
54
-
55
- if whitelisted?(req)
56
- @app.call(env)
57
- elsif blacklisted?(req)
58
- blacklisted_response[env]
59
- elsif throttled?(req)
60
- throttled_response[env]
61
- else
62
- tracked?(req)
63
- @app.call(env)
64
- end
65
- end
66
-
67
43
  def whitelisted?(req)
68
44
  whitelists.any? do |name, whitelist|
69
45
  whitelist[req]
@@ -101,4 +77,37 @@ module Rack::Attack
101
77
  end
102
78
 
103
79
  end
80
+
81
+ # Set defaults
82
+ @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
83
+ @blacklisted_response = lambda {|env| [403, {}, ["Forbidden\n"]] }
84
+ @throttled_response = lambda {|env|
85
+ retry_after = env['rack.attack.match_data'][:period] rescue nil
86
+ [429, {'Retry-After' => retry_after.to_s}, ["Retry later\n"]]
87
+ }
88
+
89
+ def initialize(app)
90
+ @app = app
91
+ end
92
+
93
+ def call(env)
94
+ req = Rack::Attack::Request.new(env)
95
+
96
+ if whitelisted?(req)
97
+ @app.call(env)
98
+ elsif blacklisted?(req)
99
+ self.class.blacklisted_response[env]
100
+ elsif throttled?(req)
101
+ self.class.throttled_response[env]
102
+ else
103
+ tracked?(req)
104
+ @app.call(env)
105
+ end
106
+ end
107
+
108
+ extend Forwardable
109
+ def_delegators self, :whitelisted?,
110
+ :blacklisted?,
111
+ :throttled?,
112
+ :tracked?
104
113
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Allow2Ban < Fail2Ban
4
4
  class << self
5
5
  protected
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Blacklist < Check
4
4
  def initialize(name, block)
5
5
  super
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Cache
4
4
 
5
5
  attr_accessor :prefix
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Check
4
4
  attr_reader :name, :block, :type
5
5
  def initialize(name, block)
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Fail2Ban
4
4
  class << self
5
5
  def filter(discriminator, options)
@@ -0,0 +1,6 @@
1
+ module Rack
2
+ class Attack
3
+ class Request < ::Rack::Request
4
+ end
5
+ end
6
+ end
@@ -1,8 +1,8 @@
1
- require 'delegate'
2
-
3
1
  module Rack
4
- module Attack
5
- class StoreProxy
2
+ class Attack
3
+ module StoreProxy
4
+ PROXIES = [DalliProxy, RedisStoreProxy]
5
+
6
6
  def self.build(store)
7
7
  # RedisStore#increment needs different behavior, so detect that
8
8
  # (method has an arity of 2; must call #expire separately
@@ -12,46 +12,11 @@ module Rack
12
12
  store = store.instance_variable_get(:@data)
13
13
  end
14
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
- rescue Redis::BaseError
30
- nil
31
- end
32
-
33
- def write(key, value, options={})
34
- if (expires_in = options[:expires_in])
35
- self.setex(key, expires_in, value)
36
- else
37
- self.set(key, value)
38
- end
39
- rescue Redis::BaseError
40
- nil
41
- end
42
-
43
- def increment(key, amount, options={})
44
- count = nil
45
- self.pipelined do
46
- count = self.incrby(key, amount)
47
- self.expire(key, options[:expires_in]) if options[:expires_in]
48
- end
49
- count.value if count
50
- rescue Redis::BaseError
51
- nil
52
- end
15
+ klass = PROXIES.find { |proxy| proxy.handle?(store) }
53
16
 
17
+ klass ? klass.new(store) : store
54
18
  end
19
+
55
20
  end
56
21
  end
57
22
  end
@@ -0,0 +1,65 @@
1
+ require 'delegate'
2
+
3
+ module Rack
4
+ class Attack
5
+ module StoreProxy
6
+ class DalliProxy < SimpleDelegator
7
+ def self.handle?(store)
8
+ return false unless defined?(::Dalli)
9
+
10
+ # Consider extracting to a separate Connection Pool proxy to reduce
11
+ # code here and handle clients other than Dalli.
12
+ if defined?(::ConnectionPool) && store.is_a?(::ConnectionPool)
13
+ store.with { |conn| conn.is_a?(::Dalli::Client) }
14
+ else
15
+ store.is_a?(::Dalli::Client)
16
+ end
17
+ end
18
+
19
+ def initialize(client)
20
+ super(client)
21
+ stub_with_if_missing
22
+ end
23
+
24
+ def read(key)
25
+ with do |client|
26
+ client.get(key)
27
+ end
28
+ rescue Dalli::DalliError
29
+ end
30
+
31
+ def write(key, value, options={})
32
+ with do |client|
33
+ client.set(key, value, options.fetch(:expires_in, 0), raw: true)
34
+ end
35
+ rescue Dalli::DalliError
36
+ end
37
+
38
+ def increment(key, amount, options={})
39
+ with do |client|
40
+ client.incr(key, amount, options.fetch(:expires_in, 0), amount)
41
+ end
42
+ rescue Dalli::DalliError
43
+ end
44
+
45
+ def delete(key)
46
+ with do |client|
47
+ client.delete(key)
48
+ end
49
+ rescue Dalli::DalliError
50
+ end
51
+
52
+ private
53
+
54
+ def stub_with_if_missing
55
+ unless __getobj__.respond_to?(:with)
56
+ class << self
57
+ def with; yield __getobj__; end
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ require 'delegate'
2
+
3
+ module Rack
4
+ class Attack
5
+ module StoreProxy
6
+ class RedisStoreProxy < SimpleDelegator
7
+ def self.handle?(store)
8
+ defined?(::Redis::Store) && store.is_a?(::Redis::Store)
9
+ end
10
+
11
+ def initialize(store)
12
+ super(store)
13
+ end
14
+
15
+ def read(key)
16
+ self.get(key)
17
+ rescue Redis::BaseError
18
+ end
19
+
20
+ def write(key, value, options={})
21
+ if (expires_in = options[:expires_in])
22
+ self.setex(key, expires_in, value)
23
+ else
24
+ self.set(key, value)
25
+ end
26
+ rescue Redis::BaseError
27
+ end
28
+
29
+ def increment(key, amount, options={})
30
+ count = nil
31
+ self.pipelined do
32
+ count = self.incrby(key, amount)
33
+ self.expire(key, options[:expires_in]) if options[:expires_in]
34
+ end
35
+ count.value if count
36
+ rescue Redis::BaseError
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Throttle
4
4
  MANDATORY_OPTIONS = [:limit, :period]
5
5
  attr_reader :name, :limit, :period, :block
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Track < Check
4
4
  def initialize(name, block)
5
5
  super
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
3
- VERSION = '3.0.0'
2
+ class Attack
3
+ VERSION = '4.0.0'
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Attack
2
+ class Attack
3
3
  class Whitelist < Check
4
4
  def initialize(name, block)
5
5
  super
@@ -0,0 +1,47 @@
1
+ require 'active_support/cache'
2
+ require 'active_support/cache/redis_store'
3
+ require 'dalli'
4
+ require_relative '../spec_helper'
5
+
6
+ OfflineExamples = Minitest::SharedExamples.new do
7
+
8
+ it 'should write' do
9
+ @cache.write('cache-test-key', 'foobar', 1)
10
+ end
11
+
12
+ it 'should read' do
13
+ @cache.read('cache-test-key')
14
+ end
15
+
16
+ it 'should count' do
17
+ @cache.send(:do_count, 'rack::attack::cache-test-key', 1)
18
+ end
19
+
20
+ end
21
+
22
+ describe 'when Redis is offline' do
23
+ include OfflineExamples
24
+
25
+ before {
26
+ @cache = Rack::Attack::Cache.new
27
+ # Use presumably unused port for Redis client
28
+ @cache.store = ActiveSupport::Cache::RedisStore.new(:host => '127.0.0.1', :port => 3333)
29
+ }
30
+
31
+ end
32
+
33
+ describe 'when Memcached is offline' do
34
+ include OfflineExamples
35
+
36
+ before {
37
+ Dalli.logger.level = Logger::FATAL
38
+
39
+ @cache = Rack::Attack::Cache.new
40
+ @cache.store = Dalli::Client.new('127.0.0.1:22122')
41
+ }
42
+
43
+ after {
44
+ Dalli.logger.level = Logger::INFO
45
+ }
46
+
47
+ end
@@ -15,10 +15,13 @@ describe Rack::Attack::Cache do
15
15
 
16
16
  require 'active_support/cache/dalli_store'
17
17
  require 'active_support/cache/redis_store'
18
+ require 'connection_pool'
18
19
  cache_stores = [
19
20
  ActiveSupport::Cache::MemoryStore.new,
20
- ActiveSupport::Cache::DalliStore.new("localhost"),
21
- ActiveSupport::Cache::RedisStore.new("localhost"),
21
+ ActiveSupport::Cache::DalliStore.new("127.0.0.1"),
22
+ ActiveSupport::Cache::RedisStore.new("127.0.0.1"),
23
+ Dalli::Client.new,
24
+ ConnectionPool.new { Dalli::Client.new },
22
25
  Redis::Store.new
23
26
  ]
24
27
 
@@ -27,7 +30,7 @@ describe Rack::Attack::Cache do
27
30
  describe "with #{store.class}" do
28
31
 
29
32
  before {
30
- @cache ||= Rack::Attack::Cache.new
33
+ @cache = Rack::Attack::Cache.new
31
34
  @key = "rack::attack:cache-test-key"
32
35
  @expires_in = 1
33
36
  @cache.store = store
@@ -80,32 +83,4 @@ describe Rack::Attack::Cache do
80
83
  end
81
84
 
82
85
  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
86
  end
@@ -0,0 +1,10 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Rack::Attack::StoreProxy::DalliProxy do
4
+
5
+ it 'should stub Dalli::Client#with on older clients' do
6
+ proxy = Rack::Attack::StoreProxy::DalliProxy.new(Class.new)
7
+ proxy.with {} # will not raise an error
8
+ end
9
+
10
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'Rack::Attack' do
4
+ describe 'helpers' do
5
+ before do
6
+ class Rack::Attack::Request
7
+ def remote_ip
8
+ ip
9
+ end
10
+ end
11
+
12
+ Rack::Attack.whitelist('valid IP') do |req|
13
+ req.remote_ip == "127.0.0.1"
14
+ end
15
+ end
16
+
17
+ allow_ok_requests
18
+ end
19
+ end
@@ -34,3 +34,7 @@ class MiniTest::Spec
34
34
  end
35
35
  end
36
36
  end
37
+
38
+ class Minitest::SharedExamples < Module
39
+ include Minitest::Spec::DSL
40
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-attack
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.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: 2014-03-15 00:00:00.000000000 Z
11
+ date: 2014-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: appraisal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: activesupport
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,20 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: connection_pool
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
111
139
  description: A rack middleware for throttling and blocking abusive requests
112
140
  email: aaron@ktheory.com
113
141
  executables: []
@@ -122,14 +150,20 @@ files:
122
150
  - lib/rack/attack/cache.rb
123
151
  - lib/rack/attack/check.rb
124
152
  - lib/rack/attack/fail2ban.rb
153
+ - lib/rack/attack/request.rb
125
154
  - lib/rack/attack/store_proxy.rb
155
+ - lib/rack/attack/store_proxy/dalli_proxy.rb
156
+ - lib/rack/attack/store_proxy/redis_store_proxy.rb
126
157
  - lib/rack/attack/throttle.rb
127
158
  - lib/rack/attack/track.rb
128
159
  - lib/rack/attack/version.rb
129
160
  - lib/rack/attack/whitelist.rb
130
161
  - spec/allow2ban_spec.rb
131
162
  - spec/fail2ban_spec.rb
163
+ - spec/integration/offline_spec.rb
132
164
  - spec/integration/rack_attack_cache_spec.rb
165
+ - spec/rack_attack_dalli_proxy_spec.rb
166
+ - spec/rack_attack_request_spec.rb
133
167
  - spec/rack_attack_spec.rb
134
168
  - spec/rack_attack_throttle_spec.rb
135
169
  - spec/rack_attack_track_spec.rb
@@ -162,7 +196,10 @@ summary: Block & throttle abusive requests
162
196
  test_files:
163
197
  - spec/allow2ban_spec.rb
164
198
  - spec/fail2ban_spec.rb
199
+ - spec/integration/offline_spec.rb
165
200
  - spec/integration/rack_attack_cache_spec.rb
201
+ - spec/rack_attack_dalli_proxy_spec.rb
202
+ - spec/rack_attack_request_spec.rb
166
203
  - spec/rack_attack_spec.rb
167
204
  - spec/rack_attack_throttle_spec.rb
168
205
  - spec/rack_attack_track_spec.rb