rack-attack 0.0.1 → 0.0.3
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 +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
|
+
[![Travis CI](https://secure.travis-ci.org/ktheory/rack-attack.png)](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
|