improved-rack-throttle 0.7.1 → 0.8.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.
- checksums.yaml +7 -0
- data/.travis.yml +9 -0
- data/Gemfile +5 -4
- data/Gemfile.lock +30 -20
- data/README.md +10 -11
- data/Rakefile +4 -2
- data/improved-rack-throttle.gemspec +20 -16
- data/lib/rack/throttle/limiters/limiter.rb +2 -2
- data/lib/rack/throttle/limiters/sliding_window.rb +14 -5
- data/lib/rack/throttle/matchers/url_matcher.rb +1 -1
- data/lib/rack/throttle/version.rb +2 -2
- data/spec/limiters/daily_spec.rb +6 -6
- data/spec/limiters/hourly_spec.rb +6 -6
- data/spec/limiters/interval_spec.rb +12 -12
- data/spec/limiters/limiter_spec.rb +18 -18
- data/spec/limiters/sliding_window_spec.rb +12 -12
- data/spec/matchers/method_matcher_spec.rb +6 -6
- data/spec/matchers/url_matcher_spec.rb +6 -6
- data/spec/matchers/user_agent_matcher_spec.rb +5 -5
- data/spec/spec_helper.rb +22 -17
- metadata +47 -52
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dea077855e9d5f1e7aa8126af311c58f518e2abf
|
4
|
+
data.tar.gz: d1c0ce949e3a7152adfb1b9b6ae96896021d5e81
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 77a62c9ad00ee3aff04a413e5e186a261cd925c9b46e281dd167604fee5b3df2451daf4eb775f1c3b94a4b1e1fb16feddd35828763ed33666dc62df7c5bfc6cb
|
7
|
+
data.tar.gz: ab2f078f3380a13826fbb30f47292540ae59c8cf3269f1fa4ce051825962574d7d9e6021709058381dabb7035cd610b4ce05d2575e57f196292abb4aa00bde8f
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
@@ -3,10 +3,11 @@ source "http://rubygems.org"
|
|
3
3
|
gem "rack", ">= 1.0.0"
|
4
4
|
|
5
5
|
group :development, :test do
|
6
|
-
gem 'timecop',
|
7
|
-
gem 'rack-test'
|
8
|
-
gem 'rspec'
|
9
|
-
gem 'yard'
|
6
|
+
gem 'timecop', "<= 0.6.2.2"
|
7
|
+
gem 'rack-test'
|
8
|
+
gem 'rspec'
|
9
|
+
gem 'yard'
|
10
|
+
gem "simplecov", :require => false
|
10
11
|
gem 'redcarpet'
|
11
12
|
gem 'rake'
|
12
13
|
gem 'jeweler'
|
data/Gemfile.lock
CHANGED
@@ -1,31 +1,40 @@
|
|
1
1
|
GEM
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
|
-
diff-lcs (1.
|
4
|
+
diff-lcs (1.2.5)
|
5
5
|
git (1.2.5)
|
6
6
|
jeweler (1.8.4)
|
7
7
|
bundler (~> 1.0)
|
8
8
|
git (>= 1.2.5)
|
9
9
|
rake
|
10
10
|
rdoc
|
11
|
-
json (1.
|
12
|
-
|
11
|
+
json (1.8.0)
|
12
|
+
multi_json (1.7.6)
|
13
|
+
rack (1.5.2)
|
13
14
|
rack-test (0.6.2)
|
14
15
|
rack (>= 1.0)
|
15
|
-
rake (10.0.
|
16
|
-
rdoc (
|
16
|
+
rake (10.0.4)
|
17
|
+
rdoc (4.0.1)
|
17
18
|
json (~> 1.4)
|
18
|
-
redcarpet (2.
|
19
|
-
rspec (
|
20
|
-
rspec-core (~>
|
21
|
-
rspec-expectations (~>
|
22
|
-
rspec-mocks (~>
|
23
|
-
rspec-core (
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
19
|
+
redcarpet (2.3.0)
|
20
|
+
rspec (3.0.0)
|
21
|
+
rspec-core (~> 3.0.0)
|
22
|
+
rspec-expectations (~> 3.0.0)
|
23
|
+
rspec-mocks (~> 3.0.0)
|
24
|
+
rspec-core (3.0.1)
|
25
|
+
rspec-support (~> 3.0.0)
|
26
|
+
rspec-expectations (3.0.1)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.0.0)
|
29
|
+
rspec-mocks (3.0.1)
|
30
|
+
rspec-support (~> 3.0.0)
|
31
|
+
rspec-support (3.0.0)
|
32
|
+
simplecov (0.7.1)
|
33
|
+
multi_json (~> 1.0)
|
34
|
+
simplecov-html (~> 0.7.1)
|
35
|
+
simplecov-html (0.7.1)
|
36
|
+
timecop (0.6.1)
|
37
|
+
yard (0.8.6.1)
|
29
38
|
|
30
39
|
PLATFORMS
|
31
40
|
ruby
|
@@ -33,9 +42,10 @@ PLATFORMS
|
|
33
42
|
DEPENDENCIES
|
34
43
|
jeweler
|
35
44
|
rack (>= 1.0.0)
|
36
|
-
rack-test
|
45
|
+
rack-test
|
37
46
|
rake
|
38
47
|
redcarpet
|
39
|
-
rspec
|
40
|
-
|
41
|
-
|
48
|
+
rspec
|
49
|
+
simplecov
|
50
|
+
timecop (<= 0.6.2.2)
|
51
|
+
yard
|
data/README.md
CHANGED
@@ -3,6 +3,7 @@ HTTP Request Rate Limiter for Rack Applications
|
|
3
3
|
|
4
4
|
[](https://codeclimate.com/github/bensomers/improved-rack-throttle)
|
5
5
|
[](https://gemnasium.com/bensomers/improved-rack-throttle)
|
6
|
+
[](https://travis-ci.org/bensomers/improved-rack-throttle)
|
6
7
|
|
7
8
|
This is a [Rack][] middleware that provides logic for rate-limiting incoming
|
8
9
|
HTTP requests to Rack applications. You can use `Rack::Throttle` with any
|
@@ -37,7 +38,7 @@ Examples
|
|
37
38
|
|
38
39
|
# config/application.rb
|
39
40
|
require 'rack/throttle'
|
40
|
-
|
41
|
+
|
41
42
|
class Application < Rails::Application
|
42
43
|
config.middleware.use Rack::Throttle::Interval
|
43
44
|
end
|
@@ -47,18 +48,18 @@ Examples
|
|
47
48
|
#!/usr/bin/env ruby -rubygems
|
48
49
|
require 'sinatra'
|
49
50
|
require 'rack/throttle'
|
50
|
-
|
51
|
+
|
51
52
|
use Rack::Throttle::Interval
|
52
|
-
|
53
|
+
|
53
54
|
get('/hello') { "Hello, world!\n" }
|
54
55
|
|
55
56
|
### Adding throttling to a Rackup application
|
56
57
|
|
57
58
|
#!/usr/bin/env rackup
|
58
59
|
require 'rack/throttle'
|
59
|
-
|
60
|
+
|
60
61
|
use Rack::Throttle::Interval
|
61
|
-
|
62
|
+
|
62
63
|
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] }
|
63
64
|
|
64
65
|
### Enforcing a minimum 3-second interval between requests
|
@@ -86,19 +87,19 @@ Examples
|
|
86
87
|
### Storing the rate-limiting counters in a GDBM database
|
87
88
|
|
88
89
|
require 'gdbm'
|
89
|
-
|
90
|
+
|
90
91
|
use Rack::Throttle::Interval, :cache => GDBM.new('tmp/throttle.db')
|
91
92
|
|
92
93
|
### Storing the rate-limiting counters on a Memcached server
|
93
94
|
|
94
95
|
require 'memcached'
|
95
|
-
|
96
|
+
|
96
97
|
use Rack::Throttle::Interval, :cache => Memcached.new, :key_prefix => :throttle
|
97
98
|
|
98
99
|
### Storing the rate-limiting counters on a Redis server
|
99
100
|
|
100
101
|
require 'redis'
|
101
|
-
|
102
|
+
|
102
103
|
use Rack::Throttle::Interval, :cache => Redis.new, :key_prefix => :throttle
|
103
104
|
|
104
105
|
### Scoping the rate-limit to a specific path and method
|
@@ -188,8 +189,6 @@ status code by passing in a `:code => 503` option when constructing a
|
|
188
189
|
|
189
190
|
Documentation
|
190
191
|
-------------
|
191
|
-
UNDER DEVELOPMENT
|
192
|
-
|
193
192
|
<http://rubydoc.info/gems/improved-rack-throttle>
|
194
193
|
|
195
194
|
* {Rack::Throttle}
|
@@ -229,7 +228,7 @@ as follows:
|
|
229
228
|
|
230
229
|
Authors
|
231
230
|
-------
|
232
|
-
* [Ben Somers](mailto:somers.ben@gmail.com) - <http://www.
|
231
|
+
* [Ben Somers](mailto:somers.ben@gmail.com) - <http://www.github.com/bensomers>
|
233
232
|
* [Arto Bendiken](mailto:arto.bendiken@gmail.com) - <http://ar.to/>
|
234
233
|
* [Brendon Murphy](mailto:disposable.20.xternal@spamourmet.com>) - <http://www.techfreak.net/>
|
235
234
|
|
data/Rakefile
CHANGED
@@ -28,8 +28,6 @@ Jeweler::Tasks.new do |gem|
|
|
28
28
|
end
|
29
29
|
Jeweler::RubygemsDotOrgTasks.new
|
30
30
|
|
31
|
-
task :default => :spec
|
32
|
-
|
33
31
|
require 'rdoc/task'
|
34
32
|
Rake::RDocTask.new do |rdoc|
|
35
33
|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
@@ -39,3 +37,7 @@ Rake::RDocTask.new do |rdoc|
|
|
39
37
|
rdoc.rdoc_files.include('README*')
|
40
38
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
41
39
|
end
|
40
|
+
|
41
|
+
require 'rspec/core/rake_task'
|
42
|
+
RSpec::Core::RakeTask.new(:spec)
|
43
|
+
task :default => :spec
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "improved-rack-throttle"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.8.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 = "
|
12
|
+
s.date = "2014-09-22"
|
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 = [
|
@@ -17,6 +17,7 @@ Gem::Specification.new do |s|
|
|
17
17
|
]
|
18
18
|
s.files = [
|
19
19
|
".document",
|
20
|
+
".travis.yml",
|
20
21
|
"Gemfile",
|
21
22
|
"Gemfile.lock",
|
22
23
|
"README.md",
|
@@ -55,37 +56,40 @@ Gem::Specification.new do |s|
|
|
55
56
|
s.homepage = "http://github.com/bensomers/improved-rack-throttle"
|
56
57
|
s.licenses = ["Public Domain"]
|
57
58
|
s.require_paths = ["lib"]
|
58
|
-
s.rubygems_version = "
|
59
|
+
s.rubygems_version = "2.0.3"
|
59
60
|
s.summary = "HTTP request rate limiter for Rack applications."
|
60
61
|
|
61
62
|
if s.respond_to? :specification_version then
|
62
|
-
s.specification_version =
|
63
|
+
s.specification_version = 4
|
63
64
|
|
64
65
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
65
66
|
s.add_runtime_dependency(%q<rack>, [">= 1.0.0"])
|
66
|
-
s.add_development_dependency(%q<timecop>, ["
|
67
|
-
s.add_development_dependency(%q<rack-test>, ["
|
68
|
-
s.add_development_dependency(%q<rspec>, ["
|
69
|
-
s.add_development_dependency(%q<yard>, [">= 0
|
67
|
+
s.add_development_dependency(%q<timecop>, ["<= 0.6.2.2"])
|
68
|
+
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
69
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
70
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
71
|
+
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
70
72
|
s.add_development_dependency(%q<redcarpet>, [">= 0"])
|
71
73
|
s.add_development_dependency(%q<rake>, [">= 0"])
|
72
74
|
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
73
75
|
else
|
74
76
|
s.add_dependency(%q<rack>, [">= 1.0.0"])
|
75
|
-
s.add_dependency(%q<timecop>, ["
|
76
|
-
s.add_dependency(%q<rack-test>, ["
|
77
|
-
s.add_dependency(%q<rspec>, ["
|
78
|
-
s.add_dependency(%q<yard>, [">= 0
|
77
|
+
s.add_dependency(%q<timecop>, ["<= 0.6.2.2"])
|
78
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
79
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
80
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
81
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
79
82
|
s.add_dependency(%q<redcarpet>, [">= 0"])
|
80
83
|
s.add_dependency(%q<rake>, [">= 0"])
|
81
84
|
s.add_dependency(%q<jeweler>, [">= 0"])
|
82
85
|
end
|
83
86
|
else
|
84
87
|
s.add_dependency(%q<rack>, [">= 1.0.0"])
|
85
|
-
s.add_dependency(%q<timecop>, ["
|
86
|
-
s.add_dependency(%q<rack-test>, ["
|
87
|
-
s.add_dependency(%q<rspec>, ["
|
88
|
-
s.add_dependency(%q<yard>, [">= 0
|
88
|
+
s.add_dependency(%q<timecop>, ["<= 0.6.2.2"])
|
89
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
90
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
91
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
92
|
+
s.add_dependency(%q<simplecov>, [">= 0"])
|
89
93
|
s.add_dependency(%q<redcarpet>, [">= 0"])
|
90
94
|
s.add_dependency(%q<rake>, [">= 0"])
|
91
95
|
s.add_dependency(%q<jeweler>, [">= 0"])
|
@@ -23,7 +23,6 @@ module Rack; module Throttle
|
|
23
23
|
def initialize(app, options = {})
|
24
24
|
rules = options.delete(:rules) || {}
|
25
25
|
@app, @options, @matchers = app, options, []
|
26
|
-
@matchers += Array(rules[:ip]).map { |rule| IpMatcher.new(rule) } if rules[:ip]
|
27
26
|
@matchers += Array(rules[:url]).map { |rule| UrlMatcher.new(rule) } if rules[:url]
|
28
27
|
@matchers += Array(rules[:user_agent]).map { |rule| UserAgentMatcher.new(rule) } if rules[:user_agent]
|
29
28
|
@matchers += Array(rules[:method]).map { |rule| MethodMatcher.new(rule) } if rules[:method]
|
@@ -197,7 +196,8 @@ module Rack; module Throttle
|
|
197
196
|
#
|
198
197
|
# @return [Array(Integer, Hash, #each)]
|
199
198
|
def rate_limit_exceeded
|
200
|
-
|
199
|
+
return_retry_after = options[:return_retry_after] || false
|
200
|
+
headers = (respond_to?(:retry_after) && return_retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
|
201
201
|
http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers)
|
202
202
|
end
|
203
203
|
|
@@ -2,8 +2,8 @@ module Rack; module Throttle
|
|
2
2
|
##
|
3
3
|
# This rate limiter strategy throttles the application with
|
4
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
|
5
|
+
# on second-level resolution. It takes :burst and :average
|
6
|
+
# options, which correspond to the maximum size of a traffic
|
7
7
|
# burst, and the maximum allowed average traffic level.
|
8
8
|
class SlidingWindow < Limiter
|
9
9
|
##
|
@@ -18,7 +18,7 @@ module Rack; module Throttle
|
|
18
18
|
end
|
19
19
|
|
20
20
|
##
|
21
|
-
# Returns `true` if the request conforms to the
|
21
|
+
# Returns `true` if the request conforms to the
|
22
22
|
# specified :average and :burst rules
|
23
23
|
#
|
24
24
|
# @param [Rack::Request] request
|
@@ -29,7 +29,7 @@ module Rack; module Throttle
|
|
29
29
|
bucket = cache_get(key) rescue nil
|
30
30
|
bucket ||= LeakyBucket.new(options[:burst], options[:average])
|
31
31
|
bucket.maximum, bucket.outflow = options[:burst], options[:average]
|
32
|
-
bucket.leak!
|
32
|
+
bucket.leak!
|
33
33
|
bucket.increment!
|
34
34
|
allowed = !bucket.full?
|
35
35
|
begin
|
@@ -44,6 +44,15 @@ module Rack; module Throttle
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
##
|
48
|
+
# Returns the number of seconds before the client is allowed to retry an
|
49
|
+
# HTTP request.
|
50
|
+
#
|
51
|
+
# @return [Float]
|
52
|
+
def retry_after
|
53
|
+
@retry_after ||= (1.0 / options[:average].to_f)
|
54
|
+
end
|
55
|
+
|
47
56
|
###
|
48
57
|
# LeakyBucket is an internal class used to implement the
|
49
58
|
# SlidingWindow limiter strategy. It is a (slightly tweaked)
|
@@ -67,7 +76,7 @@ module Rack; module Throttle
|
|
67
76
|
loss = (outflow * time).to_f
|
68
77
|
if loss > 0
|
69
78
|
@count -= loss
|
70
|
-
@last_touched = t
|
79
|
+
@last_touched = t
|
71
80
|
end
|
72
81
|
end
|
73
82
|
|
@@ -4,7 +4,7 @@ module Rack; module Throttle
|
|
4
4
|
# requested. For instance, you may care about limiting requests
|
5
5
|
# to a machine-consumed API, but not be concerned about requests
|
6
6
|
# coming from browsers.
|
7
|
-
# UrlMatchers take Regexp object to
|
7
|
+
# UrlMatchers take Regexp object to match against the request path.
|
8
8
|
class UrlMatcher < Matcher
|
9
9
|
###
|
10
10
|
# @param [Rack::Request] request
|
data/spec/limiters/daily_spec.rb
CHANGED
@@ -10,18 +10,18 @@ describe Rack::Throttle::Daily do
|
|
10
10
|
|
11
11
|
it "should be allowed if not seen this day" do
|
12
12
|
get "/foo"
|
13
|
-
last_response.body.
|
13
|
+
expect(last_response.body).to show_allowed_response
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
it "should be allowed if seen fewer than the max allowed per day" do
|
17
17
|
2.times { get "/foo" }
|
18
|
-
last_response.body.
|
18
|
+
expect(last_response.body).to show_allowed_response
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
it "should not be allowed if seen more times than the max allowed per day" do
|
22
22
|
4.times { get "/foo" }
|
23
|
-
last_response.body.
|
23
|
+
expect(last_response.body).to show_throttled_response
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# TODO mess with time travelling and requests to make sure no overlap
|
27
27
|
end
|
@@ -10,18 +10,18 @@ describe Rack::Throttle::Hourly do
|
|
10
10
|
|
11
11
|
it "should be allowed if not seen this hour" do
|
12
12
|
get "/foo"
|
13
|
-
last_response.body.
|
13
|
+
expect(last_response.body).to show_allowed_response
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
it "should be allowed if seen fewer than the max allowed per hour" do
|
17
17
|
2.times { get "/foo" }
|
18
|
-
last_response.body.
|
18
|
+
expect(last_response.body).to show_allowed_response
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
it "should not be allowed if seen more times than the max allowed per hour" do
|
22
22
|
4.times { get "/foo" }
|
23
|
-
last_response.body.
|
23
|
+
expect(last_response.body).to show_throttled_response
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# TODO mess with time travelling and requests to make sure no overlap
|
27
27
|
end
|
@@ -3,7 +3,7 @@ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
|
3
3
|
|
4
4
|
describe Rack::Throttle::Interval do
|
5
5
|
include Rack::Test::Methods
|
6
|
-
|
6
|
+
|
7
7
|
def app
|
8
8
|
@target_app ||= example_target_app
|
9
9
|
@app ||= Rack::Throttle::Interval.new(@target_app, :min => 0.1)
|
@@ -11,9 +11,9 @@ describe Rack::Throttle::Interval do
|
|
11
11
|
|
12
12
|
it "should allow the request if the source has not been seen" do
|
13
13
|
get "/foo"
|
14
|
-
last_response.body.
|
14
|
+
expect(last_response.body).to show_allowed_response
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should allow the request if the source has not been seen in the current interval" do
|
18
18
|
Timecop.freeze do
|
19
19
|
get "/foo"
|
@@ -21,25 +21,25 @@ describe Rack::Throttle::Interval do
|
|
21
21
|
get "/foo"
|
22
22
|
end
|
23
23
|
end
|
24
|
-
last_response.body.
|
24
|
+
expect(last_response.body).to show_allowed_response
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
it "should not allow the request if the source has been seen inside the current interval" do
|
28
28
|
Timecop.freeze do
|
29
29
|
2.times { get "/foo" }
|
30
30
|
end
|
31
|
-
last_response.body.
|
31
|
+
expect(last_response.body).to show_throttled_response
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
it "should gracefully allow the request if the cache bombs on getting" do
|
35
|
-
app.
|
35
|
+
expect(app).to receive(:cache_get).and_raise(StandardError)
|
36
36
|
get "/foo"
|
37
|
-
last_response.body.
|
37
|
+
expect(last_response.body).to show_allowed_response
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
it "should gracefully allow the request if the cache bombs on setting" do
|
41
|
-
app.
|
41
|
+
expect(app).to receive(:cache_set).and_raise(StandardError)
|
42
42
|
get "/foo"
|
43
|
-
last_response.body.
|
43
|
+
expect(last_response.body).to show_allowed_response
|
44
44
|
end
|
45
45
|
end
|
@@ -7,44 +7,44 @@ describe Rack::Throttle::Limiter do
|
|
7
7
|
@target_app ||= example_target_app
|
8
8
|
@app ||= Rack::Throttle::Limiter.new(@target_app)
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
describe "basic calling" do
|
12
12
|
it "should return the example app" do
|
13
13
|
get "/foo"
|
14
|
-
last_response.body.
|
14
|
+
expect(last_response.body).to show_allowed_response
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should call the application if allowed" do
|
18
|
-
app.
|
18
|
+
expect(app).to receive(:allowed?).and_return(true)
|
19
19
|
get "/foo"
|
20
|
-
last_response.body.
|
20
|
+
expect(last_response.body).to show_allowed_response
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
it "should give a rate limit exceeded message if not allowed" do
|
24
|
-
app.
|
24
|
+
expect(app).to receive(:allowed?).and_return(false)
|
25
25
|
get "/foo"
|
26
|
-
last_response.body.
|
26
|
+
expect(last_response.body).to show_throttled_response
|
27
27
|
end
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
describe "allowed?" do
|
31
31
|
it "should return true if whitelisted" do
|
32
|
-
app.
|
32
|
+
expect(app).to receive(:whitelisted?).and_return(true)
|
33
33
|
get "/foo"
|
34
|
-
last_response.body.
|
34
|
+
expect(last_response.body).to show_allowed_response
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
it "should return false if blacklisted" do
|
38
|
-
app.
|
38
|
+
expect(app).to receive(:blacklisted?).and_return(true)
|
39
39
|
get "/foo"
|
40
|
-
last_response.body.
|
40
|
+
expect(last_response.body).to show_throttled_response
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
it "should return true if not whitelisted or blacklisted" do
|
44
|
-
app.
|
45
|
-
app.
|
44
|
+
expect(app).to receive(:whitelisted?).and_return(false)
|
45
|
+
expect(app).to receive(:blacklisted?).and_return(false)
|
46
46
|
get "/foo"
|
47
|
-
last_response.body.
|
47
|
+
expect(last_response.body).to show_allowed_response
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -19,28 +19,28 @@ describe Rack::Throttle::SlidingWindow do
|
|
19
19
|
|
20
20
|
it "should allow the request if the source has not been seen at all" do
|
21
21
|
get "/foo"
|
22
|
-
last_response.body.
|
22
|
+
expect(last_response.body).to show_allowed_response
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
it "should allow the request if the rate is above-average but within the burst rule" do
|
26
26
|
Timecop.freeze(@time) { get "/foo" }
|
27
27
|
Timecop.freeze(@time + 0.5) { get "/foo" }
|
28
|
-
last_response.body.
|
28
|
+
expect(last_response.body).to show_allowed_response
|
29
29
|
end
|
30
30
|
|
31
31
|
it "should not allow the request if the rate is greater than the burst rule" do
|
32
32
|
Timecop.freeze(@time) { get "/foo" }
|
33
33
|
Timecop.freeze(@time + 0.3) { get "/foo" }
|
34
34
|
Timecop.freeze(@time + 0.6) { get "/foo" }
|
35
|
-
last_response.body.
|
35
|
+
expect(last_response.body).to show_throttled_response
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
it "should allow the request if the rate is less than the average" do
|
39
39
|
Timecop.freeze(@time) { get "/foo" }
|
40
40
|
Timecop.freeze(@time + 0.5) { get "/foo" }
|
41
41
|
Timecop.freeze(@time + 2) { get "/foo" }
|
42
42
|
|
43
|
-
last_response.body.
|
43
|
+
expect(last_response.body).to show_allowed_response
|
44
44
|
end
|
45
45
|
|
46
46
|
it "should not allow the request if the rate is more than the average" do
|
@@ -49,19 +49,19 @@ describe Rack::Throttle::SlidingWindow do
|
|
49
49
|
Timecop.freeze(@time + 1) { get "/foo" }
|
50
50
|
Timecop.freeze(@time + 1.5) { get "/foo" }
|
51
51
|
|
52
|
-
last_response.body.
|
52
|
+
expect(last_response.body).to show_throttled_response
|
53
53
|
end
|
54
54
|
|
55
55
|
it "should gracefully allow the request if the cache bombs on getting" do
|
56
|
-
app.
|
56
|
+
expect(app).to receive(:cache_get).and_raise(StandardError)
|
57
57
|
get "/foo"
|
58
|
-
last_response.body.
|
58
|
+
expect(last_response.body).to show_allowed_response
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
it "should gracefully allow the request if the cache bombs on setting" do
|
62
|
-
app.
|
62
|
+
expect(app).to receive(:cache_set).and_raise(StandardError)
|
63
63
|
get "/foo"
|
64
|
-
last_response.body.
|
64
|
+
expect(last_response.body).to show_allowed_response
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -9,19 +9,19 @@ describe Rack::Throttle::MethodMatcher do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should not bother checking if the path doesn't match the rule" do
|
12
|
-
app.
|
12
|
+
expect(app).not_to receive(:allowed?)
|
13
13
|
get "/foo"
|
14
|
-
last_response.body.
|
14
|
+
expect(last_response.body).to show_allowed_response
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should check if the path matches the rule" do
|
18
|
-
app.
|
18
|
+
expect(app).to receive(:allowed?).and_return(false)
|
19
19
|
post "/foo"
|
20
|
-
last_response.body.
|
20
|
+
expect(last_response.body).to show_throttled_response
|
21
21
|
end
|
22
22
|
|
23
23
|
it "should append the rule to the cache key" do
|
24
24
|
post "/foo"
|
25
|
-
app.send(:cache_key, last_request).
|
25
|
+
expect(app.send(:cache_key, last_request)).to eq "127.0.0.1:meth-post"
|
26
26
|
end
|
27
27
|
end
|
@@ -9,20 +9,20 @@ describe Rack::Throttle::UrlMatcher do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should not bother checking if the path doesn't match the rule" do
|
12
|
-
app.
|
12
|
+
expect(app).not_to receive(:allowed?)
|
13
13
|
get "/bar"
|
14
|
-
last_response.body.
|
14
|
+
expect(last_response.body).to show_allowed_response
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "should check if the path matches the rule" do
|
18
|
-
app.
|
18
|
+
expect(app).to receive(:allowed?).and_return(false)
|
19
19
|
get "/foo"
|
20
|
-
last_response.body.
|
20
|
+
expect(last_response.body).to show_throttled_response
|
21
21
|
end
|
22
22
|
|
23
23
|
it "should append the rule to the cache key" do
|
24
24
|
get "/foo"
|
25
|
-
app.send(:cache_key, last_request).
|
25
|
+
expect(app.send(:cache_key, last_request)).to eq "127.0.0.1:url-/foo/"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -9,20 +9,20 @@ describe Rack::Throttle::UserAgentMatcher do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should not bother checking if the path doesn't match the rule" do
|
12
|
-
app.
|
12
|
+
expect(app).not_to receive(:allowed?)
|
13
13
|
get "/foo"
|
14
|
-
last_response.body.
|
14
|
+
expect(last_response.body).to show_allowed_response
|
15
15
|
end
|
16
16
|
|
17
17
|
it "should check if the path matches the rule" do
|
18
|
-
app.
|
18
|
+
expect(app).to receive(:allowed?).and_return(false)
|
19
19
|
header 'User-Agent', 'blahdeblah GoogleBot owns your soul'
|
20
20
|
get "/foo"
|
21
|
-
last_response.body.
|
21
|
+
expect(last_response.body).to show_throttled_response
|
22
22
|
end
|
23
23
|
|
24
24
|
it "should append the rule to the cache key" do
|
25
25
|
get "/foo"
|
26
|
-
app.send(:cache_key, last_request).
|
26
|
+
expect(app.send(:cache_key, last_request)).to eq "127.0.0.1:ua-/google/i"
|
27
27
|
end
|
28
28
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,9 +3,14 @@ require "rack/test"
|
|
3
3
|
require "rack/throttle"
|
4
4
|
require "timecop"
|
5
5
|
|
6
|
+
unless RUBY_VERSION.match(/1\.8/)
|
7
|
+
require 'simplecov'
|
8
|
+
SimpleCov.start
|
9
|
+
end
|
10
|
+
|
6
11
|
def example_target_app
|
7
12
|
@target_app = double("Example Rack App")
|
8
|
-
@target_app.
|
13
|
+
allow(@target_app).to receive(:call).with(any_args()).and_return([200, {}, "Example App Body"])
|
9
14
|
@target_app
|
10
15
|
end
|
11
16
|
|
@@ -13,34 +18,34 @@ RSpec::Matchers.define :show_allowed_response do
|
|
13
18
|
match do |body|
|
14
19
|
body.include?("Example App Body")
|
15
20
|
end
|
16
|
-
|
17
|
-
failure_message_for_should do
|
18
|
-
"expected response to show the allowed response"
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
"expected response
|
22
|
+
failure_message do
|
23
|
+
"expected response to show the allowed response"
|
23
24
|
end
|
24
|
-
|
25
|
+
|
26
|
+
failure_message_when_negated do
|
27
|
+
"expected response not to show the allowed response"
|
28
|
+
end
|
29
|
+
|
25
30
|
description do
|
26
31
|
"expected the allowed response"
|
27
|
-
end
|
32
|
+
end
|
28
33
|
end
|
29
34
|
|
30
35
|
RSpec::Matchers.define :show_throttled_response do
|
31
36
|
match do |body|
|
32
37
|
body.include?("Rate Limit Exceeded")
|
33
38
|
end
|
34
|
-
|
35
|
-
failure_message_for_should do
|
36
|
-
"expected response to show the throttled response"
|
37
|
-
end
|
38
39
|
|
39
|
-
|
40
|
-
"expected response
|
40
|
+
failure_message do
|
41
|
+
"expected response to show the throttled response"
|
42
|
+
end
|
43
|
+
|
44
|
+
failure_message do
|
45
|
+
"expected response not to show the throttled response"
|
41
46
|
end
|
42
|
-
|
47
|
+
|
43
48
|
description do
|
44
49
|
"expected the throttled response"
|
45
|
-
end
|
50
|
+
end
|
46
51
|
end
|
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: improved-rack-throttle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.8.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Ben Somers
|
@@ -11,134 +10,132 @@ authors:
|
|
11
10
|
autorequire:
|
12
11
|
bindir: bin
|
13
12
|
cert_chain: []
|
14
|
-
date:
|
13
|
+
date: 2014-09-22 00:00:00.000000000 Z
|
15
14
|
dependencies:
|
16
15
|
- !ruby/object:Gem::Dependency
|
17
16
|
name: rack
|
18
17
|
requirement: !ruby/object:Gem::Requirement
|
19
|
-
none: false
|
20
18
|
requirements:
|
21
|
-
- -
|
19
|
+
- - '>='
|
22
20
|
- !ruby/object:Gem::Version
|
23
21
|
version: 1.0.0
|
24
22
|
type: :runtime
|
25
23
|
prerelease: false
|
26
24
|
version_requirements: !ruby/object:Gem::Requirement
|
27
|
-
none: false
|
28
25
|
requirements:
|
29
|
-
- -
|
26
|
+
- - '>='
|
30
27
|
- !ruby/object:Gem::Version
|
31
28
|
version: 1.0.0
|
32
29
|
- !ruby/object:Gem::Dependency
|
33
30
|
name: timecop
|
34
31
|
requirement: !ruby/object:Gem::Requirement
|
35
|
-
none: false
|
36
32
|
requirements:
|
37
|
-
- -
|
33
|
+
- - <=
|
38
34
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.
|
35
|
+
version: 0.6.2.2
|
40
36
|
type: :development
|
41
37
|
prerelease: false
|
42
38
|
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
none: false
|
44
39
|
requirements:
|
45
|
-
- -
|
40
|
+
- - <=
|
46
41
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.
|
42
|
+
version: 0.6.2.2
|
48
43
|
- !ruby/object:Gem::Dependency
|
49
44
|
name: rack-test
|
50
45
|
requirement: !ruby/object:Gem::Requirement
|
51
|
-
none: false
|
52
46
|
requirements:
|
53
|
-
- -
|
47
|
+
- - '>='
|
54
48
|
- !ruby/object:Gem::Version
|
55
|
-
version: 0
|
49
|
+
version: '0'
|
56
50
|
type: :development
|
57
51
|
prerelease: false
|
58
52
|
version_requirements: !ruby/object:Gem::Requirement
|
59
|
-
none: false
|
60
53
|
requirements:
|
61
|
-
- -
|
54
|
+
- - '>='
|
62
55
|
- !ruby/object:Gem::Version
|
63
|
-
version: 0
|
56
|
+
version: '0'
|
64
57
|
- !ruby/object:Gem::Dependency
|
65
58
|
name: rspec
|
66
59
|
requirement: !ruby/object:Gem::Requirement
|
67
|
-
none: false
|
68
60
|
requirements:
|
69
|
-
- -
|
61
|
+
- - '>='
|
70
62
|
- !ruby/object:Gem::Version
|
71
|
-
version:
|
63
|
+
version: '0'
|
72
64
|
type: :development
|
73
65
|
prerelease: false
|
74
66
|
version_requirements: !ruby/object:Gem::Requirement
|
75
|
-
none: false
|
76
67
|
requirements:
|
77
|
-
- -
|
68
|
+
- - '>='
|
78
69
|
- !ruby/object:Gem::Version
|
79
|
-
version:
|
70
|
+
version: '0'
|
80
71
|
- !ruby/object:Gem::Dependency
|
81
72
|
name: yard
|
82
73
|
requirement: !ruby/object:Gem::Requirement
|
83
|
-
none: false
|
84
74
|
requirements:
|
85
|
-
- -
|
75
|
+
- - '>='
|
86
76
|
- !ruby/object:Gem::Version
|
87
|
-
version: 0
|
77
|
+
version: '0'
|
88
78
|
type: :development
|
89
79
|
prerelease: false
|
90
80
|
version_requirements: !ruby/object:Gem::Requirement
|
91
|
-
none: false
|
92
81
|
requirements:
|
93
|
-
- -
|
82
|
+
- - '>='
|
94
83
|
- !ruby/object:Gem::Version
|
95
|
-
version: 0
|
84
|
+
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: simplecov
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
96
99
|
- !ruby/object:Gem::Dependency
|
97
100
|
name: redcarpet
|
98
101
|
requirement: !ruby/object:Gem::Requirement
|
99
|
-
none: false
|
100
102
|
requirements:
|
101
|
-
- -
|
103
|
+
- - '>='
|
102
104
|
- !ruby/object:Gem::Version
|
103
105
|
version: '0'
|
104
106
|
type: :development
|
105
107
|
prerelease: false
|
106
108
|
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
none: false
|
108
109
|
requirements:
|
109
|
-
- -
|
110
|
+
- - '>='
|
110
111
|
- !ruby/object:Gem::Version
|
111
112
|
version: '0'
|
112
113
|
- !ruby/object:Gem::Dependency
|
113
114
|
name: rake
|
114
115
|
requirement: !ruby/object:Gem::Requirement
|
115
|
-
none: false
|
116
116
|
requirements:
|
117
|
-
- -
|
117
|
+
- - '>='
|
118
118
|
- !ruby/object:Gem::Version
|
119
119
|
version: '0'
|
120
120
|
type: :development
|
121
121
|
prerelease: false
|
122
122
|
version_requirements: !ruby/object:Gem::Requirement
|
123
|
-
none: false
|
124
123
|
requirements:
|
125
|
-
- -
|
124
|
+
- - '>='
|
126
125
|
- !ruby/object:Gem::Version
|
127
126
|
version: '0'
|
128
127
|
- !ruby/object:Gem::Dependency
|
129
128
|
name: jeweler
|
130
129
|
requirement: !ruby/object:Gem::Requirement
|
131
|
-
none: false
|
132
130
|
requirements:
|
133
|
-
- -
|
131
|
+
- - '>='
|
134
132
|
- !ruby/object:Gem::Version
|
135
133
|
version: '0'
|
136
134
|
type: :development
|
137
135
|
prerelease: false
|
138
136
|
version_requirements: !ruby/object:Gem::Requirement
|
139
|
-
none: false
|
140
137
|
requirements:
|
141
|
-
- -
|
138
|
+
- - '>='
|
142
139
|
- !ruby/object:Gem::Version
|
143
140
|
version: '0'
|
144
141
|
description: Rack middleware for rate-limiting incoming HTTP requests.
|
@@ -149,6 +146,7 @@ extra_rdoc_files:
|
|
149
146
|
- README.md
|
150
147
|
files:
|
151
148
|
- .document
|
149
|
+
- .travis.yml
|
152
150
|
- Gemfile
|
153
151
|
- Gemfile.lock
|
154
152
|
- README.md
|
@@ -186,29 +184,26 @@ files:
|
|
186
184
|
homepage: http://github.com/bensomers/improved-rack-throttle
|
187
185
|
licenses:
|
188
186
|
- Public Domain
|
187
|
+
metadata: {}
|
189
188
|
post_install_message:
|
190
189
|
rdoc_options: []
|
191
190
|
require_paths:
|
192
191
|
- lib
|
193
192
|
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
-
none: false
|
195
193
|
requirements:
|
196
|
-
- -
|
194
|
+
- - '>='
|
197
195
|
- !ruby/object:Gem::Version
|
198
196
|
version: '0'
|
199
|
-
segments:
|
200
|
-
- 0
|
201
|
-
hash: -4496223408765979053
|
202
197
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
|
-
none: false
|
204
198
|
requirements:
|
205
|
-
- -
|
199
|
+
- - '>='
|
206
200
|
- !ruby/object:Gem::Version
|
207
201
|
version: '0'
|
208
202
|
requirements: []
|
209
203
|
rubyforge_project:
|
210
|
-
rubygems_version:
|
204
|
+
rubygems_version: 2.0.3
|
211
205
|
signing_key:
|
212
|
-
specification_version:
|
206
|
+
specification_version: 4
|
213
207
|
summary: HTTP request rate limiter for Rack applications.
|
214
208
|
test_files: []
|
209
|
+
has_rdoc:
|