railslove-rack-throttle 0.0.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/.gitignore +5 -0
- data/.yardopts +11 -0
- data/AUTHORS +2 -0
- data/README +240 -0
- data/README.md +240 -0
- data/Rakefile +47 -0
- data/UNLICENSE +24 -0
- data/VERSION +1 -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/lib/rack/throttle.rb +13 -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 +214 -0
- data/lib/rack/throttle/per_minute.rb +43 -0
- data/lib/rack/throttle/time_window.rb +21 -0
- data/lib/rack/throttle/version.rb +23 -0
- data/railslove-rack-throttle.gemspec +84 -0
- data/spec/daily_spec.rb +40 -0
- data/spec/hourly_spec.rb +40 -0
- data/spec/interval_spec.rb +41 -0
- data/spec/limiter_spec.rb +64 -0
- data/spec/per_minute_spec.rb +40 -0
- data/spec/spec_helper.rb +49 -0
- metadata +140 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rack; module Throttle
|
2
|
+
##
|
3
|
+
# This rate limiter strategy throttles the application by defining a
|
4
|
+
# maximum number of allowed HTTP requests per minute (by default, 60
|
5
|
+
# requests per minute, which works out to an average of 1 request per
|
6
|
+
# second).
|
7
|
+
#
|
8
|
+
# Note that this strategy doesn't use a sliding time window, but rather
|
9
|
+
# tracks requests per distinct minute. This means that the throttling
|
10
|
+
# counter is reset every minute on the minute (according to the server's local
|
11
|
+
# timezone).
|
12
|
+
#
|
13
|
+
# @example Allowing up to 3,600 requests per minute
|
14
|
+
# use Rack::Throttle::PerMinute
|
15
|
+
#
|
16
|
+
# @example Allowing up to 100 requests per minute
|
17
|
+
# use Rack::Throttle::PerMinute, :max => 100
|
18
|
+
#
|
19
|
+
class PerMinute < TimeWindow
|
20
|
+
##
|
21
|
+
# @param [#call] app
|
22
|
+
# @param [Hash{Symbol => Object}] options
|
23
|
+
# @option options [Integer] :max (3600)
|
24
|
+
def initialize(app, options = {})
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_per_minute
|
29
|
+
@max_per_minute ||= options[:max_per_minute] || options[:max] || 60
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :max_per_window, :max_per_minute
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
##
|
37
|
+
# @param [Rack::Request] request
|
38
|
+
# @return [String]
|
39
|
+
def cache_key(request)
|
40
|
+
[super, Time.now.strftime('%Y-%m-%dT%H%M')].join(':')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end; end
|
@@ -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 = 3
|
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,84 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{railslove-rack-throttle}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Arto Bendiken", "Brendon Murphy", "reddavis"]
|
12
|
+
s.date = %q{2010-07-14}
|
13
|
+
s.description = %q{Rack middleware for rate-limiting incoming HTTP requests.}
|
14
|
+
s.email = %q{reddavis@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
".yardopts",
|
22
|
+
"AUTHORS",
|
23
|
+
"README",
|
24
|
+
"README.md",
|
25
|
+
"Rakefile",
|
26
|
+
"UNLICENSE",
|
27
|
+
"VERSION",
|
28
|
+
"doc/.gitignore",
|
29
|
+
"etc/gdbm.ru",
|
30
|
+
"etc/hash.ru",
|
31
|
+
"etc/memcache-client.ru",
|
32
|
+
"etc/memcache.ru",
|
33
|
+
"etc/memcached.ru",
|
34
|
+
"etc/redis.ru",
|
35
|
+
"lib/rack/throttle.rb",
|
36
|
+
"lib/rack/throttle/daily.rb",
|
37
|
+
"lib/rack/throttle/hourly.rb",
|
38
|
+
"lib/rack/throttle/interval.rb",
|
39
|
+
"lib/rack/throttle/limiter.rb",
|
40
|
+
"lib/rack/throttle/per_minute.rb",
|
41
|
+
"lib/rack/throttle/time_window.rb",
|
42
|
+
"lib/rack/throttle/version.rb",
|
43
|
+
"railslove-rack-throttle.gemspec",
|
44
|
+
"spec/daily_spec.rb",
|
45
|
+
"spec/hourly_spec.rb",
|
46
|
+
"spec/interval_spec.rb",
|
47
|
+
"spec/limiter_spec.rb",
|
48
|
+
"spec/per_minute_spec.rb",
|
49
|
+
"spec/spec_helper.rb"
|
50
|
+
]
|
51
|
+
s.homepage = %q{http://github.com/railslove/rack-throttle}
|
52
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
53
|
+
s.require_paths = ["lib"]
|
54
|
+
s.rubygems_version = %q{1.3.6}
|
55
|
+
s.summary = %q{Extension of rack-throttle - HTTP request rate limiter for Rack applications.}
|
56
|
+
s.test_files = [
|
57
|
+
"spec/daily_spec.rb",
|
58
|
+
"spec/hourly_spec.rb",
|
59
|
+
"spec/interval_spec.rb",
|
60
|
+
"spec/limiter_spec.rb",
|
61
|
+
"spec/per_minute_spec.rb",
|
62
|
+
"spec/spec_helper.rb"
|
63
|
+
]
|
64
|
+
|
65
|
+
if s.respond_to? :specification_version then
|
66
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
67
|
+
s.specification_version = 3
|
68
|
+
|
69
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
70
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
71
|
+
s.add_development_dependency(%q<rack-test>, [">= 0.5.3"])
|
72
|
+
s.add_runtime_dependency(%q<rack>, [">= 1.0.0"])
|
73
|
+
else
|
74
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
75
|
+
s.add_dependency(%q<rack-test>, [">= 0.5.3"])
|
76
|
+
s.add_dependency(%q<rack>, [">= 1.0.0"])
|
77
|
+
end
|
78
|
+
else
|
79
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
80
|
+
s.add_dependency(%q<rack-test>, [">= 0.5.3"])
|
81
|
+
s.add_dependency(%q<rack>, [">= 1.0.0"])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
data/spec/daily_spec.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Throttle::Daily do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
before do
|
7
|
+
def app
|
8
|
+
@target_app ||= example_target_app
|
9
|
+
@app ||= Rack::Throttle::Daily.new(@target_app, :max_per_day => 3)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be allowed if not seen this day" do
|
14
|
+
get "/foo"
|
15
|
+
last_response.body.should show_allowed_response
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be allowed if seen fewer than the max allowed per day" do
|
19
|
+
2.times { get "/foo" }
|
20
|
+
last_response.body.should show_allowed_response
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not be allowed if seen more times than the max allowed per day" do
|
24
|
+
4.times { get "/foo" }
|
25
|
+
last_response.body.should show_throttled_response
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be allowed if we are in a new day" do
|
29
|
+
Time.stub!(:now).and_return(Time.local(2000,"jan",1,0,0,0))
|
30
|
+
|
31
|
+
4.times { get "/foo" }
|
32
|
+
last_response.body.should show_throttled_response
|
33
|
+
|
34
|
+
forward_one_minute = Time.now + 60*60*24
|
35
|
+
Time.stub!(:now).and_return(forward_one_minute)
|
36
|
+
|
37
|
+
get "/foo"
|
38
|
+
last_response.body.should show_allowed_response
|
39
|
+
end
|
40
|
+
end
|
data/spec/hourly_spec.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Throttle::Hourly do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
before do
|
7
|
+
def app
|
8
|
+
@target_app ||= example_target_app
|
9
|
+
@app ||= Rack::Throttle::Hourly.new(@target_app, :max_per_hour => 3)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be allowed if not seen this hour" do
|
14
|
+
get "/foo"
|
15
|
+
last_response.body.should show_allowed_response
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be allowed if seen fewer than the max allowed per hour" do
|
19
|
+
2.times { get "/foo" }
|
20
|
+
last_response.body.should show_allowed_response
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not be allowed if seen more times than the max allowed per hour" do
|
24
|
+
4.times { get "/foo" }
|
25
|
+
last_response.body.should show_throttled_response
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be allowed if we are in a new hour" do
|
29
|
+
Time.stub!(:now).and_return(Time.local(2000,"jan",1,0,0,0))
|
30
|
+
|
31
|
+
4.times { get "/foo" }
|
32
|
+
last_response.body.should show_throttled_response
|
33
|
+
|
34
|
+
forward_one_minute = Time.now + 60*60
|
35
|
+
Time.stub!(:now).and_return(forward_one_minute)
|
36
|
+
|
37
|
+
get "/foo"
|
38
|
+
last_response.body.should show_allowed_response
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Throttle::Interval do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
before do
|
7
|
+
def app
|
8
|
+
@target_app ||= example_target_app
|
9
|
+
@app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should allow the request if the source has not been seen" do
|
14
|
+
get "/foo"
|
15
|
+
last_response.body.should show_allowed_response
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should allow the request if the source has not been seen in the current interval" do
|
19
|
+
get "/foo"
|
20
|
+
sleep 0.2 # Should time travel this instead?
|
21
|
+
get "/foo"
|
22
|
+
last_response.body.should show_allowed_response
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not all the request if the source has been seen inside the current interval" do
|
26
|
+
2.times { get "/foo" }
|
27
|
+
last_response.body.should show_throttled_response
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should gracefully allow the request if the cache bombs on getting" do
|
31
|
+
app.should_receive(:cache_get).and_raise(StandardError)
|
32
|
+
get "/foo"
|
33
|
+
last_response.body.should show_allowed_response
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should gracefully allow the request if the cache bombs on setting" do
|
37
|
+
app.should_receive(:cache_set).and_raise(StandardError)
|
38
|
+
get "/foo"
|
39
|
+
last_response.body.should show_allowed_response
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Throttle::Limiter do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
before do
|
7
|
+
def app
|
8
|
+
@target_app ||= example_target_app
|
9
|
+
@app ||= Rack::Throttle::Limiter.new(@target_app)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "basic calling" do
|
14
|
+
it "should return the example app" do
|
15
|
+
get "/foo"
|
16
|
+
last_response.body.should show_allowed_response
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should call the application if allowed" do
|
20
|
+
app.should_receive(:allowed?).and_return(true)
|
21
|
+
get "/foo"
|
22
|
+
last_response.body.should show_allowed_response
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should give a rate limit exceeded message if not allowed" do
|
26
|
+
app.should_receive(:allowed?).and_return(false)
|
27
|
+
get "/foo"
|
28
|
+
last_response.body.should show_throttled_response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "allowed?" do
|
33
|
+
it "should return true if whitelisted" do
|
34
|
+
app.should_receive(:whitelisted?).and_return(true)
|
35
|
+
get "/foo"
|
36
|
+
last_response.body.should show_allowed_response
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return false if blacklisted" do
|
40
|
+
app.should_receive(:blacklisted?).and_return(true)
|
41
|
+
get "/foo"
|
42
|
+
last_response.body.should show_throttled_response
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return true if not whitelisted or blacklisted" do
|
46
|
+
app.should_receive(:whitelisted?).and_return(false)
|
47
|
+
app.should_receive(:blacklisted?).and_return(false)
|
48
|
+
get "/foo"
|
49
|
+
last_response.body.should show_allowed_response
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should call proc when false" do
|
53
|
+
#proc = mock("test")
|
54
|
+
#@app = Rack::Throttle::Limiter.new(@target_app, :on_reject => proc)
|
55
|
+
#
|
56
|
+
#app.should_receive(:allowed?).and_return(false)
|
57
|
+
#proc.should_receive(:call).once
|
58
|
+
#
|
59
|
+
#get "/foo"
|
60
|
+
#
|
61
|
+
#@app = Rack::Throttle::Limiter.new(@target_app)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Throttle::PerMinute do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
before do
|
7
|
+
def app
|
8
|
+
@target_app ||= example_target_app
|
9
|
+
@app ||= Rack::Throttle::PerMinute.new(@target_app, :max_per_minute => 3)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be allowed if not seen this hour" do
|
14
|
+
get "/foo"
|
15
|
+
last_response.body.should show_allowed_response
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be allowed if seen fewer than the max allowed per hour" do
|
19
|
+
2.times { get "/foo" }
|
20
|
+
last_response.body.should show_allowed_response
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not be allowed if seen more times than the max allowed per hour" do
|
24
|
+
4.times { get "/foo" }
|
25
|
+
last_response.body.should show_throttled_response
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be allowed if we are in a new minute" do
|
29
|
+
Time.stub!(:now).and_return(Time.local(2000,"jan",1,0,0,0))
|
30
|
+
|
31
|
+
4.times { get "/foo" }
|
32
|
+
last_response.body.should show_throttled_response
|
33
|
+
|
34
|
+
forward_one_minute = Time.now + 60
|
35
|
+
Time.stub!(:now).and_return(forward_one_minute)
|
36
|
+
|
37
|
+
get "/foo"
|
38
|
+
last_response.body.should show_allowed_response
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# So that the installed throttle gem doesnt interfere
|
2
|
+
$:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require "spec"
|
6
|
+
require 'spec/autorun'
|
7
|
+
require "rack/test"
|
8
|
+
require "rack/throttle"
|
9
|
+
|
10
|
+
def example_target_app
|
11
|
+
@target_app ||= mock("Example Rack App")
|
12
|
+
@target_app.stub!(:call).and_return([200, {}, "Example App Body"])
|
13
|
+
end
|
14
|
+
|
15
|
+
Spec::Matchers.define :show_allowed_response do
|
16
|
+
match do |body|
|
17
|
+
body.include?("Example App Body")
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_for_should do
|
21
|
+
"expected response to show the allowed response"
|
22
|
+
end
|
23
|
+
|
24
|
+
failure_message_for_should_not do
|
25
|
+
"expected response not to show the allowed response"
|
26
|
+
end
|
27
|
+
|
28
|
+
description do
|
29
|
+
"expected the allowed response"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Matchers.define :show_throttled_response do
|
34
|
+
match do |body|
|
35
|
+
body.include?("Rate Limit Exceeded")
|
36
|
+
end
|
37
|
+
|
38
|
+
failure_message_for_should do
|
39
|
+
"expected response to show the throttled response"
|
40
|
+
end
|
41
|
+
|
42
|
+
failure_message_for_should_not do
|
43
|
+
"expected response not to show the throttled response"
|
44
|
+
end
|
45
|
+
|
46
|
+
description do
|
47
|
+
"expected the throttled response"
|
48
|
+
end
|
49
|
+
end
|