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 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
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "spec/*_spec.rb"
7
+ end
8
+
9
+ task :default => :test
data/lib/rack/attack.rb CHANGED
@@ -1,29 +1,86 @@
1
1
  require 'rack'
2
- module Rack
3
- class Attack
4
- class << self
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
- attr_reader :blocks, :throttles, :whitelists
8
+ class << self
7
9
 
8
- def block(name, &block)
9
- (@blocks ||= {})[name] = block
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
- def throttle
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
- def whitelist
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 initialize(app)
21
- @app = app
64
+ def throttled?(req)
65
+ throttles.any? do |name, throttle|
66
+ throttle[req]
67
+ end
22
68
  end
23
69
 
24
- def call(env)
25
- puts 'Rack attack!'
26
- @app.call(env)
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,13 @@
1
+ require_relative 'check'
2
+ module Rack
3
+ module Attack
4
+ class Blacklist < Check
5
+ def initialize(name, block)
6
+ super
7
+ @type = :blacklist
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- class Attack
3
- VERSION = '0.0.1'
2
+ module Attack
3
+ VERSION = '0.0.3'
4
4
  end
5
5
  end
@@ -0,0 +1,12 @@
1
+ require_relative 'check'
2
+ module Rack
3
+ module Attack
4
+ class Whitelist < Check
5
+ def initialize(name, block)
6
+ super
7
+ @type = :whitelist
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -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
- it 'has a block' do
18
- Rack::Attack.blocks.class.must_equal Hash
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
- it "says hello" do
22
- get '/'
23
- last_response.status.must_equal 200
24
- last_response.body.must_equal 'Hello World'
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
@@ -3,5 +3,7 @@ require "bundler/setup"
3
3
 
4
4
  require "minitest/autorun"
5
5
  require "rack/test"
6
+ require 'debugger'
7
+ require 'active_support'
6
8
 
7
9
  require "rack/attack"
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.1
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-25 00:00:00.000000000 Z
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.23
149
+ rubygems_version: 1.8.24
112
150
  signing_key:
113
151
  specification_version: 3
114
152
  summary: Block & throttle abusive requests