improved-rack-throttle 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "rack", "~> 1.0.0"
3
+ gem "rack", ">= 1.0.0"
4
4
 
5
5
  group :development, :test do
6
6
  gem 'timecop', '~> 0.5.2'
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GEM
9
9
  rake
10
10
  rdoc
11
11
  json (1.7.5)
12
- rack (1.0.1)
12
+ rack (1.4.1)
13
13
  rack-test (0.6.2)
14
14
  rack (>= 1.0)
15
15
  rake (0.9.2.2)
@@ -31,7 +31,7 @@ PLATFORMS
31
31
 
32
32
  DEPENDENCIES
33
33
  jeweler
34
- rack (~> 1.0.0)
34
+ rack (>= 1.0.0)
35
35
  rack-test (~> 0.6.2)
36
36
  rake
37
37
  rspec (~> 2.11.0)
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "improved-rack-throttle"
8
- s.version = "0.6.0"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Ben Somers", "Arto Bendiken", "Brendon Murphy"]
12
- s.date = "2012-10-03"
12
+ s.date = "2012-10-05"
13
13
  s.description = "Rack middleware for rate-limiting incoming HTTP requests."
14
14
  s.email = "somers.ben@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -31,24 +31,26 @@ Gem::Specification.new do |s|
31
31
  "etc/redis.ru",
32
32
  "improved-rack-throttle.gemspec",
33
33
  "lib/rack/throttle.rb",
34
- "lib/rack/throttle/daily.rb",
35
- "lib/rack/throttle/hourly.rb",
36
- "lib/rack/throttle/interval.rb",
37
- "lib/rack/throttle/limiter.rb",
38
- "lib/rack/throttle/matcher.rb",
34
+ "lib/rack/throttle/limiters/daily.rb",
35
+ "lib/rack/throttle/limiters/hourly.rb",
36
+ "lib/rack/throttle/limiters/interval.rb",
37
+ "lib/rack/throttle/limiters/limiter.rb",
38
+ "lib/rack/throttle/limiters/sliding_window.rb",
39
+ "lib/rack/throttle/limiters/time_window.rb",
40
+ "lib/rack/throttle/matchers/matcher.rb",
39
41
  "lib/rack/throttle/matchers/method_matcher.rb",
40
42
  "lib/rack/throttle/matchers/url_matcher.rb",
41
43
  "lib/rack/throttle/matchers/user_agent_matcher.rb",
42
- "lib/rack/throttle/time_window.rb",
43
44
  "lib/rack/throttle/version.rb",
44
- "spec/daily_spec.rb",
45
- "spec/hourly_spec.rb",
46
- "spec/interval_spec.rb",
47
- "spec/limiter_spec.rb",
48
- "spec/method_matcher_spec.rb",
49
- "spec/spec_helper.rb",
50
- "spec/url_matcher_spec.rb",
51
- "spec/user_agent_matcher_spec.rb"
45
+ "spec/limiters/daily_spec.rb",
46
+ "spec/limiters/hourly_spec.rb",
47
+ "spec/limiters/interval_spec.rb",
48
+ "spec/limiters/limiter_spec.rb",
49
+ "spec/limiters/sliding_window_spec.rb",
50
+ "spec/matchers/method_matcher_spec.rb",
51
+ "spec/matchers/url_matcher_spec.rb",
52
+ "spec/matchers/user_agent_matcher_spec.rb",
53
+ "spec/spec_helper.rb"
52
54
  ]
53
55
  s.homepage = "http://github.com/bensomers/improved-rack-throttle"
54
56
  s.licenses = ["Public Domain"]
@@ -60,7 +62,7 @@ Gem::Specification.new do |s|
60
62
  s.specification_version = 3
61
63
 
62
64
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
63
- s.add_runtime_dependency(%q<rack>, ["~> 1.0.0"])
65
+ s.add_runtime_dependency(%q<rack>, [">= 1.0.0"])
64
66
  s.add_development_dependency(%q<timecop>, ["~> 0.5.2"])
65
67
  s.add_development_dependency(%q<rack-test>, ["~> 0.6.2"])
66
68
  s.add_development_dependency(%q<rspec>, ["~> 2.11.0"])
@@ -68,7 +70,7 @@ Gem::Specification.new do |s|
68
70
  s.add_development_dependency(%q<rake>, [">= 0"])
69
71
  s.add_development_dependency(%q<jeweler>, [">= 0"])
70
72
  else
71
- s.add_dependency(%q<rack>, ["~> 1.0.0"])
73
+ s.add_dependency(%q<rack>, [">= 1.0.0"])
72
74
  s.add_dependency(%q<timecop>, ["~> 0.5.2"])
73
75
  s.add_dependency(%q<rack-test>, ["~> 0.6.2"])
74
76
  s.add_dependency(%q<rspec>, ["~> 2.11.0"])
@@ -77,7 +79,7 @@ Gem::Specification.new do |s|
77
79
  s.add_dependency(%q<jeweler>, [">= 0"])
78
80
  end
79
81
  else
80
- s.add_dependency(%q<rack>, ["~> 1.0.0"])
82
+ s.add_dependency(%q<rack>, [">= 1.0.0"])
81
83
  s.add_dependency(%q<timecop>, ["~> 0.5.2"])
82
84
  s.add_dependency(%q<rack-test>, ["~> 0.6.2"])
83
85
  s.add_dependency(%q<rspec>, ["~> 2.11.0"])
File without changes
File without changes
@@ -20,7 +20,6 @@ module Rack; module Throttle
20
20
  # @option options [String] :key_prefix (nil)
21
21
  # @option options [Integer] :code (403)
22
22
  # @option options [String] :message ("Rate Limit Exceeded")
23
- # @option options [Regexp] :url_rule (nil)
24
23
  def initialize(app, options = {})
25
24
  rules = options.delete(:rules) || {}
26
25
  @app, @options, @matchers = app, options, []
@@ -0,0 +1,79 @@
1
+ module Rack; module Throttle
2
+ ##
3
+ # This rate limiter strategy throttles the application with
4
+ # a sliding window (implemented as a leaky bucket). It operates
5
+ # on second-level resolution. It takes :burst and :average
6
+ # options, which correspond to the maximum size of a traffic
7
+ # burst, and the maximum allowed average traffic level.
8
+ class SlidingWindow < Limiter
9
+ ##
10
+ # @param [#call] app
11
+ # @param [Hash{Symbol => Object}] options
12
+ # @option options [Integer] :burst 5
13
+ # @option options [Float] :average 1
14
+ def initialize(app, options = {})
15
+ super
16
+ options[:burst] ||= 5
17
+ options[:average] ||= 1
18
+ end
19
+
20
+ ##
21
+ # Returns `true` if sufficient time (equal to or more than
22
+ # {#minimum_interval}) has passed since the last request and the given
23
+ # present `request`.
24
+ #
25
+ # @param [Rack::Request] request
26
+ # @return [Boolean]
27
+ def allowed?(request)
28
+ t1 = request_start_time(request)
29
+ key = cache_key(request)
30
+ bucket = cache_get(key) rescue nil
31
+ bucket ||= LeakyBucket.new(options[:burst], options[:average])
32
+ bucket.maximum, bucket.outflow = options[:burst], options[:average]
33
+ bucket.leak!
34
+ bucket.increment!
35
+ allowed = !bucket.full?
36
+ begin
37
+ cache_set(key, bucket)
38
+ allowed
39
+ rescue StandardError => e
40
+ allowed = true
41
+ # If an error occurred while trying to update the timestamp stored
42
+ # in the cache, we will fall back to allowing the request through.
43
+ # This prevents the Rack application blowing up merely due to a
44
+ # backend cache server (Memcached, Redis, etc.) being offline.
45
+ end
46
+ end
47
+
48
+ class LeakyBucket
49
+ attr_accessor :maximum, :outflow
50
+ attr_reader :count, :last_touched
51
+
52
+ def initialize(maximum, outflow)
53
+ @maximum, @outflow = maximum, outflow
54
+ @count, @last_touched = 0, Time.now
55
+ end
56
+
57
+ def leak!
58
+ t = Time.now
59
+ time = t - last_touched
60
+ loss = (outflow * time).to_f
61
+ if loss > 0
62
+ @count -= loss
63
+ @last_touched = t
64
+ end
65
+ end
66
+
67
+ def increment!
68
+ @count = 0 if count < 0
69
+ @count += 1
70
+ @count = maximum if count > maximum
71
+ end
72
+
73
+ def full?
74
+ count == maximum
75
+ end
76
+ end
77
+
78
+ end
79
+ end; end
@@ -1,7 +1,7 @@
1
1
  module Rack; module Throttle
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 6
4
+ MINOR = 7
5
5
  TINY = 0
6
6
  EXTRA = nil
7
7
 
data/lib/rack/throttle.rb CHANGED
@@ -2,15 +2,16 @@ require 'rack'
2
2
 
3
3
  module Rack
