Class: Rack::Throttle::Limiter

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/throttle/limiter.rb

Overview

This is the base class for rate limiter implementations.

Examples:

Defining a rate limiter subclass

class MyLimiter < Limiter
  def allowed?(request)
    # TODO: custom logic goes here
  end
end

Direct Known Subclasses

Interval, TimeWindow

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Limiter

Returns a new instance of Limiter.

Parameters:

  • app (#call)
  • options (Hash{Symbol => Object}) (defaults to: {})

Options Hash (options):

  • :cache (String) — default: Hash.new
  • :key (String) — default: nil
  • :key_prefix (String) — default: nil
  • :code (Integer) — default: 403
  • :message (String) — default: "Rate Limit Exceeded"
  • :on_reject (Proc) — default: Proc.new { puts "hey!" }


25
26
27
# File 'lib/rack/throttle/limiter.rb', line 25

def initialize(app, options = {})
  @app, @options = app, options
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



13
14
15
# File 'lib/rack/throttle/limiter.rb', line 13

def app
  @app
end

#optionsObject (readonly)

Returns the value of attribute options.



14
15
16
# File 'lib/rack/throttle/limiter.rb', line 14

def options
  @options
end

Instance Method Details

#allowed?(request) ⇒ Boolean

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.

Parameters:

  • request (Rack::Request)

Returns:

  • (Boolean)


52
53
54
55
56
57
58
# File 'lib/rack/throttle/limiter.rb', line 52

def allowed?(request)
  case
    when whitelisted?(request) then true
    when blacklisted?(request) then false
    else true # override in subclasses
  end
end

#blacklisted?(request) ⇒ Boolean

This method is abstract.

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.

Parameters:

  • request (Rack::Request)

Returns:

  • (Boolean)


85
86
87
# File 'lib/rack/throttle/limiter.rb', line 85

def blacklisted?(request)
  false
end

#cacheHash (protected)

Returns:

  • (Hash)


98
99
100
101
102
103
# File 'lib/rack/throttle/limiter.rb', line 98

def cache
  case cache = (options[:cache] ||= {})
    when Proc then cache.call
    else cache
  end
end

#cache_get(key, default = nil) ⇒ Object (protected)

Parameters:

  • key (String)

Returns:

  • (Object)


120
121
122
123
124
125
126
127
# File 'lib/rack/throttle/limiter.rb', line 120

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

#cache_has?(key) ⇒ Boolean (protected)

Parameters:

  • key (String)

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
115
# File 'lib/rack/throttle/limiter.rb', line 107

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

#cache_key(request) ⇒ String (protected)

Parameters:

  • request (Rack::Request)

Returns:

  • (String)


154
155
156
157
158
159
160
161
162
163
# File 'lib/rack/throttle/limiter.rb', line 154

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

#cache_set(key, value) (protected)

This method returns an undefined value.

Parameters:

  • key (String)
  • value (Object)


133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rack/throttle/limiter.rb', line 133

def cache_set(key, value)
  case
    when cache.respond_to?(:[]=)
      begin
        cache[key] = value
      rescue TypeError => e
        # 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

#call(env) ⇒ Array(Integer, Hash, #each)

Parameters:

  • env (Hash{String => String})

Returns:

  • (Array(Integer, Hash, #each))

See Also:



33
34
35
36
37
38
39
40
41
# File 'lib/rack/throttle/limiter.rb', line 33

def call(env)
  request = Rack::Request.new(env)
  if allowed?(request)
    app.call(env)
  else
    call_on_reject(env)
    rate_limit_exceeded
  end
end

#call_on_reject(env) ⇒ Object (protected)

Calls whatever object is passed with options[:on_reject] on initialize



92
93
94
# File 'lib/rack/throttle/limiter.rb', line 92

def call_on_reject(env)
  @options[:on_reject].call(env) if @options[:on_reject]
end

#client_identifier(request) ⇒ String (protected)

Parameters:

  • request (Rack::Request)

Returns:

  • (String)


168
169
170
# File 'lib/rack/throttle/limiter.rb', line 168

def client_identifier(request)
  request.ip.to_s
end

#http_error(code, message = nil, headers = {}) ⇒ Array(Integer, Hash, #each) (protected)

Outputs an HTTP 4xx or 5xx response.

Parameters:

  • code (Integer)
  • message (String, #to_s) (defaults to: nil)
  • headers (Hash{String => String}) (defaults to: {})

Returns:

  • (Array(Integer, Hash, #each))


200
201
202
203
# File 'lib/rack/throttle/limiter.rb', line 200

def http_error(code, message = nil, headers = {})
  [code, {'Content-Type' => 'text/plain; charset=utf-8'}.merge(headers),
    http_status(code) + (message.nil? ? "\n" : " (#{message})\n")]
end

#http_status(code) ⇒ String (protected)

Returns the standard HTTP status message for the given status code.

Parameters:

  • code (Integer)

Returns:

  • (String)


210
211
212
# File 'lib/rack/throttle/limiter.rb', line 210

def http_status(code)
  [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ')
end

#rate_limit_exceededArray(Integer, Hash, #each) (protected)

Outputs a Rate Limit Exceeded error.

Returns:

  • (Array(Integer, Hash, #each))


188
189
190
191
# File 'lib/rack/throttle/limiter.rb', line 188

def rate_limit_exceeded
  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

#request_start_time(request) ⇒ Float (protected)

Parameters:

  • request (Rack::Request)

Returns:

  • (Float)


175
176
177
178
179
180
181
182
# File 'lib/rack/throttle/limiter.rb', line 175

def request_start_time(request)
  case
    when request.env.has_key?('HTTP_X_REQUEST_START')
      request.env['HTTP_X_REQUEST_START'].to_f / 1000
    else
      Time.now.to_f
  end
end

#whitelisted?(request) ⇒ Boolean

This method is abstract.

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.

Parameters:

  • request (Rack::Request)

Returns:

  • (Boolean)


70
71
72
# File 'lib/rack/throttle/limiter.rb', line 70

def whitelisted?(request)
  false
end