class Rack::Throttle::Limiter
This is the base class for rate limiter implementations.
@example Defining a rate limiter subclass
class MyLimiter < Limiter def allowed?(request) # TODO: custom logic goes here end end
Attributes
Public Class Methods
@param [#call] app @param [Hash{Symbol => Object}] options @option options [String] :cache (Hash.new) @option options [String] :key (nil) @option options [String] :key_prefix (nil) @option options [Integer] :code (403) @option options [String] :message (“Rate Limit Exceeded”) @option options [String] :type (“text/plain; charset=utf-8”)
# File lib/rack/throttle/limiter.rb, line 25 def initialize(app, options = {}) @app, @options = app, options end
Public Instance Methods
Returns `false` if the rate limit has been exceeded for the given `request`, or `true` otherwise.
Override this method in subclasses that implement custom rate limiter strategies.
@param [Rack::Request] request @return [Boolean]
# File lib/rack/throttle/limiter.rb, line 47 def allowed?(request) case when whitelisted?(request) then true when blacklisted?(request) then false else true # override in subclasses end end
Returns `true` if the originator of the given `request` is blacklisted (not honoring rate limits, and thus permanently forbidden access without the need to maintain further rate limit counters).
The default implementation always returns `false`. Override this method in a subclass to implement custom blacklisting logic.
@param [Rack::Request] request @return [Boolean] @abstract
# File lib/rack/throttle/limiter.rb, line 80 def blacklisted?(request) false end
@param [Hash{String => String}] env @return [Array(Integer, Hash, each)] @see rack.rubyforge.org/doc/SPEC.html
# File lib/rack/throttle/limiter.rb, line 33 def call(env) request = Rack::Request.new(env) allowed?(request) ? app.call(env) : rate_limit_exceeded(request) end
Returns `true` if the originator of the given `request` is whitelisted (not subject to further rate limits).
The default implementation always returns `false`. Override this method in a subclass to implement custom whitelisting logic.
@param [Rack::Request] request @return [Boolean] @abstract
# File lib/rack/throttle/limiter.rb, line 65 def whitelisted?(request) false end
Protected Instance Methods
@return [Hash]
# File lib/rack/throttle/limiter.rb, line 88 def cache case cache = (options[:cache] ||= {}) when Proc then cache.call else cache end end
@param [String] key @return [Object]
# File lib/rack/throttle/limiter.rb, line 110 def cache_get(key, default = nil) case when cache.respond_to?(:[]) cache[key] || default when cache.respond_to?(:get) cache.get(key) || default end end
@param [String] key
# File lib/rack/throttle/limiter.rb, line 97 def cache_has?(key) case when cache.respond_to?(:has_key?) cache.has_key?(key) when cache.respond_to?(:get) cache.get(key) rescue false else false end end
@param [Rack::Request] request @return [String]
# File lib/rack/throttle/limiter.rb, line 144 def cache_key(request) id = client_identifier(request) case when options.has_key?(:key) options[:key].call(request) when options.has_key?(:key_prefix) [options[:key_prefix], id].join(':') else id end end
@param [String] key @param [Object] value @return [void]
# File lib/rack/throttle/limiter.rb, line 123 def cache_set(key, value) case when cache.respond_to?(:[]=) begin cache[key] = value rescue TypeError # GDBM throws a "TypeError: can't convert Float into String" # exception when trying to store a Float. On the other hand, we # don't want to unnecessarily coerce the value to a String for # any stores that do support other data types (e.g. in-memory # hash objects). So, this is a compromise. cache[key] = value.to_s end when cache.respond_to?(:set) cache.set(key, value) end end
@param [Rack::Request] request @return [String]
# File lib/rack/throttle/limiter.rb, line 158 def client_identifier(request) request.ip.to_s end
Outputs an HTTP `4xx` or `5xx` response.
@param [Integer] code @param [String, to_s] message @param [Hash{String => String}] headers @return [Array(Integer, Hash, each)]
# File lib/rack/throttle/limiter.rb, line 192 def http_error(code, message = nil, headers = {}) contentType = 'text/plain; charset=utf-8' if options[:type] contentType = options[:type] end [code, {'Content-Type' => contentType}.merge(headers), [message]] end
Returns the standard HTTP status message for the given status `code`.
@param [Integer] code @return [String]
# File lib/rack/throttle/limiter.rb, line 206 def http_status(code) [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') end
Outputs a `Rate Limit Exceeded` error.
@return [Array(Integer, Hash, each)]
# File lib/rack/throttle/limiter.rb, line 179 def rate_limit_exceeded(request) options[:rate_limit_exceeded_callback].call(request) if options[:rate_limit_exceeded_callback] headers = respond_to?(:retry_after) ? {'Retry-After' => retry_after.to_f.ceil.to_s} : {} http_error(options[:code] || 403, options[:message] || 'Rate Limit Exceeded', headers) end
@param [Rack::Request] request @return [Float]
# File lib/rack/throttle/limiter.rb, line 165 def request_start_time(request) # Check whether HTTP_X_REQUEST_START or HTTP_X_QUEUE_START exist and parse its value (for # example, when having nginx in your stack, it's going to be in the "t=\d+" format). if val = (request.env['HTTP_X_REQUEST_START'] || request.env['HTTP_X_QUEUE_START']) val[/(?:^t=)?(\d+)/, 1].to_f / 1000 else Time.now.to_f end end