rack-throttle 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/AUTHORS +1 -2
- data/README +61 -31
- data/VERSION +1 -1
- data/lib/rack/throttle.rb +1 -0
- data/lib/rack/throttle/limiter.rb +11 -9
- data/lib/rack/throttle/minute.rb +43 -0
- data/lib/rack/throttle/time_window.rb +2 -1
- metadata +84 -88
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MDNkYTliNDNmN2ExYWQ1OWY3NjkyZDNkZTk0OTQ0ZGZiODk1ZWFiNw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ODdlYzYyOGQwYzY2OGZiNDgyNzc4NWY3OTEzNDc2ZDFlYzZhZjE1ZA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NjUxNmJmMTBlNWU1MDk3ZjM2Zjc1YmEzNWJlYTYyYzBlMWQxNDFiOTgzMGMx
|
10
|
+
YjM1OGQ5Mzk1ZjRlZDQxYzQxZjNiZDdlN2IzMjM2NDU3ZGRjZWUwZGJiYmQw
|
11
|
+
ZDQ3NGYxNDNhZGJhZmQ3MzcyZGU4ZDgwYTg5OTE1NmNmYzZhMDg=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OTRmNWNlN2VkYWU0ODAyMTAzMmRjMmE0ZTllMmE3OTliODA5MmFkMTQxOGRm
|
14
|
+
YjUzOTc0MTNlZjliZmZhN2Q2NWMyNzliYTk1MGMyMTJhYTliMDE5M2I1ZDdh
|
15
|
+
NTRmMWNlYzEyMDhiOWNhYzY0YmFhZGFiZWQxNjI0ZmRjMWE3Nzk=
|
data/AUTHORS
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
* Arto Bendiken <arto
|
2
|
-
* Brendon Murphy <disposable.20.xternal@spamourmet.com>
|
1
|
+
* Arto Bendiken <arto@bendiken.net>
|
data/README
CHANGED
@@ -3,18 +3,18 @@ HTTP Request Rate Limiter for Rack Applications
|
|
3
3
|
|
4
4
|
This is [Rack][] middleware that provides logic for rate-limiting incoming
|
5
5
|
HTTP requests to Rack applications. You can use `Rack::Throttle` with any
|
6
|
-
Ruby web framework based on Rack, including with Ruby on Rails
|
6
|
+
Ruby web framework based on Rack, including with Ruby on Rails and with
|
7
7
|
Sinatra.
|
8
8
|
|
9
|
-
* <
|
9
|
+
* <https://github.com/bendiken/rack-throttle>
|
10
10
|
|
11
11
|
Features
|
12
12
|
--------
|
13
13
|
|
14
14
|
* Throttles a Rack application by enforcing a minimum time interval between
|
15
15
|
subsequent HTTP requests from a particular client, as well as by defining
|
16
|
-
a maximum number of allowed HTTP requests per a given time period (
|
17
|
-
or daily).
|
16
|
+
a maximum number of allowed HTTP requests per a given time period (per minute,
|
17
|
+
hourly, or daily).
|
18
18
|
* Compatible with any Rack application and any Rack-based framework.
|
19
19
|
* Stores rate-limiting counters in any key/value store implementation that
|
20
20
|
responds to `#[]`/`#[]=` (like Ruby's hashes) or to `#get`/`#set` (like
|
@@ -28,18 +28,42 @@ Features
|
|
28
28
|
Examples
|
29
29
|
--------
|
30
30
|
|
31
|
-
### Adding throttling to a
|
31
|
+
### Adding throttling to a Rails application
|
32
32
|
|
33
|
+
# config/application.rb
|
33
34
|
require 'rack/throttle'
|
35
|
+
|
36
|
+
class Application < Rails::Application
|
37
|
+
config.middleware.use Rack::Throttle::Interval
|
38
|
+
end
|
34
39
|
|
40
|
+
### Adding throttling to a Sinatra application
|
41
|
+
|
42
|
+
#!/usr/bin/env ruby -rubygems
|
43
|
+
require 'sinatra'
|
44
|
+
require 'rack/throttle'
|
45
|
+
|
35
46
|
use Rack::Throttle::Interval
|
47
|
+
|
48
|
+
get('/hello') { "Hello, world!\n" }
|
36
49
|
|
50
|
+
### Adding throttling to a Rackup application
|
51
|
+
|
52
|
+
#!/usr/bin/env rackup
|
53
|
+
require 'rack/throttle'
|
54
|
+
|
55
|
+
use Rack::Throttle::Interval
|
56
|
+
|
37
57
|
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] }
|
38
58
|
|
39
59
|
### Enforcing a minimum 3-second interval between requests
|
40
60
|
|
41
61
|
use Rack::Throttle::Interval, :min => 3.0
|
42
62
|
|
63
|
+
### Allowing a maximum of 60 requests per minute
|
64
|
+
|
65
|
+
use Rack::Throttle::Minute, :max => 60
|
66
|
+
|
43
67
|
### Allowing a maximum of 100 requests per hour
|
44
68
|
|
45
69
|
use Rack::Throttle::Hourly, :max => 100
|
@@ -52,6 +76,7 @@ Examples
|
|
52
76
|
|
53
77
|
use Rack::Throttle::Daily, :max => 1000 # requests
|
54
78
|
use Rack::Throttle::Hourly, :max => 100 # requests
|
79
|
+
use Rack::Throttle::Minute, :max => 60 # requests
|
55
80
|
use Rack::Throttle::Interval, :min => 3.0 # seconds
|
56
81
|
|
57
82
|
### Storing the rate-limiting counters in a GDBM database
|
@@ -136,16 +161,6 @@ otherwise, `Rack::Throttle` does allow you to override the returned HTTP
|
|
136
161
|
status code by passing in a `:code => 503` option when constructing a
|
137
162
|
`Rack::Throttle::Limiter` instance.
|
138
163
|
|
139
|
-
Documentation
|
140
|
-
-------------
|
141
|
-
|
142
|
-
<http://datagraph.rubyforge.org/rack-throttle/>
|
143
|
-
|
144
|
-
* {Rack::Throttle}
|
145
|
-
* {Rack::Throttle::Interval}
|
146
|
-
* {Rack::Throttle::Daily}
|
147
|
-
* {Rack::Throttle::Hourly}
|
148
|
-
|
149
164
|
Dependencies
|
150
165
|
------------
|
151
166
|
|
@@ -154,34 +169,47 @@ Dependencies
|
|
154
169
|
Installation
|
155
170
|
------------
|
156
171
|
|
157
|
-
The recommended installation method is via RubyGems.
|
158
|
-
official release, do:
|
172
|
+
The recommended installation method is via [RubyGems](http://rubygems.org/).
|
173
|
+
To install the latest official release of the gem, do:
|
159
174
|
|
160
175
|
% [sudo] gem install rack-throttle
|
161
176
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
To get a local working copy of the development repository, do:
|
177
|
+
Authors
|
178
|
+
-------
|
166
179
|
|
167
|
-
|
180
|
+
* [Arto Bendiken](https://gratipay.com/bendiken) - <http://ar.to/>
|
168
181
|
|
169
|
-
|
170
|
-
|
182
|
+
Contributors
|
183
|
+
------------
|
171
184
|
|
172
|
-
|
185
|
+
* [Brendon Murphy](https://github.com/bemurphy)
|
186
|
+
* [Hendrik Kleinwaechter](https://github.com/hendricius)
|
187
|
+
* [Karel Minarik](https://github.com/karmi)
|
188
|
+
* [Keita Urashima](https://github.com/ursm)
|
189
|
+
* [Leonid Beder](https://github.com/lbeder)
|
190
|
+
* [TJ Singleton](https://github.com/tjsingleton)
|
191
|
+
* [Winfield Peterson](https://github.com/wpeterson)
|
173
192
|
|
174
|
-
|
175
|
-
|
193
|
+
Contributing
|
194
|
+
------------
|
176
195
|
|
177
|
-
*
|
178
|
-
*
|
196
|
+
* Do your best to adhere to the existing coding conventions and idioms.
|
197
|
+
* Don't use hard tabs, and don't leave trailing whitespace on any line.
|
198
|
+
Before committing, run `git diff --check` to make sure of this.
|
199
|
+
* Do document every method you add using [YARD][] annotations. Read the
|
200
|
+
[tutorial][YARD-GS] or just look at the existing code for examples.
|
201
|
+
* Don't touch the gemspec or `VERSION` files. If you need to change them,
|
202
|
+
do so on your private branch only.
|
203
|
+
* Do feel free to add yourself to the `CREDITS` file and the
|
204
|
+
corresponding list in the the `README`. Alphabetical order applies.
|
205
|
+
* Don't touch the `AUTHORS` file. If your contributions are significant
|
206
|
+
enough, be assured we will eventually add you in there.
|
179
207
|
|
180
208
|
License
|
181
209
|
-------
|
182
210
|
|
183
|
-
|
184
|
-
|
211
|
+
This is free and unencumbered public domain software. For more information,
|
212
|
+
see <http://unlicense.org/> or the accompanying `UNLICENSE` file.
|
185
213
|
|
186
214
|
[Rack]: http://rack.rubyforge.org/
|
187
215
|
[gdbm]: http://ruby-doc.org/stdlib/libdoc/gdbm/rdoc/classes/GDBM.html
|
@@ -191,3 +219,5 @@ information, see <http://unlicense.org/> or the accompanying UNLICENSE file.
|
|
191
219
|
[redis]: http://rubygems.org/gems/redis
|
192
220
|
[Heroku]: http://heroku.com/
|
193
221
|
[Heroku memcache]: http://docs.heroku.com/memcache
|
222
|
+
[YARD]: http://yardoc.org/
|
223
|
+
[YARD-GS]: http://rubydoc.info/docs/yard/file/docs/GettingStarted.md
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/rack/throttle.rb
CHANGED
@@ -31,7 +31,7 @@ module Rack; module Throttle
|
|
31
31
|
# @see http://rack.rubyforge.org/doc/SPEC.html
|
32
32
|
def call(env)
|
33
33
|
request = Rack::Request.new(env)
|
34
|
-
allowed?(request) ? app.call(env) : rate_limit_exceeded
|
34
|
+
allowed?(request) ? app.call(env) : rate_limit_exceeded(request)
|
35
35
|
end
|
36
36
|
|
37
37
|
##
|
@@ -124,7 +124,7 @@ module Rack; module Throttle
|
|
124
124
|
when cache.respond_to?(:[]=)
|
125
125
|
begin
|
126
126
|
cache[key] = value
|
127
|
-
rescue TypeError
|
127
|
+
rescue TypeError
|
128
128
|
# GDBM throws a "TypeError: can't convert Float into String"
|
129
129
|
# exception when trying to store a Float. On the other hand, we
|
130
130
|
# don't want to unnecessarily coerce the value to a String for
|
@@ -162,11 +162,12 @@ module Rack; module Throttle
|
|
162
162
|
# @param [Rack::Request] request
|
163
163
|
# @return [Float]
|
164
164
|
def request_start_time(request)
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
165
|
+
# Check whether HTTP_X_REQUEST_START or HTTP_X_QUEUE_START exist and parse its value (for
|
166
|
+
# example, when having nginx in your stack, it's going to be in the "t=\d+" format).
|
167
|
+
if val = (request.env['HTTP_X_REQUEST_START'] || request.env['HTTP_X_QUEUE_START'])
|
168
|
+
val[/(?:^t=)?(\d+)/, 1].to_f / 1000
|
169
|
+
else
|
170
|
+
Time.now.to_f
|
170
171
|
end
|
171
172
|
end
|
172
173
|
|
@@ -174,7 +175,8 @@ module Rack; module Throttle
|
|
174
175
|
# Outputs a `Rate Limit Exceeded` error.
|
175
176
|
#
|
176
177
|
# @return [Array(Integer, Hash, #each)]
|
177
|
-
def rate_limit_exceeded
|
178
|
+
def rate_limit_exceeded(request)
|
179
|
+
options[:rate_limit_exceeded_callback].call(request) if options[:rate_limit_exceeded_callback]
|
178
180
|
headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
|
179
181
|
http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers)
|
180
182
|
end
|
@@ -188,7 +190,7 @@ module Rack; module Throttle
|
|
188
190
|
# @return [Array(Integer, Hash, #each)]
|
189
191
|
def http_error(code, message = nil, headers = {})
|
190
192
|
[code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers),
|
191
|
-
http_status(code)
|
193
|
+
[http_status(code), (message.nil? ? "\n" : " (#{message})\n")]]
|
192
194
|
end
|
193
195
|
|
194
196
|
##
|
@@ -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.
|
11
|
+
#
|
12
|
+
# @example Allowing up to 60 requests/minute
|
13
|
+
# use Rack::Throttle::Minute
|
14
|
+
#
|
15
|
+
# @example Allowing up to 100 requests per hour
|
16
|
+
# use Rack::Throttle::Minute, :max => 100
|
17
|
+
#
|
18
|
+
class Minute < TimeWindow
|
19
|
+
##
|
20
|
+
# @param [#call] app
|
21
|
+
# @param [Hash{Symbol => Object}] options
|
22
|
+
# @option options [Integer] :max (60)
|
23
|
+
def initialize(app, options = {})
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
def max_per_minute
|
29
|
+
@max_per_hour ||= 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
|
@@ -8,8 +8,9 @@ module Rack; module Throttle
|
|
8
8
|
# @param [Rack::Request] request
|
9
9
|
# @return [Boolean]
|
10
10
|
def allowed?(request)
|
11
|
+
return true if whitelisted?(request)
|
11
12
|
count = cache_get(key = cache_key(request)).to_i + 1 rescue 1
|
12
|
-
allowed = count <= max_per_window
|
13
|
+
allowed = count <= max_per_window.to_i
|
13
14
|
begin
|
14
15
|
cache_set(key, count)
|
15
16
|
allowed
|
metadata
CHANGED
@@ -1,88 +1,91 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-throttle
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
segments:
|
6
|
-
- 0
|
7
|
-
- 3
|
8
|
-
- 0
|
9
|
-
version: 0.3.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
10
5
|
platform: ruby
|
11
|
-
authors:
|
6
|
+
authors:
|
12
7
|
- Arto Bendiken
|
13
|
-
- Brendon Murphy
|
14
8
|
autorequire:
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
11
|
+
date: 2014-11-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
22
14
|
name: rack-test
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 0
|
30
|
-
- 5
|
31
|
-
- 3
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
32
19
|
version: 0.5.3
|
33
20
|
type: :development
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: rspec
|
37
21
|
prerelease: false
|
38
|
-
|
39
|
-
requirements:
|
40
|
-
- -
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
46
33
|
version: 1.3.0
|
47
34
|
type: :development
|
48
|
-
|
49
|
-
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
50
42
|
name: yard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.5.5
|
48
|
+
type: :development
|
51
49
|
prerelease: false
|
52
|
-
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.5
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.4
|
61
62
|
type: :development
|
62
|
-
version_requirements: *id003
|
63
|
-
- !ruby/object:Gem::Dependency
|
64
|
-
name: rack
|
65
63
|
prerelease: false
|
66
|
-
|
67
|
-
requirements:
|
68
|
-
- -
|
69
|
-
- !ruby/object:Gem::Version
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.3.4
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
74
75
|
version: 1.0.0
|
75
76
|
type: :runtime
|
76
|
-
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.0
|
77
83
|
description: Rack middleware for rate-limiting incoming HTTP requests.
|
78
|
-
email: arto
|
84
|
+
email: arto@bendiken.net
|
79
85
|
executables: []
|
80
|
-
|
81
86
|
extensions: []
|
82
|
-
|
83
87
|
extra_rdoc_files: []
|
84
|
-
|
85
|
-
files:
|
88
|
+
files:
|
86
89
|
- AUTHORS
|
87
90
|
- README
|
88
91
|
- UNLICENSE
|
@@ -91,40 +94,33 @@ files:
|
|
91
94
|
- lib/rack/throttle/hourly.rb
|
92
95
|
- lib/rack/throttle/interval.rb
|
93
96
|
- lib/rack/throttle/limiter.rb
|
97
|
+
- lib/rack/throttle/minute.rb
|
94
98
|
- lib/rack/throttle/time_window.rb
|
95
99
|
- lib/rack/throttle/version.rb
|
96
100
|
- lib/rack/throttle.rb
|
97
|
-
|
98
|
-
|
99
|
-
licenses:
|
101
|
+
homepage: https://github.com/bendiken/rack-throttle
|
102
|
+
licenses:
|
100
103
|
- Public Domain
|
104
|
+
metadata: {}
|
101
105
|
post_install_message:
|
102
106
|
rdoc_options: []
|
103
|
-
|
104
|
-
require_paths:
|
107
|
+
require_paths:
|
105
108
|
- lib
|
106
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- -
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
segments:
|
111
|
-
- 1
|
112
|
-
- 8
|
113
|
-
- 2
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
114
113
|
version: 1.8.2
|
115
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
-
requirements:
|
117
|
-
- -
|
118
|
-
- !ruby/object:Gem::Version
|
119
|
-
|
120
|
-
- 0
|
121
|
-
version: "0"
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
122
119
|
requirements: []
|
123
|
-
|
124
|
-
|
125
|
-
rubygems_version: 1.3.6
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.0.3
|
126
122
|
signing_key:
|
127
|
-
specification_version:
|
123
|
+
specification_version: 4
|
128
124
|
summary: HTTP request rate limiter for Rack applications.
|
129
125
|
test_files: []
|
130
|
-
|
126
|
+
has_rdoc: false
|