4
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'
5
+ autoload :Limiter, 'rack/throttle/limiters/limiter'
6
+ autoload :Interval, 'rack/throttle/limiters/interval'
7
+ autoload :TimeWindow, 'rack/throttle/limiters/time_window'
8
+ autoload :Daily, 'rack/throttle/limiters/daily'
9
+ autoload :Hourly, 'rack/throttle/limiters/hourly'
10
+ autoload :SlidingWindow, 'rack/throttle/limiters/sliding_window'
11
+ autoload :VERSION, 'rack/throttle/version'
12
+ autoload :Matcher, 'rack/throttle/matchers/matcher'
13
+ autoload :UrlMatcher, 'rack/throttle/matchers/url_matcher'
14
+ autoload :MethodMatcher, 'rack/throttle/matchers/method_matcher'
14
15
  autoload :UserAgentMatcher, 'rack/throttle/matchers/user_agent_matcher'
15
16
  end
16
17
  end
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::Daily do
4
4
  include Rack::Test::Methods
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::Hourly do
4
4
  include Rack::Test::Methods
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
 
4
4
  describe Rack::Throttle::Interval do
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::Limiter do
4
4
  include Rack::Test::Methods
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Rack::Throttle::SlidingWindow do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ @target_app ||= example_target_app
8
+ @app ||= Rack::Throttle::SlidingWindow.new(@target_app, :burst => 2, :average => 1)
9
+ end
10
+
11
+ before(:each) do
12
+ @time = Time.now
13
+ Timecop.freeze
14
+ end
15
+
16
+ after(:each) do
17
+ Timecop.return
18
+ end
19
+
20
+ it "should allow the request if the source has not been seen at all" do
21
+ get "/foo"
22
+ last_response.body.should show_allowed_response
23
+ end
24
+
25
+ it "should allow the request if the rate is above-average but within the burst rule" do
26
+ Timecop.freeze(@time) { get "/foo" }
27
+ Timecop.freeze(@time + 0.5) { get "/foo" }
28
+ last_response.body.should show_allowed_response
29
+ end
30
+
31
+ it "should not allow the request if the rate is greater than the burst rule" do
32
+ Timecop.freeze(@time) { get "/foo" }
33
+ Timecop.freeze(@time + 0.3) { get "/foo" }
34
+ Timecop.freeze(@time + 0.6) { get "/foo" }
35
+ last_response.body.should show_throttled_response
36
+ end
37
+
38
+ it "should allow the request if the rate is less than the average" do
39
+ Timecop.freeze(@time) { get "/foo" }
40
+ Timecop.freeze(@time + 0.5) { get "/foo" }
41
+ Timecop.freeze(@time + 2) { get "/foo" }
42
+
43
+ last_response.body.should show_allowed_response
44
+ end
45
+
46
+ it "should not allow the request if the rate is more than the average" do
47
+ Timecop.freeze(@time) { get "/foo" }
48
+ Timecop.freeze(@time + 0.5) { get "/foo" }
49
+ Timecop.freeze(@time + 1) { get "/foo" }
50
+ Timecop.freeze(@time + 1.5) { get "/foo" }
51
+
52
+ last_response.body.should show_throttled_response
53
+ end
54
+
55
+ it "should gracefully allow the request if the cache bombs on getting" do
56
+ app.should_receive(:cache_get).and_raise(StandardError)
57
+ get "/foo"
58
+ last_response.body.should show_allowed_response
59
+ end
60
+
61
+ it "should gracefully allow the request if the cache bombs on setting" do
62
+ app.should_receive(:cache_set).and_raise(StandardError)
63
+ get "/foo"
64
+ last_response.body.should show_allowed_response
65
+ end
66
+ end
67
+
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::MethodMatcher do
4
4
  include Rack::Test::Methods
@@ -25,5 +25,3 @@ describe Rack::Throttle::MethodMatcher do
25
25
  app.send(:cache_key, last_request).should == "127.0.0.1:meth-post"
26
26
  end
27
27
  end
28
-
29
-
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::UrlMatcher do
4
4
  include Rack::Test::Methods
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
2
 
3
3
  describe Rack::Throttle::UserAgentMatcher do
4
4
  include Rack::Test::Methods
@@ -13,7 +13,7 @@ describe Rack::Throttle::UserAgentMatcher do
13
13
  get "/foo"
14
14
  last_response.body.should show_allowed_response
15
15
  end
16
-
16
+
17
17
  it "should check if the path matches the rule" do
18
18
  app.should_receive(:allowed?).and_return(false)
19
19
  header 'User-Agent', 'blahdeblah GoogleBot owns your soul'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: improved-rack-throttle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,22 +11,22 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-10-03 00:00:00.000000000 Z
14
+ date: 2012-10-05 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rack
18
- requirement: &70147455830820 !ruby/object:Gem::Requirement
18
+ requirement: &70296124961520 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
- - - ~>
21
+ - - ! '>='
22
22
  - !ruby/object:Gem::Version
