improved-rack-throttle 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
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
+ # Note that this strategy doesn't use a sliding time window, but rather
9
+ # tracks requests per calendar day. This means that the throttling counter
10
+ # is reset at midnight (according to the server's local timezone) every
11
+ # night.
12
+ #
13
+ # @example Allowing up to 86,400 requests per day
14
+ # use Rack::Throttle::Daily
15
+ #
16
+ # @example Allowing up to 1,000 requests per day
17
+ # use Rack::Throttle::Daily, :max => 1000
18
+ #
19
+ class Daily < TimeWindow
20
+ ##
21
+ # @param [#call] app
22
+ # @param [Hash{Symbol => Object}] options
23
+ # @option options [Integer] :max (86400)
24
+ def initialize(app, options = {})
25
+ super
26
+ end
27
+
28
+ ##
29
+ def max_per_day
30
+ @max_per_hour ||= options[:max_per_day] || options[:max] || 86_400
31
+ end
32
+
33
+ alias_method :max_per_window, :max_per_day
34
+
35
+ protected
36
+
37
+ ##
38
+ # @param [Rack::Request] request
39
+ # @return [String]
40
+ def cache_key(request)
41
+ [super, Time.now.strftime('%Y-%m-%d')].join(':')
42
+ end
43
+ end
44
+ end; end
@@ -0,0 +1,44 @@
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
+ # Note that this strategy doesn't use a sliding time window, but rather
9
+ # tracks requests per distinct hour. This means that the throttling
10
+ # counter is reset every hour on the hour (according to the server's local
11
+ # timezone).
12
+ #
13
+ # @example Allowing up to 3,600 requests per hour
14
+ # use Rack::Throttle::Hourly
15
+ #
16
+ # @example Allowing up to 100 requests per hour
17
+ # use Rack::Throttle::Hourly, :max => 100
18
+ #
19
+ class Hourly < TimeWindow
20
+ ##
21
+ # @param [#call] app
22
+ # @param [Hash{Symbol => Object}] options
23
+ # @option options [Integer] :max (3600)
24
+ def initialize(app, options = {})
25
+ super
26
+ end
27
+
28
+ ##
29
+ def max_per_hour
30
+ @max_per_hour ||= options[:max_per_hour] || options[:max] || 3_600
31
+ end
32
+
33
+ alias_method :max_per_window, :max_per_hour
34
+
35
+ protected
36
+
37
+ ##
38
+ # @param [Rack::Request] request
39
+ # @return [String]
40
+ def cache_key(request)
41
+ [super, Time.now.strftime('%Y-%m-%dT%H')].join(':')
42
+ end
43
+ end
44
+ end; end
@@ -0,0 +1,63 @@
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 || (dt = t1 - t0.to_f) >= minimum_interval
33
+ begin
34
+ cache_set(key, t1)
35
+ allowed
36
+ rescue StandardError => e
37
+ allowed = true
38
+ # If an error occurred while trying to update the timestamp stored
39
+ # in the cache, we will fall back to allowing the request through.
40
+ # This prevents the Rack application blowing up merely due to a
41
+ # backend cache server (Memcached, Redis, etc.) being offline.
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Returns the number of seconds before the client is allowed to retry an
47
+ # HTTP request.
48
+ #
49
+ # @return [Float]
50
+ def retry_after
51
+ minimum_interval
52
+ end
53
+
54
+ ##
55
+ # Returns the required minimal interval (in terms of seconds) that must
56
+ # elapse between two subsequent HTTP requests.
57
+ #
58
+ # @return [Float]
59
+ def minimum_interval
60
+ @min ||= (@options[:min] || 1.0).to_f
61
+ end
62
+ end
63
+ end; end
@@ -0,0 +1,228 @@
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, :options, :matchers
14
+
15
+ ##
16
+ # @param [#call] app
17
+ # @param [Hash{Symbol => Object}] options
18
+ # @option options [String] :cache (Hash.new)
19
+ # @option options [String] :key (nil)
20
+ # @option options [String] :key_prefix (nil)
21
+ # @option options [Integer] :code (403)
22
+ # @option options [String] :message ("Rate Limit Exceeded")
23
+ # @option options [Regexp] :url_rule (nil)
24
+ def initialize(app, options = {})
25
+ rules = options.delete(:rules) || {}
26
+ @app, @options, @matchers = app, options, []
27
+ @matchers += Array(rules[:ip]).map { |rule| IpMatcher.new(rule) } if rules[:ip]
28
+ @matchers += Array(rules[:url]).map { |rule| UrlMatcher.new(rule) } if rules[:url]
29
+ @matchers += Array(rules[:user_agent]).map { |rule| UserAgentMatcher.new(rule) } if rules[:user_agent]
30
+ @matchers += Array(rules[:method]).map { |rule| MethodMatcher.new(rule) } if rules[:method]
31
+ end
32
+
33
+ ##
34
+ # @param [Hash{String => String}] env
35
+ # @return [Array(Integer, Hash, #each)]
36
+ # @see http://rack.rubyforge.org/doc/SPEC.html
37
+ def call(env)
38
+ request = Rack::Request.new(env)
39
+ match_results = @matchers.map { |m| m.match?(request) }.uniq
40
+ applicable = @matchers.empty? || match_results == [true]
41
+ if applicable and !allowed?(request)
42
+ rate_limit_exceeded
43
+ else
44
+ app.call(env)
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Returns `true` if no :url_rule regex or if the request path
50
+ # matches the :url regex, `false` otherwise.
51
+ #
52
+ # You can override this class, though that might be weird.
53
+ #
54
+ # @param [String] path
55
+ # @return [Boolean]
56
+ def restricted_url?(path)
57
+ options[:url_rule].nil? || options[:url_rule].match(path)
58
+ end
59
+
60
+ ##
61
+ # Returns `false` if the rate limit has been exceeded for the given
62
+ # `request`, or `true` otherwise.
63
+ #
64
+ # Override this method in subclasses that implement custom rate limiter
65
+ # strategies.
66
+ #
67
+ # @param [Rack::Request] request
68
+ # @return [Boolean]
69
+ def allowed?(request)
70
+ case
71
+ when whitelisted?(request) then true
72
+ when blacklisted?(request) then false
73
+ else true # override in subclasses
74
+ end
75
+ end
76
+
77
+ ##
78
+ # Returns `true` if the originator of the given `request` is whitelisted
79
+ # (not subject to further rate limits).
80
+ #
81
+ # The default implementation always returns `false`. Override this
82
+ # method in a subclass to implement custom whitelisting logic.
83
+ #
84
+ # @param [Rack::Request] request
85
+ # @return [Boolean]
86
+ # @abstract
87
+ def whitelisted?(request)
88
+ false
89
+ end
90
+
91
+ ##
92
+ # Returns `true` if the originator of the given `request` is blacklisted
93
+ # (not honoring rate limits, and thus permanently forbidden access
94
+ # without the need to maintain further rate limit counters).
95
+ #
96
+ # The default implementation always returns `false`. Override this
97
+ # method in a subclass to implement custom blacklisting logic.
98
+ #
99
+ # @param [Rack::Request] request
100
+ # @return [Boolean]
101
+ # @abstract
102
+ def blacklisted?(request)
103
+ false
104
+ end
105
+
106
+ protected
107
+
108
+ ##
109
+ # @return [Hash]
110
+ def cache
111
+ case cache = (options[:cache] ||= {})
112
+ when Proc then cache.call
113
+ else cache
114
+ end
115
+ end
116
+
117
+ ##
118
+ # @param [String] key
119
+ def cache_has?(key)
120
+ case
121
+ when cache.respond_to?(:has_key?)
122
+ cache.has_key?(key)
123
+ when cache.respond_to?(:get)
124
+ cache.get(key) rescue false
125
+ else false
126
+ end
127
+ end
128
+
129
+ ##
130
+ # @param [String] key
131
+ # @return [Object]
132
+ def cache_get(key, default = nil)
133
+ case
134
+ when cache.respond_to?(:[])
135
+ cache[key] || default
136
+ when cache.respond_to?(:get)
137
+ cache.get(key) || default
138
+ end
139
+ end
140
+
141
+ ##
142
+ # @param [String] key
143
+ # @param [Object] value
144
+ # @return [void]
145
+ def cache_set(key, value)
146
+ case
147
+ when cache.respond_to?(:[]=)
148
+ begin
149
+ cache[key] = value
150
+ rescue TypeError => e
151
+ # GDBM throws a "TypeError: can't convert Float into String"
152
+ # exception when trying to store a Float. On the other hand, we
153
+ # don't want to unnecessarily coerce the value to a String for
154
+ # any stores that do support other data types (e.g. in-memory
155
+ # hash objects). So, this is a compromise.
156
+ cache[key] = value.to_s
157
+ end
158
+ when cache.respond_to?(:set)
159
+ cache.set(key, value)
160
+ end
161
+ end
162
+
163
+ ##
164
+ # @param [Rack::Request] request
165
+ # @return [String]
166
+ def cache_key(request)
167
+ id = client_identifier(request)
168
+ id = options[:key].call(request) if options.has_key?(:key)
169
+ id = [options[:key_prefix], id].join(':') if options.has_key?(:key_prefix)
170
+ @matchers.each do |matcher|
171
+ id += ":#{matcher.identifier}"
172
+ end
173
+
174
+ id
175
+ end
176
+
177
+ ##
178
+ # @param [Rack::Request] request
179
+ # @return [String]
180
+ def client_identifier(request)
181
+ request.ip.to_s
182
+ end
183
+
184
+ ##
185
+ # @param [Rack::Request] request
186
+ # @return [Float]
187
+ def request_start_time(request)
188
+ case
189
+ when request.env.has_key?('HTTP_X_REQUEST_START')
190
+ request.env['HTTP_X_REQUEST_START'].to_f / 1000
191
+ else
192
+ Time.now.to_f
193
+ end
194
+ end
195
+
196
+ ##
197
+ # Outputs a `Rate Limit Exceeded` error.
198
+ #
199
+ # @return [Array(Integer, Hash, #each)]
200
+ def rate_limit_exceeded
201
+ headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {}
202
+ http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers)
203
+ end
204
+
205
+ ##
206
+ # Outputs an HTTP `4xx` or `5xx` response.
207
+ #
208
+ # @param [Integer] code
209
+ # @param [String, #to_s] message
210
+ # @param [Hash{String => String}] headers
211
+ # @return [Array(Integer, Hash, #each)]
212
+ def http_error(code, message = nil, headers = {})
213
+ [ code,
214
+ { 'Content-Type' => 'text/plain; charset=utf-8' }.merge(headers),
215
+ Array( http_status(code) + (message.nil? ? "\n" : " (#{message})\n") )
216
+ ]
217
+ end
218
+
219
+ ##
220
+ # Returns the standard HTTP status message for the given status `code`.
221
+ #
222
+ # @param [Integer] code
223
+ # @return [String]
224
+ def http_status(code)
225
+ [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
226
+ end
227
+ end
228
+ end; end
@@ -0,0 +1,24 @@
1
+ module Rack; module Throttle
2
+ ###
3
+ # This is the base class for matcher implementations
4
+ # Implements IP, user agent, url, and request method based matching
5
+ # e.g. implement throttling rules that discriminate by ip, user agent, url, request method,
6
+ # or any combination thereof
7
+ class Matcher
8
+ attr_reader :rule
9
+
10
+ def initialize(rule)
11
+ @rule = rule
12
+ end
13
+
14
+ # MUST return true or false
15
+ def match?
16
+ true
17
+ end
18
+
19
+ def identifier
20
+ ""
21
+ end
22
+ end
23
+
24
+ end; end
@@ -0,0 +1,14 @@
1
+ module Rack; module Throttle
2
+ ###
3
+ # IpMatchers take RegExp objects and compare the request ip against them
4
+ class IpMatcher < Matcher
5
+ def match?(request)
6
+ !!(@rule =~ request.ip)
7
+ end
8
+
9
+ def identifier
10
+ "ip" + @rule.inspect
11
+ end
12
+ end
13
+
14
+ end; end
@@ -0,0 +1,15 @@
1
+ module Rack; module Throttle
2
+ ###
3
+ # MethodMatchers take Symbol objects of :get, :put, :post, or :delete
4
+ class MethodMatcher < Matcher
5
+ def match?(request)
6
+ rack_method = :"#{@rule}?"
7
+ request.send(rack_method)
8
+ end
9
+
10
+ def identifier
11
+ "meth-#{@rule}"
12
+ end
13
+ end
14
+
15
+ end; end
@@ -0,0 +1,15 @@
1
+ module Rack; module Throttle
2
+ ###
3
+ # UrlMatchers take Regexp objects and compare the request path against them
4
+ class UrlMatcher < Matcher
5
+ def match?(request)
6
+ !!(@rule =~ request.path)
7
+ end
8
+
9
+ def identifier
10
+ "url" + @rule.inspect
11
+ end
12
+ end
13
+
14
+ end; end
15
+