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