improved-rack-throttle 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ module Rack; module Throttle
2
+ ##
3
+ class TimeWindow < Limiter
4
+ ##
5
+ # Returns `true` if fewer than the maximum number of requests permitted
6
+ # for the current window of time have been made.
7
+ #
8
+ # @param [Rack::Request] request
9
+ # @return [Boolean]
10
+ def allowed?(request)
11
+ count = cache_get(key = cache_key(request)).to_i + 1 rescue 1
12
+ allowed = count <= max_per_window.to_i
13
+ begin
14
+ cache_set(key, count)
15
+ allowed
16
+ rescue => e
17
+ allowed = true
18
+ end
19
+ end
20
+ end
21
+ end; end
@@ -0,0 +1,23 @@
1
+ module Rack; module Throttle
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 5
5
+ TINY = 0
6
+ EXTRA = nil
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ STRING << "-#{EXTRA}" if EXTRA
10
+
11
+ ##
12
+ # @return [String]
13
+ def self.to_s() STRING end
14
+
15
+ ##
16
+ # @return [String]
17
+ def self.to_str() STRING end
18
+
19
+ ##
20
+ # @return [Array(Integer, Integer, Integer)]
21
+ def self.to_a() [MAJOR, MINOR, TINY] end
22
+ end
23
+ end; end
@@ -0,0 +1,15 @@
1
+ require 'rack'
2
+
3
+ module Rack
4
+ module Throttle
5
+ autoload :Limiter, 'rack/throttle/limiter'
6
+ autoload :Interval, 'rack/throttle/interval'
7
+ autoload :TimeWindow, 'rack/throttle/time_window'
8
+ autoload :Daily, 'rack/throttle/daily'
9
+ autoload :Hourly, 'rack/throttle/hourly'
10
+ autoload :VERSION, 'rack/throttle/version'
11
+ autoload :Matcher, 'rack/throttle/matcher'
12
+ autoload :UrlMatcher, 'rack/throttle/matchers/url_matcher'
13
+ autoload :MethodMatcher, 'rack/throttle/matchers/method_matcher'
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Rack::Throttle::Daily do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::Daily.new(@target_app, :max_per_day => 3)
9
+ end
10
+
11
+ it "should be allowed if not seen this day" do
12
+ get "/foo"
13
+ last_response.body.should show_allowed_response
14
+ end
15
+
16
+ it "should be allowed if seen fewer than the max allowed per day" do
17
+ 2.times { get "/foo" }
18
+ last_response.body.should show_allowed_response
19
+ end
20
+
21
+ it "should not be allowed if seen more times than the max allowed per day" do
22
+ 4.times { get "/foo" }
23
+ last_response.body.should show_throttled_response
24
+ end
25
+
26
+ # TODO mess with time travelling and requests to make sure no overlap
27
+ end
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Rack::Throttle::Hourly do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::Hourly.new(@target_app, :max_per_hour => 3)
9
+ end
10
+
11
+ it "should be allowed if not seen this hour" do
12
+ get "/foo"
13
+ last_response.body.should show_allowed_response
14
+ end
15
+
16
+ it "should be allowed if seen fewer than the max allowed per hour" do
17
+ 2.times { get "/foo" }
18
+ last_response.body.should show_allowed_response
19
+ end
20
+
21
+ it "should not be allowed if seen more times than the max allowed per hour" do
22
+ 4.times { get "/foo" }
23
+ last_response.body.should show_throttled_response
24
+ end
25
+
26
+ # TODO mess with time travelling and requests to make sure no overlap
27
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+
4
+ describe Rack::Throttle::Interval do
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ @target_app ||= example_target_app
9
+ @app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1)
10
+ end
11
+
12
+ it "should allow the request if the source has not been seen" do
13
+ get "/foo"
14
+ last_response.body.should show_allowed_response
15
+ end
16
+
17
+ it "should allow the request if the source has not been seen in the current interval" do
18
+ Timecop.freeze do
19
+ get "/foo"
20
+ Timecop.freeze(1) do # Timecop.freeze won't do subsecond resolution
21
+ get "/foo"
22
+ end
23
+ end
24
+ last_response.body.should show_allowed_response
25
+ end
26
+
27
+ it "should not allow the request if the source has been seen inside the current interval" do
28
+ Timecop.freeze do
29
+ 2.times { get "/foo" }
30
+ end
31
+ last_response.body.should show_throttled_response
32
+ end
33
+
34
+ it "should gracefully allow the request if the cache bombs on getting" do
35
+ app.should_receive(:cache_get).and_raise(StandardError)
36
+ get "/foo"
37
+ last_response.body.should show_allowed_response
38
+ end
39
+
40
+ it "should gracefully allow the request if the cache bombs on setting" do
41
+ app.should_receive(:cache_set).and_raise(StandardError)
42
+ get "/foo"
43
+ last_response.body.should show_allowed_response
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Rack::Throttle::Limiter do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::Limiter.new(@target_app)
9
+ end
10
+
11
+ describe "basic calling" do
12
+ it "should return the example app" do
13
+ get "/foo"
14
+ last_response.body.should show_allowed_response
15
+ end
16
+
17
+ it "should call the application if allowed" do
18
+ app.should_receive(:allowed?).and_return(true)
19
+ get "/foo"
20
+ last_response.body.should show_allowed_response
21
+ end
22
+
23
+ it "should give a rate limit exceeded message if not allowed" do
24
+ app.should_receive(:allowed?).and_return(false)
25
+ get "/foo"
26
+ last_response.body.should show_throttled_response
27
+ end
28
+ end
29
+
30
+ describe "allowed?" do
31
+ it "should return true if whitelisted" do
32
+ app.should_receive(:whitelisted?).and_return(true)
33
+ get "/foo"
34
+ last_response.body.should show_allowed_response
35
+ end
36
+
37
+ it "should return false if blacklisted" do
38
+ app.should_receive(:blacklisted?).and_return(true)
39
+ get "/foo"
40
+ last_response.body.should show_throttled_response
41
+ end
42
+
43
+ it "should return true if not whitelisted or blacklisted" do
44
+ app.should_receive(:whitelisted?).and_return(false)
45
+ app.should_receive(:blacklisted?).and_return(false)
46
+ get "/foo"
47
+ last_response.body.should show_allowed_response
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Rack::Throttle::MethodMatcher do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::Limiter.new(@target_app, :rules => {:method => :post})
9
+ end
10
+
11
+ it "should not bother checking if the path doesn't match the rule" do
12
+ app.should_not_receive(:allowed?)
13
+ get "/foo"
14
+ last_response.body.should show_allowed_response
15
+ end
16
+
17
+ it "should check if the path matches the rule" do
18
+ app.should_receive(:allowed?).and_return(false)
19
+ post "/foo"
20
+ last_response.body.should show_throttled_response
21
+ end
22
+
23
+ it "should append the rule to the cache key" do
24
+ post "/foo"
25
+ app.send(:cache_key, last_request).should == "127.0.0.1:meth-post"
26
+ end
27
+ end
28
+
29
+
@@ -0,0 +1,46 @@
1
+ require "rspec"
2
+ require "rack/test"
3
+ require "rack/throttle"
4
+ require "timecop"
5
+
6
+ def example_target_app
7
+ @target_app = double("Example Rack App")
8
+ @target_app.stub(:call).with(any_args()).and_return([200, {}, "Example App Body"])
9
+ @target_app
10
+ end
11
+
12
+ RSpec::Matchers.define :show_allowed_response do
13
+ match do |body|
14
+ body.include?("Example App Body")
15
+ end
16
+
17
+ failure_message_for_should do
18
+ "expected response to show the allowed response"
19
+ end
20
+
21
+ failure_message_for_should_not do
22
+ "expected response not to show the allowed response"
23
+ end
24
+
25
+ description do
26
+ "expected the allowed response"
27
+ end
28
+ end
29
+
30
+ RSpec::Matchers.define :show_throttled_response do
31
+ match do |body|
32
+ body.include?("Rate Limit Exceeded")
33
+ end
34
+
35
+ failure_message_for_should do
36
+ "expected response to show the throttled response"
37
+ end
38
+
39
+ failure_message_for_should_not do
40
+ "expected response not to show the throttled response"
41
+ end
42
+
43
+ description do
44
+ "expected the throttled response"
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Rack::Throttle::UrlMatcher do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::Limiter.new(@target_app, :rules => {:url => /foo/})
9
+ end
10
+
11
+ it "should not bother checking if the path doesn't match the rule" do
12
+ app.should_not_receive(:allowed?)
13
+ get "/bar"
14
+ last_response.body.should show_allowed_response
15
+ end
16
+
17
+ it "should check if the path matches the rule" do
18
+ app.should_receive(:allowed?).and_return(false)
19
+ get "/foo"
20
+ last_response.body.should show_throttled_response
21
+ end
22
+
23
+ it "should append the rule to the cache key" do
24
+ get "/foo"
25
+ app.send(:cache_key, last_request).should == "127.0.0.1:url/foo/"
26
+ end
27
+ end
28
+
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: improved-rack-throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ben Somers
9
+ - Arto Bendiken
10
+ - Brendon Murphy
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-10-03 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rack
18
+ requirement: &70364343200440 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.0
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *70364343200440
27
+ - !ruby/object:Gem::Dependency
28
+ name: timecop
29
+ requirement: &70364343199340 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.5.2
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: *70364343199340
38
+ - !ruby/object:Gem::Dependency
39
+ name: rack-test
40
+ requirement: &70364343198560 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.6.2
46
+ type: :development
47
+ prerelease: false
48
+ version_requirements: *70364343198560
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ requirement: &70364343197860 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: 2.11.0
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: *70364343197860
60
+ - !ruby/object:Gem::Dependency
61
+ name: yard
62
+ requirement: &70364343213300 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: 0.5.5
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: *70364343213300
71
+ - !ruby/object:Gem::Dependency
72
+ name: rake
73
+ requirement: &70364343212560 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: *70364343212560
82
+ - !ruby/object:Gem::Dependency
83
+ name: jeweler
84
+ requirement: &70364343211840 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: *70364343211840
93
+ description: Rack middleware for rate-limiting incoming HTTP requests.
94
+ email: somers.ben@gmail.com
95
+ executables: []
96
+ extensions: []
97
+ extra_rdoc_files:
98
+ - README.md
99
+ files:
100
+ - .document
101
+ - Gemfile
102
+ - Gemfile.lock
103
+ - README.md
104
+ - Rakefile
105
+ - UNLICENSE
106
+ - doc/.gitignore
107
+ - etc/gdbm.ru
108
+ - etc/hash.ru
109
+ - etc/memcache-client.ru
110
+ - etc/memcache.ru
111
+ - etc/memcached.ru
112
+ - etc/redis.ru
113
+ - improved-rack-throttle.gemspec
114
+ - lib/rack/throttle.rb
115
+ - lib/rack/throttle/daily.rb
116
+ - lib/rack/throttle/hourly.rb
117
+ - lib/rack/throttle/interval.rb
118
+ - lib/rack/throttle/limiter.rb
119
+ - lib/rack/throttle/matcher.rb
120
+ - lib/rack/throttle/matchers/ip_matcher.rb
121
+ - lib/rack/throttle/matchers/method_matcher.rb
122
+ - lib/rack/throttle/matchers/url_matcher.rb
123
+ - lib/rack/throttle/time_window.rb
124
+ - lib/rack/throttle/version.rb
125
+ - spec/daily_spec.rb
126
+ - spec/hourly_spec.rb
127
+ - spec/interval_spec.rb
128
+ - spec/limiter_spec.rb
129
+ - spec/method_matcher_spec.rb
130
+ - spec/spec_helper.rb
131
+ - spec/url_matcher_spec.rb
132
+ homepage: http://github.com/bensomers/improved-rack-throttle
133
+ licenses:
134
+ - Public Domain
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ! '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ segments:
146
+ - 0
147
+ hash: -1128518403050699337
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 1.8.17
157
+ signing_key:
158
+ specification_version: 3
159
+ summary: HTTP request rate limiter for Rack applications.
160
+ test_files: []