rack-throttle 0.0.0 → 0.1.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/README CHANGED
@@ -0,0 +1,76 @@
1
+ HTTP Request Rate Limiter for Rack
2
+ ==================================
3
+
4
+ This is [Rack][] middleware that provides logic for rate-limiting incoming
5
+ HTTP requests to your Rack application. You can use `Rack::Throttle` with
6
+ any Ruby web framework based on Rack, including with Ruby on Rails 3.0 and
7
+ with Sinatra.
8
+
9
+ * <http://github.com/datagraph/rack-throttle>
10
+
11
+ Examples
12
+ --------
13
+
14
+ ### Adding throttling to a Rackup application
15
+
16
+ require 'rack/throttle'
17
+
18
+ use Rack::Throttle::Interval
19
+
20
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, world!\n"] }
21
+
22
+ ### Enforcing a 3-second interval between requests
23
+
24
+ use Rack::Throttle::Interval, :min => 3.0
25
+
26
+ ### Using Memcached to store rate-limiting counters
27
+
28
+ use Rack::Throttle::Interval, :cache => Memcached.new, :key_prefix => :throttle
29
+
30
+ Documentation
31
+ -------------
32
+
33
+ <http://datagraph.rubyforge.org/rack-throttle/>
34
+
35
+ * {Rack::Throttle}
36
+ * {Rack::Throttle::Interval}
37
+ * {Rack::Throttle::Daily}
38
+ * {Rack::Throttle::Hourly}
39
+
40
+ Dependencies
41
+ ------------
42
+
43
+ * [Rack](http://rubygems.org/gems/rack) (>= 1.0.0)
44
+
45
+ Installation
46
+ ------------
47
+
48
+ The recommended installation method is via RubyGems. To install the latest
49
+ official release, do:
50
+
51
+ % [sudo] gem install rack-throttle
52
+
53
+ Download
54
+ --------
55
+
56
+ To get a local working copy of the development repository, do:
57
+
58
+ % git clone git://github.com/datagraph/rack-throttle.git
59
+
60
+ Alternatively, you can download the latest development version as a tarball
61
+ as follows:
62
+
63
+ % wget http://github.com/datagraph/rack-throttle/tarball/master
64
+
65
+ Author
66
+ ------
67
+
68
+ * [Arto Bendiken](mailto:arto.bendiken@gmail.com) - <http://ar.to/>
69
+
70
+ License
71
+ -------
72
+
73
+ `Rack::Throttle` is free and unencumbered public domain software. For more
74
+ information, see <http://unlicense.org/> or the accompanying UNLICENSE file.
75
+
76
+ [Rack]: http://rack.rubyforge.org/
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/lib/rack/throttle.rb CHANGED
@@ -2,6 +2,10 @@ require 'rack'
2
2
 
3
3
  module Rack
4
4
  module Throttle
5
- autoload :VERSION, 'rack/throttle/version'
5
+ autoload :Daily, 'rack/throttle/daily'
6
+ autoload :Hourly, 'rack/throttle/hourly'
7
+ autoload :Interval, 'rack/throttle/interval'
8
+ autoload :Limiter, 'rack/throttle/limiter'
9
+ autoload :VERSION, 'rack/throttle/version'
6
10
  end
7
11
  end
@@ -0,0 +1,12 @@
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 day (by default, 86,400
5
+ # requests per 24 hours, which works out to an average of 1 request per
6
+ # second).
7
+ #
8
+ # _Not yet implemented in the current release._
9
+ class Daily < Limiter
10
+ # TODO
11
+ end
12
+ end; end
@@ -0,0 +1,12 @@
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 hour (by default, 3,600
5
+ # requests per 60 minutes, which works out to an average of 1 request per
6
+ # second).
7
+ #
8
+ # _Not yet implemented in the current release._
9
+ class Hourly < Limiter
10
+ # TODO
11
+ end
12
+ end; end
@@ -0,0 +1,54 @@
1
+ module Rack; module Throttle
2
+ ##
3
+ # This rate limiter strategy throttles the application by enforcing a
4
+ # minimum interval (by default, 1 second) between subsequent allowed HTTP
5
+ # requests.
6
+ #
7
+ # @example Allowing up to two requests per second
8
+ # use Rack::Throttle::Interval, :min => 0.5 # 500 ms interval
9
+ #
10
+ # @example Allowing a request every two seconds
11
+ # use Rack::Throttle::Interval, :min => 2.0 # 2000 ms interval
12
+ #
13
+ class Interval < Limiter
14
+ ##
15
+ # @param [#call] app
16
+ # @param [Hash{Symbol => Object}] options
17
+ # @option options [Float] :min (1.0)
18
+ def initialize(app, options = {})
19
+ super
20
+ end
21
+
22
+ ##
23
+ # Returns `true` if sufficient time (equal to or more than
24
+ # {#minimum_interval}) has passed since the last request and the given
25
+ # present `request`.
26
+ #
27
+ # @param [Rack::Request] request
28
+ # @return [Boolean]
29
+ def allowed?(request)
30
+ t1 = request_start_time(request)
31
+ t0 = cache_get(key = cache_key(request)) rescue nil
32
+ allowed = !t0 || (t1 - t0.to_f) >= minimum_interval
33
+ begin
34
+ cache_set(key, t1)
35
+ allowed
36
+ rescue => e
37
+ # If an error occurred while trying to update the timestamp stored
38
+ # in the cache, we will fall back to allowing the request through.
39
+ # This prevents the Rack application blowing up merely due to a
40
+ # backend cache server (Memcached, Redis, etc.) being offline.
41
+ allowed = true
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Returns the required minimal interval (in terms of seconds) that must
47
+ # elapse between two subsequent HTTP requests.
48
+ #
49
+ # @return [Float]
50
+ def minimum_interval
51
+ @min ||= (@options[:min] || 1.0).to_f
52
+ end
53
+ end
54
+ end; end
@@ -0,0 +1,195 @@
1
+ module Rack; module Throttle
2
+ ##
3
+ # This is the base class for rate limiter implementations.
4
+ #
5
+ # @example Defining a rate limiter subclass
6
+ # class MyLimiter < Limiter
7
+ # def allowed?(request)
8
+ # # TODO: custom logic goes here
9
+ # end
10
+ # end
11
+ #
12
+ class Limiter
13
+ attr_reader :app
14
+ attr_reader :options
15
+
16
+ ##
17
+ # @param [#call] app
18
+ # @param [Hash{Symbol => Object}] options
19
+ # @option options [String] :cache (Hash.new)
20
+ # @option options [String] :key (nil)
21
+ # @option options [String] :key_prefix (nil)
22
+ # @option options [Integer] :code (403)
23
+ # @option options [String] :message ("Rate Limit Exceeded")
24
+ def initialize(app, options = {})
25
+ @app, @options = app, options
26
+ end
27
+
28
+ ##
29
+ # @param [Hash{String => String}] env
30
+ # @return [Array(Integer, Hash, #each)]
31
+ # @see http://rack.rubyforge.org/doc/SPEC.html
32
+ def call(env)
33
+ request = Rack::Request.new(env)
34
+ allowed?(request) ? app.call(env) : rate_limit_exceeded
35
+ end
36
+
37
+ ##
38
+ # Returns `false` if the rate limit has been exceeded for the given
39
+ # `request`, or `true` otherwise.
40
+ #
41
+ # Override this method in subclasses that implement custom rate limiter
42
+ # strategies.
43
+ #
44
+ # @param [Rack::Request] request
45
+ # @return [Boolean]
46
+ def allowed?(request)
47
+ case
48
+ when whitelisted?(request) then true
49
+ when blacklisted?(request) then false
50
+ else true # override in subclasses
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Returns `true` if the originator of the given `request` is whitelisted
56
+ # (not subject to further rate limits).
57
+ #
58
+ # The default implementation always returns `false`. Override this
59
+ # method in a subclass to implement custom whitelisting logic.
60
+ #
61
+ # @param [Rack::Request] request
62
+ # @return [Boolean]
63
+ # @abstract
64
+ def whitelisted?(request)
65
+ false
66
+ end
67
+
68
+ ##
69
+ # Returns `true` if the originator of the given `request` is blacklisted
70
+ # (not honoring rate limits, and thus permanently forbidden access
71
+ # without the need to maintain further rate limit counters).
72
+ #
73
+ # The default implementation always returns `false`. Override this
74
+ # method in a subclass to implement custom blacklisting logic.
75
+ #
76
+ # @param [Rack::Request] request
77
+ # @return [Boolean]
78
+ # @abstract
79
+ def blacklisted?(request)
80
+ false
81
+ end
82
+
83
+ protected
84
+
85
+ ##
86
+ # @return [Hash]
87
+ def cache
88
+ case cache = (@options[:cache] ||= {})
89
+ when Proc then cache.call
90
+ else cache
91
+ end
92
+ end
93
+
94
+ ##
95
+ # @param [String] key
96
+ def cache_has?(key)
97
+ case
98
+ when cache.respond_to?(:has_key?)
99
+ cache.has_key?(key)
100
+ when cache.respond_to?(:get)
101
+ cache.get(key) rescue false
102
+ else false
103
+ end
104
+ end
105
+
106
+ ##
107
+ # @param [String] key
108
+ # @return [Object]
109
+ def cache_get(key, default = nil)
110
+ case
111
+ when cache.respond_to?(:[])
112
+ cache[key] || default
113
+ when cache.respond_to?(:get)
114
+ cache.get(key) || default
115
+ end
116
+ end
117
+
118
+ ##
119
+ # @param [String] key
120
+ # @param [Object] value
121
+ # @return [void]
122
+ def cache_set(key, value)
123
+ case
124
+ when cache.respond_to?(:[]=)
125
+ cache[key] = value
126
+ when cache.respond_to?(:set)
127
+ cache.set(key, value)
128
+ end
129
+ end
130
+
131
+ ##
132
+ # @param [Rack::Request] request
133
+ # @return [String]
134
+ def cache_key(request)
135
+ id = client_identifier(request)
136
+ case
137
+ when options.has_key?(:key)
138
+ options[:key].call(request)
139
+ when options.has_key?(:key_prefix)
140
+ [options[:key_prefix], id].join(':')
141
+ else id
142
+ end
143
+ end
144
+
145
+ ##
146
+ # @param [Rack::Request] request
147
+ # @return [String]
148
+ def client_identifier(request)
149
+ request.ip.to_s
150
+ end
151
+
152
+ ##
153
+ # @param [Rack::Request] request
154
+ # @return [Float]
155
+ def request_start_time(request)
156
+ case
157
+ when request.env.has_key?('HTTP_X_REQUEST_START')
158
+ request.env['HTTP_X_REQUEST_START'].to_f / 1000
159
+ else
160
+ Time.now.to_f
161
+ end
162
+ end
163
+
164
+ ##
165
+ # Outputs a `Rate Limit Exceeded` error.
166
+ #
167
+ # @param [Integer] code
168
+ # @param [String] message
169
+ # @return [Array(Integer, Hash, #each)]
170
+ def rate_limit_exceeded(code = nil, message = nil)
171
+ http_error(code || options[:code] || 403,
172
+ message || options[:message] || 'Rate Limit Exceeded')
173
+ end
174
+
175
+ ##
176
+ # Outputs an HTTP `4xx` or `5xx` response.
177
+ #
178
+ # @param [Integer] code
179
+ # @param [String, #to_s] message
180
+ # @return [Array(Integer, Hash, #each)]
181
+ def http_error(code, message = nil)
182
+ [code, {'Content-Type' => 'text/plain; charset=utf-8'},
183
+ http_status(code) + (message.nil? ? "\n" : " (#{message})\n")]
184
+ end
185
+
186
+ ##
187
+ # Returns the standard HTTP status message for the given status `code`.
188
+ #
189
+ # @param [Integer] code
190
+ # @return [String]
191
+ def http_status(code)
192
+ [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
193
+ end
194
+ end
195
+ end; end
@@ -1,7 +1,7 @@
1
- module Rack module Throttle
1
+ module Rack; module Throttle
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 0
4
+ MINOR = 1
5
5
  TINY = 0
6
6
  EXTRA = nil
7
7
 
@@ -20,4 +20,4 @@ module Rack module Throttle
20
20
  # @return [Array(Integer, Integer, Integer)]
21
21
  def self.to_a() [MAJOR, MINOR, TINY] end
22
22
  end
23
- end end
23
+ end; end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 1
7
8
  - 0
8
- - 0
9
- version: 0.0.0
9
+ version: 0.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Arto Bendiken
@@ -14,13 +14,27 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-20 00:00:00 +01:00
17
+ date: 2010-03-21 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: rspec
21
+ name: rack-test
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 5
30
+ - 3
31
+ version: 0.5.3
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
24
38
  requirements:
25
39
  - - ">="
26
40
  - !ruby/object:Gem::Version
@@ -30,11 +44,11 @@ dependencies:
30
44
  - 0
31
45
  version: 1.3.0
32
46
  type: :development
33
- version_requirements: *id001
47
+ version_requirements: *id002
34
48
  - !ruby/object:Gem::Dependency
35
49
  name: yard
36
50
  prerelease: false
37
- requirement: &id002 !ruby/object:Gem::Requirement
51
+ requirement: &id003 !ruby/object:Gem::Requirement
38
52
  requirements:
39
53
  - - ">="
40
54
  - !ruby/object:Gem::Version
@@ -44,11 +58,11 @@ dependencies:
44
58
  - 3
45
59
  version: 0.5.3
46
60
  type: :development
47
- version_requirements: *id002
61
+ version_requirements: *id003
48
62
  - !ruby/object:Gem::Dependency
49
63
  name: rack
50
64
  prerelease: false
51
- requirement: &id003 !ruby/object:Gem::Requirement
65
+ requirement: &id004 !ruby/object:Gem::Requirement
52
66
  requirements:
53
67
  - - ">="
54
68
  - !ruby/object:Gem::Version
@@ -58,7 +72,7 @@ dependencies:
58
72
  - 0
59
73
  version: 1.0.0
60
74
  type: :runtime
61
- version_requirements: *id003
75
+ version_requirements: *id004
62
76
  description: Rack middleware for rate-limiting HTTP requests.
63
77
  email: arto.bendiken@gmail.com
64
78
  executables: []
@@ -72,10 +86,14 @@ files:
72
86
  - README
73
87
  - UNLICENSE
74
88
  - VERSION
89
+ - lib/rack/throttle/daily.rb
90
+ - lib/rack/throttle/hourly.rb
91
+ - lib/rack/throttle/interval.rb
92
+ - lib/rack/throttle/limiter.rb
75
93
  - lib/rack/throttle/version.rb
76
94
  - lib/rack/throttle.rb
77
95
  has_rdoc: false
78
- homepage: http://github.com/datagraph/rack-throttle
96
+ homepage: http://github.com/datagraph
79
97
  licenses:
80
98
  - Public Domain
81
99
  post_install_message: