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 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