railslove-rack-throttle 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|