improved-rack-throttle 0.6.0 → 0.7.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/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: