rack-attack 0.0.1 → 0.0.3
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 +8 -0
- data/Rakefile +9 -0
- data/lib/rack/attack.rb +70 -13
- data/lib/rack/attack/blacklist.rb +13 -0
- data/lib/rack/attack/cache.rb +23 -0
- data/lib/rack/attack/check.rb +19 -0
- data/lib/rack/attack/throttle.rb +31 -0
- data/lib/rack/attack/version.rb +2 -2
- data/lib/rack/attack/whitelist.rb +12 -0
- data/spec/rack_attack_spec.rb +59 -10
- data/spec/spec_helper.rb +2 -0
- metadata +41 -3
data/README.md
CHANGED
@@ -1 +1,9 @@
|
|
1
1
|
# Rack::Attack - middleware for throttling & blocking abusive clients
|
2
|
+
|
3
|
+
## Processing order
|
4
|
+
* If any whitelist matches, the request is allowed
|
5
|
+
* If any blacklist matches, the request is blocked (unless a whitelist matched)
|
6
|
+
* If any throttle matches, the request is throttled (unless a whitelist or blacklist matched)
|
7
|
+
|
8
|
+
[](http://travis-ci.org/ktheory/rack-attack)
|
9
|
+
|
data/Rakefile
ADDED
data/lib/rack/attack.rb
CHANGED
@@ -1,29 +1,86 @@
|
|
1
1
|
require 'rack'
|
2
|
-
module Rack
|
3
|
-
|
4
|
-
|
2
|
+
module Rack::Attack
|
3
|
+
require 'rack/attack/cache'
|
4
|
+
require 'rack/attack/throttle'
|
5
|
+
require 'rack/attack/whitelist'
|
6
|
+
require 'rack/attack/blacklist'
|
5
7
|
|
6
|
-
|
8
|
+
class << self
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
attr_reader :cache, :notifier
|
11
|
+
|
12
|
+
def whitelist(name, &block)
|
13
|
+
(@whitelists ||= {})[name] = Whitelist.new(name, block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def blacklist(name, &block)
|
17
|
+
(@blacklists ||= {})[name] = Blacklist.new(name, block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def throttle(name, options, &block)
|
21
|
+
(@throttles ||= {})[name] = Throttle.new(name, options, block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def whitelists; @whitelists ||= {}; end
|
25
|
+
def blacklists; @blacklists ||= {}; end
|
26
|
+
def throttles; @throttles ||= {}; end
|
27
|
+
|
28
|
+
def new(app)
|
29
|
+
@cache ||= Cache.new
|
30
|
+
@notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
31
|
+
@app = app
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
req = Rack::Request.new(env)
|
38
|
+
|
39
|
+
if whitelisted?(req)
|
40
|
+
return @app.call(env)
|
10
41
|
end
|
11
42
|
|
12
|
-
|
43
|
+
if blacklisted?(req)
|
44
|
+
blacklisted_response
|
45
|
+
elsif throttled?(req)
|
46
|
+
throttled_response
|
47
|
+
else
|
48
|
+
@app.call(env)
|
13
49
|
end
|
50
|
+
end
|
14
51
|
|
15
|
-
|
52
|
+
def whitelisted?(req)
|
53
|
+
whitelists.any? do |name, whitelist|
|
54
|
+
whitelist[req]
|
16
55
|
end
|
56
|
+
end
|
17
57
|
|
58
|
+
def blacklisted?(req)
|
59
|
+
blacklists.any? do |name, blacklist|
|
60
|
+
blacklist[req]
|
61
|
+
end
|
18
62
|
end
|
19
63
|
|
20
|
-
def
|
21
|
-
|
64
|
+
def throttled?(req)
|
65
|
+
throttles.any? do |name, throttle|
|
66
|
+
throttle[req]
|
67
|
+
end
|
22
68
|
end
|
23
69
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
70
|
+
def instrument(payload)
|
71
|
+
notifier.instrument('rack.attack', payload) if notifier
|
72
|
+
end
|
73
|
+
|
74
|
+
def blacklisted_response
|
75
|
+
[503, {}, ['Blocked']]
|
76
|
+
end
|
77
|
+
|
78
|
+
def throttled_response
|
79
|
+
[503, {}, ['Throttled']]
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear!
|
83
|
+
@whitelists, @blacklists, @throttles = {}, {}, {}
|
27
84
|
end
|
28
85
|
|
29
86
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rack
|
2
|
+
module Attack
|
3
|
+
class Cache
|
4
|
+
|
5
|
+
attr_accessor :store, :prefix
|
6
|
+
def initialize
|
7
|
+
@store = ::Rails.cache if defined?(::Rails.cache)
|
8
|
+
@prefix = 'rack::attack'
|
9
|
+
end
|
10
|
+
|
11
|
+
def count(unprefixed_key, expires_in)
|
12
|
+
key = "#{prefix}:#{unprefixed_key}"
|
13
|
+
result = store.increment(key, 1, :expires_in => expires_in)
|
14
|
+
# NB: Some stores return nil when incrementing uninitialized values
|
15
|
+
if result.nil?
|
16
|
+
store.write(key, 1, :expires_in => expires_in)
|
17
|
+
end
|
18
|
+
result || 1
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack
|
2
|
+
module Attack
|
3
|
+
class Check
|
4
|
+
attr_reader :name, :block, :type
|
5
|
+
def initialize(name, block)
|
6
|
+
@name, @block = name, block
|
7
|
+
@type = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](req)
|
11
|
+
block[req].tap {|match|
|
12
|
+
Rack::Attack.instrument(:type => type, :name => name, :request => req) if match
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rack
|
2
|
+
module Attack
|
3
|
+
class Throttle
|
4
|
+
attr_reader :name, :limit, :period, :block
|
5
|
+
def initialize(name, options, block)
|
6
|
+
@name, @block = name, block
|
7
|
+
[:limit, :period].each do |opt|
|
8
|
+
raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt]
|
9
|
+
end
|
10
|
+
@limit = options[:limit]
|
11
|
+
@period = options[:period]
|
12
|
+
end
|
13
|
+
|
14
|
+
def cache
|
15
|
+
Rack::Attack.cache
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](req)
|
19
|
+
discriminator = block[req]
|
20
|
+
return false unless discriminator
|
21
|
+
|
22
|
+
key = "#{name}:#{discriminator}"
|
23
|
+
count = cache.count(key, period)
|
24
|
+
throttled = count > limit
|
25
|
+
Rack::Attack.instrument(:type => :throttle, :name => name, :request => req, :count => count, :throttled => throttled)
|
26
|
+
throttled
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/rack/attack/version.rb
CHANGED
data/spec/rack_attack_spec.rb
CHANGED
@@ -3,10 +3,6 @@ require_relative 'spec_helper'
|
|
3
3
|
describe 'Rack::Attack' do
|
4
4
|
include Rack::Test::Methods
|
5
5
|
|
6
|
-
before do
|
7
|
-
Rack::Attack.block("ip 1.2.3.4") {|req| req.ip == '1.2.3.4' }
|
8
|
-
end
|
9
|
-
|
10
6
|
def app
|
11
7
|
Rack::Builder.new {
|
12
8
|
use Rack::Attack
|
@@ -14,14 +10,67 @@ describe 'Rack::Attack' do
|
|
14
10
|
}.to_app
|
15
11
|
end
|
16
12
|
|
17
|
-
|
18
|
-
|
13
|
+
def self.allow_ok_requests
|
14
|
+
it "must allow ok requests" do
|
15
|
+
get '/', {}, 'REMOTE_ADDR' => '127.0.0.1'
|
16
|
+
last_response.status.must_equal 200
|
17
|
+
last_response.body.must_equal 'Hello World'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
after { Rack::Attack.clear! }
|
22
|
+
|
23
|
+
allow_ok_requests
|
24
|
+
|
25
|
+
describe 'with a blacklist' do
|
26
|
+
before do
|
27
|
+
@bad_ip = '1.2.3.4'
|
28
|
+
Rack::Attack.blacklist("ip #{@bad_ip}") {|req| req.ip == @bad_ip }
|
29
|
+
end
|
30
|
+
|
31
|
+
it('has a blacklist') { Rack::Attack.blacklists.key?("ip #{@bad_ip}") }
|
32
|
+
|
33
|
+
it "should blacklist bad requests" do
|
34
|
+
get '/', {}, 'REMOTE_ADDR' => @bad_ip
|
35
|
+
last_response.status.must_equal 503
|
36
|
+
end
|
37
|
+
|
38
|
+
allow_ok_requests
|
39
|
+
|
40
|
+
describe "and with a whitelist" do
|
41
|
+
before do
|
42
|
+
@good_ua = 'GoodUA'
|
43
|
+
Rack::Attack.whitelist("good ua") {|req| req.user_agent == @good_ua }
|
44
|
+
end
|
45
|
+
|
46
|
+
it('has a whitelist'){ Rack::Attack.whitelists.key?("good ua") }
|
47
|
+
it "should allow whitelists before blacklists" do
|
48
|
+
get '/', {}, 'REMOTE_ADDR' => @bad_ip, 'HTTP_USER_AGENT' => @good_ua
|
49
|
+
last_response.status.must_equal 200
|
50
|
+
end
|
51
|
+
end
|
19
52
|
end
|
20
53
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
54
|
+
describe 'with a throttle' do
|
55
|
+
before do
|
56
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
57
|
+
Rack::Attack.throttle('ip/sec', :limit => 1, :period => 1) { |req| req.ip }
|
58
|
+
end
|
59
|
+
|
60
|
+
it('should have a throttle'){ Rack::Attack.throttles.key?('ip/sec') }
|
61
|
+
allow_ok_requests
|
62
|
+
|
63
|
+
it 'should set the counter for one request' do
|
64
|
+
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
65
|
+
Rack::Attack.cache.store.read('rack::attack:ip/sec:1.2.3.4').must_equal 1
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should block 2 requests' do
|
69
|
+
2.times do
|
70
|
+
get '/', {}, 'REMOTE_ADDR' => '1.2.3.4'
|
71
|
+
end
|
72
|
+
last_response.status.must_equal 503
|
73
|
+
end
|
25
74
|
end
|
26
75
|
|
27
76
|
|
data/spec/spec_helper.rb
CHANGED
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: 0.0.
|
4
|
+
version: 0.0.3
|
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: 2012-07-
|
12
|
+
date: 2012-07-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -59,6 +59,38 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: activesupport
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.0.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.0.0
|
62
94
|
- !ruby/object:Gem::Dependency
|
63
95
|
name: debugger
|
64
96
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,8 +113,14 @@ executables: []
|
|
81
113
|
extensions: []
|
82
114
|
extra_rdoc_files: []
|
83
115
|
files:
|
116
|
+
- lib/rack/attack/blacklist.rb
|
117
|
+
- lib/rack/attack/cache.rb
|
118
|
+
- lib/rack/attack/check.rb
|
119
|
+
- lib/rack/attack/throttle.rb
|
84
120
|
- lib/rack/attack/version.rb
|
121
|
+
- lib/rack/attack/whitelist.rb
|
85
122
|
- lib/rack/attack.rb
|
123
|
+
- Rakefile
|
86
124
|
- LICENSE
|
87
125
|
- README.md
|
88
126
|
- spec/rack_attack_spec.rb
|
@@ -108,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
146
|
version: '0'
|
109
147
|
requirements: []
|
110
148
|
rubyforge_project:
|
111
|
-
rubygems_version: 1.8.
|
149
|
+
rubygems_version: 1.8.24
|
112
150
|
signing_key:
|
113
151
|
specification_version: 3
|
114
152
|
summary: Block & throttle abusive requests
|