23
23
  version: 1.0.0
24
24
  type: :runtime
25
25
  prerelease: false
26
- version_requirements: *70147455830820
26
+ version_requirements: *70296124961520
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: timecop
29
- requirement: &70147455830340 !ruby/object:Gem::Requirement
29
+ requirement: &70296124960800 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ~>
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: 0.5.2
35
35
  type: :development
36
36
  prerelease: false
37
- version_requirements: *70147455830340
37
+ version_requirements: *70296124960800
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: rack-test
40
- requirement: &70147455829840 !ruby/object:Gem::Requirement
40
+ requirement: &70296124960000 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ~>
@@ -45,10 +45,10 @@ dependencies:
45
45
  version: 0.6.2
46
46
  type: :development
47
47
  prerelease: false
48
- version_requirements: *70147455829840
48
+ version_requirements: *70296124960000
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: rspec
51
- requirement: &70147455829140 !ruby/object:Gem::Requirement
51
+ requirement: &70296124959520 !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
54
  - - ~>
@@ -56,10 +56,10 @@ dependencies:
56
56
  version: 2.11.0
57
57
  type: :development
58
58
  prerelease: false
59
- version_requirements: *70147455829140
59
+ version_requirements: *70296124959520
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: yard
62
- requirement: &70147455828360 !ruby/object:Gem::Requirement
62
+ requirement: &70296124958980 !ruby/object:Gem::Requirement
63
63
  none: false
64
64
  requirements:
65
65
  - - ! '>='
@@ -67,10 +67,10 @@ dependencies:
67
67
  version: 0.5.5
68
68
  type: :development
69
69
  prerelease: false
70
- version_requirements: *70147455828360
70
+ version_requirements: *70296124958980
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: rake
73
- requirement: &70147455844160 !ruby/object:Gem::Requirement
73
+ requirement: &70296124958220 !ruby/object:Gem::Requirement
74
74
  none: false
75
75
  requirements:
76
76
  - - ! '>='
@@ -78,10 +78,10 @@ dependencies:
78
78
  version: '0'
79
79
  type: :development
80
80
  prerelease: false
81
- version_requirements: *70147455844160
81
+ version_requirements: *70296124958220
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: jeweler
84
- requirement: &70147455843680 !ruby/object:Gem::Requirement
84
+ requirement: &70296124957640 !ruby/object:Gem::Requirement
85
85
  none: false
86
86
  requirements:
87
87
  - - ! '>='
@@ -89,7 +89,7 @@ dependencies:
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
- version_requirements: *70147455843680
92
+ version_requirements: *70296124957640
93
93
  description: Rack middleware for rate-limiting incoming HTTP requests.
94
94
  email: somers.ben@gmail.com
95
95
  executables: []
@@ -112,24 +112,26 @@ files:
112
112
  - etc/redis.ru
113
113
  - improved-rack-throttle.gemspec
114
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
115
+ - lib/rack/throttle/limiters/daily.rb
116
+ - lib/rack/throttle/limiters/hourly.rb
117
+ - lib/rack/throttle/limiters/interval.rb
118
+ - lib/rack/throttle/limiters/limiter.rb
119
+ - lib/rack/throttle/limiters/sliding_window.rb
120
+ - lib/rack/throttle/limiters/time_window.rb
121
+ - lib/rack/throttle/matchers/matcher.rb
120
122
  - lib/rack/throttle/matchers/method_matcher.rb
121
123
  - lib/rack/throttle/matchers/url_matcher.rb
122
124
  - lib/rack/throttle/matchers/user_agent_matcher.rb
123
- - lib/rack/throttle/time_window.rb
124
125
  - 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
126
+ - spec/limiters/daily_spec.rb
127
+ - spec/limiters/hourly_spec.rb
128
+ - spec/limiters/interval_spec.rb
129
+ - spec/limiters/limiter_spec.rb
130
+ - spec/limiters/sliding_window_spec.rb
131
+ - spec/matchers/method_matcher_spec.rb
132
+ - spec/matchers/url_matcher_spec.rb
133
+ - spec/matchers/user_agent_matcher_spec.rb
130
134
  - spec/spec_helper.rb
131
- - spec/url_matcher_spec.rb
132
- - spec/user_agent_matcher_spec.rb
133
135
  homepage: http://github.com/bensomers/improved-rack-throttle
134
136
  licenses:
135
137
  - Public Domain
@@ -145,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
145
147
  version: '0'
146
148
  segments:
147
149
  - 0
148
- hash: -1924272463384256508
150
+ hash: 3964473901145440251
149
151
  required_rubygems_version: !ruby/object:Gem::Requirement
150
152
  none: false
151
153
  requirements: