Class: Merb::Controller

Inherits:
Object
  • Object
show all
Defined in:
lib/merb_threshold/controller/merb_controller.rb

Constant Summary collapse

THRESHOLD_OPTIONS =
[:limit, :params]
THRESHOLD_DEFAULTS =
{
  :limit      => [0,0.seconds],  #[Access, PerSecond]
  :params     => nil            #[:list, :of, :params, :to, :use, :in, :key]
}
@@_threshold_map =

Use to keep an index of thresholds for looking up information by name

Mash.new

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.register_threshold(threshold_name, opts = {}) ⇒ Array[~Symbol,Hash]

Registers a threshold for tracking

Parameters:

  • threshold_name (~Symbol)

    name of threshold

  • opts (Hash) (defaults to: {})

    Options on how to enforce threshold

    • :params [Array] Parameters to include in the threshold key :params => [:blog_id]

    • :limit [Array] number of access per time interval before

      threshold constraints are applied
      Default [0,0.seconds] #Always
      

      :limit => [2,5,:minutes] #=> Frequency(2,5,:minutes) 2 per 5 minutes :limit => [1, 5.minutes] #=> Frequency(1,5.minutes) 1 per 5 minutes

Returns:

  • (Array[~Symbol,Hash])

    The name, opts it was registered as



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/merb_threshold/controller/merb_controller.rb', line 35

def register_threshold(threshold_name,opts={})
  if threshold_name.is_a?(Hash)
    raise ArgumentError, "Thresolds must be named!"
  end

  opts = THRESHOLD_DEFAULTS.merge(opts)

  opts.each_key do |key| 
    raise(ArgumentError,
      "You can only specify known threshold options (#{THRESHOLD_OPTIONS.join(', ')}). #{key} is invalid."
    ) unless THRESHOLD_OPTIONS.include?(key)
  end

  #register it
  @@_threshold_map[threshold_name] = opts
end

.threshold_actions(*args) ⇒ Object

Note:

using the class method threshold_actions registers the threshold (no need for a register_threshold statement) and creates a before filter for the given actions where the actual threshold check will take place

A succinct wrapper to bulk register thresholds on actions and check access to those thresholds in before filters. This method will register the threshold and create the before filters.

The threshold names will be “#controlLer_name/#action_name” when actions are given.

If not actions are specified the threshold will be named for the controller.

Examples:

Using threshold and the before filter it creates:

#Create two action level thresholds
class MyController < Application
  threshold_actions :index, :create, :limit => [5, 30.seconds]

  #equivalent to:
  register_threshold :"my_controller/index", :limit => [5, 30.seconds]
  before(nil,{:only => [:index]}) do
    permit_access? :"my_controller/index"
  end
  register_threshold :"my_controller/create", :limit => [5, 30.seconds]
  before(nil,{:only => [:create]}) do
    permit_access? :"my_controller/create"
  end

#create a controller level threshold
class MyController < Application
  threshold_actions :limit => [5000, 1.day]

  #equivalent to:
  register_threshold :my_controller, :limit => [5000, 1.day]
  before(nil,{}) do
    permit_access? :my_controller
  end

#create 1 action level threshold with :unless statement and halt
class MyController < Application
threshold_actions :search, :limit => [10, 5.minutes], 
  :unless => :is_admin?, 
  :halt_with => "Too many searches"

#equivalent to:
register_threshold :"my_controller/search", :limit => [10, 5.minutes]
before(nil,{:only => [:search], :unless => :is_admin?}) do
  if !permit_access?(:"my_controller/search")
    throw(:halt, "Too many searches")
  end
end    

Parameters:

  • *args (~Array)

    args array for handling array of action names and threshold options Threshold queues are keyed with the controller & action name, so each action will have its own queue

  • opts (Hash)
    • :limit [Array] number of access per time interval before

      threshold constraints are applied
      Default [0,0.seconds] #Always
      

      :limit => [2,5,:minutes] #=> Frequency(2,5,:minutes) 2 per 5 minutes :limit => [1, 5.minutes] #=> Frequency(1,5.minutes) 1 per 5 minutes

    • :halt_with [String,Symbol,Proc] Halts the filter chain instead if the

      threshold is in effect
      takes same params as before filter's throw :halt
      not specifying :halt_with when the mode is :halt
      will result in: throw(:halt)
      
    • :params [Array] Parameters to include in the threshold key :params => [:blog_id]

    • :if / :unless - Passed to :if / :unless on before filter



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/merb_threshold/controller/merb_controller.rb', line 131

def threshold_actions(*args)
  opts = args.last.is_a?(Hash) ? args.pop : {}
  thresholds_to_register = args

  #exctract :limit, :params
  threshold_opts  = {}
  threshold_opts[:limit]      = opts.delete(:limit) || [0, 0.seconds] #Always
  threshold_opts[:params]     = opts.delete(:params)

  halt_with                   = opts.delete(:halt_with)

  #get threshold supported before filter options
  filter_opts = {}
  filter_opts[:if]            = opts.delete(:if)      if opts[:if]
  filter_opts[:unless]        = opts.delete(:unless)  if opts[:unless]

  if thresholds_to_register.empty?
    # Register a controller level threshold
    self.register_threshold(controller_name,threshold_opts)

    self.before(nil,filter_opts) do
      if !permit_access?(controller_name) && halt_with
        throw(:halt, halt_with)
      end
    end
  else
    #register a threshold for each action given
    thresholds_to_register.each do |action_to_register|
      tmp_threshold_name = "#{controller_name}/#{action_to_register}".to_sym
      
      self.register_threshold(tmp_threshold_name,threshold_opts)

      self.before(nil, filter_opts.merge({:only => [action_to_register]})) do 
        if !permit_access?(tmp_threshold_name) && halt_with
          throw(:halt,halt_with)
        end
      end
    end
  end
end

Instance Method Details

#access_history(curr_threshold_key) ⇒ Array[Fixnum]

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

this is a shortcut to the session hash, thus it needs the threshold_key not the threshold_name

Looks up the users access history

Parameters:

  • curr_threshold_key (~Symbol)

    current threshold key to lookup

Returns:

  • (Array[Fixnum])

See Also:



344
345
346
347
348
# File 'lib/merb_threshold/controller/merb_controller.rb', line 344

def access_history(curr_threshold_key)        
  session[:merb_threshold_history] ||= {}
  session[:merb_threshold_history][curr_threshold_key] ||= []
  session[:merb_threshold_history][curr_threshold_key]
end

#exceeded_thresholdsArray[~Symbol]

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Note:

this is a shortcut to the session hash, thus it needs the threshold_key not the threshold_name

Gets a list of exceeded thresholds

Returns:

  • (Array[~Symbol])

See Also:



363
364
365
366
# File 'lib/merb_threshold/controller/merb_controller.rb', line 363

def exceeded_thresholds
  session[:merb_threshold_exceeded_thresholds] ||= []
  session[:merb_threshold_exceeded_thresholds]      
end

#is_currently_exceeded?(threshold_name = nil) ⇒ Boolean

Note:

See READEME: will_permit_another? vs is_currently_exceeded?

Is the threshold currenlty exceeded either by this request or a previous one

Good for redirecting access during the current request

Parameters:

  • threshold_name (~Symbol) (defaults to: nil)

    current threshold key to lookup

Returns:

  • (Boolean)


223
224
225
226
227
# File 'lib/merb_threshold/controller/merb_controller.rb', line 223

def is_currently_exceeded?(threshold_name=nil)
  threshold_name ||= action_name
  curr_threshold_key = threshold_key(threshold_name)
  exceeded_thresholds.member? curr_threshold_key
end

#permit_access?(threshold_name = nil) ⇒ Boolean

Is access permitted to the threshold protected resource.

Parameters:

  • threshold_name (~Symbol) (defaults to: nil)

    Name of threshold to monitor

Returns:

  • (Boolean)


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/merb_threshold/controller/merb_controller.rb', line 281

def permit_access?(threshold_name=nil)
  threshold_name ||= "#{controller_name}/#{action_name}".to_sym
  
  curr_threshold_key = threshold_key(threshold_name)
  opts = @@_threshold_map[threshold_name]
  
  if opts.nil?
    raise Exception, "Threshold (#{threshold_name}) was not registered"
  end
  
  # keep track of thresholds access and if they were relaxed
  @relaxed_thresholds ||= {}
  @relaxed_thresholds[curr_threshold_key] = false
      
  # may or may not be exceeded, but threshold was not relaxed
  frequency = if opts[:limit].is_a?(Array)
    Frequency.new(*opts[:limit])
  else
    opts[:limit].clone
  end
  
  frequency.load! access_history(curr_threshold_key)
  
  # Is this request permitted?
  if frequency.permit? && !is_currently_exceeded?(threshold_name)
    # if it is also in the exceeded list
    access_history(curr_threshold_key) << Time.now.to_i
    @relaxed_thresholds[curr_threshold_key] = true
  else
    # if request wasn't permitted and isn't already marked exceeded, mark it
    exceeded_thresholds << curr_threshold_key unless is_currently_exceeded?(threshold_name)
    
    #set the time until the treshold expires
    waiting_period[curr_threshold_key] = frequency.wait

    # try to relax threshold via captcha if enabled, then via waiting
    if Merb::Plugins.config[:merb_threshold][:recaptcha]
      @relaxed_thresholds[curr_threshold_key] = relax_via_captcha!(curr_threshold_key)
    end
    @relaxed_thresholds[curr_threshold_key] ||= relax_via_waiting! curr_threshold_key
  end

  #Only keep the last n number of access where n is frequency.occurence
  access_history(curr_threshold_key).replace frequency.current_events  
  
  return @relaxed_thresholds[curr_threshold_key]
end

#threshold_key(threshold_name) ⇒ ~Symbol

Note:

This is needed to support Params values as a part of the threshold name

get the key representation of the threshold name. Used to store data

in session.  This should be used whenever accessing data stored in the session
hash.

Parameters:

  • threshold_name (~Symbol)

    name of the threshold to get key for

Returns:

  • (~Symbol)


260
261
262
263
264
265
266
267
268
269
270
# File 'lib/merb_threshold/controller/merb_controller.rb', line 260

def threshold_key(threshold_name)  
  curr_threshold_key = threshold_name.to_s

  # create key to lookup threshold data from users session
  opts = @@_threshold_map[threshold_name]
  if opts[:params]
    opts[:params].each{ |param_key| curr_threshold_key += "/#{params[param_key]}" }
  end
  
  curr_threshold_key.to_sym
end

#waiting_periodHash

Note:

values stored in here are keyed with #threshold_key waiting_period is not guaranteed to work instead use waiting_period

Shortcut to session

Returns:

  • (Hash)

See Also:



243
244
245
# File 'lib/merb_threshold/controller/merb_controller.rb', line 243

def waiting_period
  session[:merb_threshold_waiting_period] ||= {}
end

#will_permit_another?(threshold_name = nil) ⇒ Boolean

Note:

See READEME: will_permit_another? vs is_currently_exceeded?

Used for determining if a subsequent request would exceed the threshold

Good for protecting a post with a form or display captcha/wait before

the threshold is exceeded

Parameters:

  • threshold_name (~Symbol) (defaults to: nil)

    The threshold to look up

  • (Boolean)

Returns:

  • (Boolean)


188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/merb_threshold/controller/merb_controller.rb', line 188

def will_permit_another?(threshold_name=nil)
  threshold_name ||= action_name
  opts = @@_threshold_map[threshold_name]
  curr_threshold_key = threshold_key(threshold_name)

  # if opts[:limit] is not set that means the threshold hasn't been registered yet
  #   so permit access, the threshold will be registered once threshold() is called
  #   which is usually behind the post request this would be submitted to.
  if opts[:limit]
    frequency = if opts[:limit].is_a?(Array)
      Frequency.new(*opts[:limit])
    else
      opts[:limit].clone
    end
    frequency.load! access_history(curr_threshold_key)

    frequency.permit?
  else
    true
  end
end