improved-rack-throttle 0.5.0

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.
@@ -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: []