rack-throttle 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: