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.
- data/.document +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +39 -0
- data/README.md +213 -0
- data/Rakefile +41 -0
- data/UNLICENSE +24 -0
- data/doc/.gitignore +2 -0
- data/etc/gdbm.ru +7 -0
- data/etc/hash.ru +6 -0
- data/etc/memcache-client.ru +8 -0
- data/etc/memcache.ru +8 -0
- data/etc/memcached.ru +8 -0
- data/etc/redis.ru +8 -0
- data/improved-rack-throttle.gemspec +85 -0
- data/lib/rack/throttle/daily.rb +44 -0
- data/lib/rack/throttle/hourly.rb +44 -0
- data/lib/rack/throttle/interval.rb +63 -0
- data/lib/rack/throttle/limiter.rb +228 -0
- data/lib/rack/throttle/matcher.rb +24 -0
- data/lib/rack/throttle/matchers/ip_matcher.rb +14 -0
- data/lib/rack/throttle/matchers/method_matcher.rb +15 -0
- data/lib/rack/throttle/matchers/url_matcher.rb +15 -0
- data/lib/rack/throttle/time_window.rb +21 -0
- data/lib/rack/throttle/version.rb +23 -0
- data/lib/rack/throttle.rb +15 -0
- data/spec/daily_spec.rb +27 -0
- data/spec/hourly_spec.rb +27 -0
- data/spec/interval_spec.rb +45 -0
- data/spec/limiter_spec.rb +51 -0
- data/spec/method_matcher_spec.rb +29 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/url_matcher_spec.rb +28 -0
- metadata +160 -0
@@ -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
|
data/spec/daily_spec.rb
ADDED
@@ -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
|
data/spec/hourly_spec.rb
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|