merb_threshold 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Cory ODaniel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,225 @@
1
+ merb_threshold
2
+ ==============
3
+
4
+ Merb Threshold provides an easy way to apply control the number of times something is done in a merb
5
+ app, this includes dispatching to actions, rendering partials, etc.
6
+
7
+ Thresholds can be applied to multiple partials per page and can even span controllers and actions.
8
+
9
+ Also all of the methods are pretty well documented, so it might be pretty helpful to look at the method documentation.
10
+
11
+ When a threshold is exceeded it can be relaxed either by captcha or waiting, depending on the partial that is rendered.
12
+
13
+ ==== Set up
14
+
15
+ # Add to init.rb
16
+ require 'merb_threshold'
17
+
18
+ Merb::Plugins.config[:merb_threshold] = {
19
+ :public_key => nil, # Recaptcha Public Key
20
+ :private_key => nil, # Recaptcha Private Key
21
+ :recaptcha => true, # Enable Recaptcha
22
+
23
+ # Only needed if using Merb::Threshold::Helpers
24
+ # both take :partial as an option to specify an alternate partial
25
+ # wait()
26
+ # wait(:partial => "shared/other_wait_partial")
27
+ #
28
+ :wait_partial => 'shared/wait_partial', #Path to default partial
29
+ :captcha_partial => 'shared/recaptcha_partial' #Path to default partial
30
+ }
31
+
32
+
33
+ # Two helpers are included: captcha() and wait()
34
+ # The helpers aren't needed (see API) below, and are just provided as two examples
35
+ # of how to use the API to render wait and captch partials
36
+ #
37
+ class Merb::Controller
38
+ include Merb::Threshold::Helpers
39
+ end
40
+
41
+
42
+ ==== Public API
43
+ Thresholds are shared across controllers, which makes it possible to do things like
44
+ display a shared login partial in your Home controller while your Authentication controller
45
+ manages the access.
46
+
47
+ Instance Methods
48
+ * Merb::Controller#permit_access?
49
+ - is access permitted to the resources
50
+ - Takes a threshold name
51
+ * Merb::Controller#will_permit_another?
52
+ - will the threshold permit another request.
53
+ - Takes threshold name
54
+ * Merb::Controller#is_currently_exceeded?
55
+ - is the threshold currently exceeded (may have been exceeded by a previous request)
56
+ - Take threshold name
57
+
58
+ Class Methods
59
+ * Merb::Controller.register_threshold - registers a threshold, all thresholds must be 'registered'
60
+ * Merb::Controller.threshold_actions - This is a helper method. It registers the thresholds automatically
61
+ and creates before filters to check permit_access?. If no actions are listed thresholds, are maintained
62
+ on the controller itself (shared between actions). Optionally a list of action names can be given and an
63
+ threshold will be created for each one.
64
+
65
+ If you are not keen of the names generated by
66
+
67
+
68
+ ==== Thresholding at the Controller / Action level
69
+
70
+ Action based thresholding allows for controlling access to an action as a whole. The class level 'threshold' method
71
+ is actually just a wrapper around a before filter that calls the instances level threshold. Providing the
72
+ wrapper does allow for the threshold to halt the filter chain.
73
+
74
+ Example:
75
+ class MyController < Application
76
+ threshold_actions :index, :create, :limit => [5, 30.seconds]
77
+
78
+ #equivalent to:
79
+ register_threshold :"my_controller/index", :limit => [5, 30.seconds]
80
+ before(nil,{:only => [:index]}) do
81
+ permit_access? :"my_controller/index"
82
+ end
83
+
84
+ register_threshold :"my_controller/create", :limit => [5, 30.seconds]
85
+ before(nil,{:only => [:create]}) do
86
+ permit_access? :"my_controller/create"
87
+ end
88
+
89
+ #create a controller level threshold
90
+ class MyController < Application
91
+ threshold_actions :limit => [5000, 1.day]
92
+
93
+ #equivalent to:
94
+ register_threshold :my_controller, :limit => [5000, 1.day]
95
+ before(nil,{}) do
96
+ permit_access? :my_controller
97
+ end
98
+
99
+ #create 1 action level threshold with :unless statement and halt
100
+ class MyController < Application
101
+ threshold_actions :search, :limit => [10, 5.minutes],
102
+ :unless => :is_admin?,
103
+ :halt_with => "Too many searches"
104
+
105
+ #equivalent to:
106
+ register_threshold :"my_controller/search", :limit => [10, 5.minutes]
107
+ before(nil,{:only => [:search], :unless => :is_admin?}) do
108
+ if !permit_access?(:"my_controller/search")
109
+ throw(:halt, "Too many searches")
110
+ end
111
+ end
112
+
113
+ ==== Partial / View based thresholding
114
+
115
+ Partial / View based thresholding can take advantage of action based thresholds or named thresholds.
116
+ class Whatever < Application
117
+ register_threshold :stuff_to_protect, :limit => 1.per(3.minutes)
118
+ # --- apps/views/whatever/index.html.erb
119
+ <!--
120
+ Cool html stuff
121
+ -->
122
+ <% if permit_access? :stuff_to_protect %>
123
+ <!-- Show your stuff, access wasn't thresholded -->
124
+ <div> Coolness?! </div>
125
+ <% else %>
126
+ <%= wait(:stuff_to_protect) %>
127
+ <!-- your users gets a wait message -->
128
+ <% end %>
129
+
130
+ ==== Cross Controller/Action thresholding
131
+
132
+ Using named thresholds it is possible to control a threshold across several controllers, actions, and partials.
133
+
134
+ Thresholds are shared across all controllers that extend from Merb::Controller, so a threshold can be accessed
135
+ by any controller, and more importantly partials can be rendered for thresholds in other controllers
136
+
137
+ Just like in 'Partial / View based thresholding' theses three methods can be used to control the flow of what
138
+ is rendered.
139
+
140
+ Methods:
141
+ * permit_access? - Returns True|False, was the request OVER the threshold
142
+ * will_permit_another? - Will the threshold permit another request
143
+ * is_currently_exceeded? - Is the current request over the threshold
144
+
145
+ Helpers (Override at your leisure in Merb::Threshold::Helpers)
146
+ * wait(threshold_name) - displays the wait message
147
+ * captcha(threshold_name) - displays the captcha
148
+
149
+
150
+ === Threshold Names vs Threshold Keys
151
+
152
+ * A threshold name is used as the identifier so the controller can keep track of registered options
153
+ * A threshold key is how to look up a particular threshold's data in the user's session
154
+
155
+ Threshold keys should be used whenever accessing any of the data stored in the users' session.
156
+
157
+ === Clearing / Destroying sessions
158
+
159
+ Since all merb_threshold data is stored in the session clearing or destroying the session will remove
160
+ any data stored for that session. This becomes important if you clear session on logout, because it
161
+ essentially resets the access history for a user. To get around this simply copy out the merb_threshold
162
+ data before clearing/destroy and then put it back in the new 'anonymous' session afterwards. This applies
163
+ to logins if they clear/destroy the anonymous session before presenting the authenticated one.
164
+
165
+ You could do something like this or whatev.
166
+ def logout
167
+ clear_keys = session.keys - [
168
+ 'merb_threshold_history',
169
+ 'merb_threshold_exceeded_thresholds',
170
+ 'merb_threshold_waiting_period']
171
+
172
+ session.regenerate
173
+ clear_keys.each { |k| session.delete(k) }
174
+ end
175
+
176
+
177
+ === will_permit_another? vs is_currently_exceeded?
178
+
179
+ * will_permit_another?
180
+ * Note: takes a threshold_name
181
+ * determines if an immediate subsequent request would exceed the threshold
182
+ * Suggested Use: when one request 'protects' another
183
+ * Example: A GET request that retrieved a form could protect the subsequent POST request
184
+
185
+ * is_currently_exceeded?
186
+ * Note: takes a threshold_name
187
+ * determines if the current request is in over the threshold
188
+ * Suggested Use: when throttling the amount of requests a resource gets
189
+ * Example: An API that allows X amount of free accesses before display a 'please pay me' page
190
+
191
+ ==== A case for will_permit_another?
192
+
193
+ * A sign up page has a limit of 5 signups per minute.
194
+ * A user signs up 5 accounts in a row (w/ no captcha | wait)
195
+ * On the sixth GET of the form the request's call to captcha() determines another request will exceeded the threshold, so a captcha is presented
196
+
197
+ ==== A case for will_permit_another?
198
+
199
+ * A user is promised access to a free API 5000 times per day
200
+ * The user's app makes 5000 requests
201
+ * On the 5001 request is_currently_exceeded? could be used to render an 'over the limit' partial
202
+
203
+
204
+ === Misc. Notes
205
+
206
+ * Thresholds can be named whatever you want, so they can be programmatically created. Also
207
+ the option :params => [:blog_id,:etc] is available that will use param values as part of the key
208
+
209
+ * merb_threshold currently stores everything in the session (may have support for)
210
+ additional stores in the future. On that note, it is not recommended to be used
211
+ with cookie base sessions because it could be easy for a user to go over 4k worth
212
+ of data if the site is composed of many controllers, actions, and partials
213
+
214
+ * A threshold is EXCEEDED when it goes beyond its limit
215
+ Given:
216
+ register_threshold :index, :limit => [3,30.seconds]
217
+ The threshold would be considered EXCEEDED on its 4th request.
218
+
219
+ * Time.now is used for all times since access times are relative
220
+ The frequency class could be a lot more useful if it didn't explicitly use Time.now and you could
221
+ look forward and backward over time. Fortunately that complexity isn't needed for actions because
222
+ you are accessing them 'now' and the plugin is concerned with 'when' they were last accessed.
223
+
224
+ * If you dont like the way units are cast using :limit => [1,30.minutes]
225
+ You can override Frequency#cast_units OR specify :limt => [1,30,:minutes]
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ require 'spec/rake/spectask'
5
+ require "rake/testtask"
6
+
7
+ require "spec"
8
+ require "spec/rake/spectask"
9
+
10
+ require 'merb-core'
11
+ require 'merb-core/tasks/merb'
12
+
13
+ GEM_NAME = "merb_threshold"
14
+ GEM_VERSION = "0.1.4"
15
+ AUTHOR = "Cory O'Daniel"
16
+ EMAIL = "contact@coryodaniel.com"
17
+ HOMEPAGE = "http://github.com/coryodaniel"
18
+ SUMMARY = "Merb plugin that provides resource access rate limits and captcha'ing"
19
+
20
+ Dir['tasks/**/*.rb'].each do |f|
21
+ puts f
22
+ require f
23
+ end
24
+
25
+ spec = Gem::Specification.new do |s|
26
+ s.rubyforge_project = 'merb'
27
+ s.name = GEM_NAME
28
+ s.version = GEM_VERSION
29
+ s.platform = Gem::Platform::RUBY
30
+ s.has_rdoc = true
31
+ s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
32
+ s.summary = SUMMARY
33
+ s.description = s.summary
34
+ s.author = AUTHOR
35
+ s.email = EMAIL
36
+ s.homepage = HOMEPAGE
37
+ s.add_dependency('merb', '>= 1.0.7.1')
38
+ s.require_path = 'lib'
39
+ s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,spec}/**/*")
40
+
41
+ end
42
+
43
+ Rake::GemPackageTask.new(spec) do |pkg|
44
+ pkg.gem_spec = spec
45
+ end
46
+
47
+ desc "install the plugin as a gem"
48
+ task :install do
49
+ Merb::RakeHelper.install(GEM_NAME, :version => GEM_VERSION)
50
+ end
51
+
52
+ desc "Uninstall the gem"
53
+ task :uninstall do
54
+ Merb::RakeHelper.uninstall(GEM_NAME, :version => GEM_VERSION)
55
+ end
56
+
57
+ desc "Create a gemspec file"
58
+ task :gemspec do
59
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
60
+ file.puts spec.to_ruby
61
+ end
62
+ end
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ TODO:
2
+ * Consider adding ability to have various stores for threshold data
3
+ * Consider HTTP method option to threshold
4
+ * Support for MerbParts
@@ -0,0 +1,35 @@
1
+ if defined?(Merb::Plugins)
2
+ Merb::Plugins.config[:merb_threshold] = {
3
+ :public_key => nil,
4
+ :private_key => nil,
5
+ :recaptcha => true,
6
+ :wait_partial => 'shared/wait_partial',
7
+ :captcha_partial => 'shared/recaptcha_partial'
8
+ }
9
+
10
+ module Merb
11
+ module Threshold
12
+ end
13
+ end
14
+
15
+ require 'merb-helpers/time_dsl'
16
+
17
+ require "merb_threshold/frequency"
18
+ require "merb_threshold/per"
19
+ require "merb_threshold/controller/merb_controller"
20
+ require "merb_threshold/helpers/wait_helper"
21
+
22
+ Numeric.send :include, Merb::Threshold::Per
23
+ include Merb::Threshold
24
+
25
+ Merb::Plugins.add_rakefiles "merb_threshold/merbtasks"
26
+
27
+ Merb::BootLoader.before_app_loads do
28
+ if Merb::Plugins.config[:merb_threshold][:recaptcha]
29
+ require 'merb_threshold/recaptcha_client'
30
+ require 'merb_threshold/helpers/recaptcha_helper'
31
+ RecaptchaClient.public_key = Merb::Plugins.config[:merb_threshold][:public_key]
32
+ RecaptchaClient.private_key = Merb::Plugins.config[:merb_threshold][:private_key]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,452 @@
1
+ module Merb
2
+ class Controller
3
+ THRESHOLD_OPTIONS = [:limit, :params]
4
+ THRESHOLD_DEFAULTS = {
5
+ :limit => [0,0.seconds], #[Access, PerSecond]
6
+ :params => nil #[:list, :of, :params, :to, :use, :in, :key]
7
+ }
8
+
9
+ #Use to keep an index of thresholds for looking up information
10
+ # by name
11
+ @@_threshold_map = Mash.new
12
+
13
+ class << self
14
+ ##
15
+ # Registers a threshold for tracking
16
+ #
17
+ # @param threshold_name [~Symbol] name of threshold
18
+ #
19
+ # @param opts [Hash] Options on how to enforce threshold
20
+ # * :params [Array[~Symbol]] Parameters to include in the threshold key
21
+ # :params => [:blog_id]
22
+ #
23
+ # * :limit [Array[Fixnum,Fixnum,Symbol]] number of access per time interval before
24
+ # threshold constraints are applied
25
+ # Default [0,0.seconds] #Always
26
+ # :limit => [2,5,:minutes] #=> Frequency(2,5,:minutes) 2 per 5 minutes
27
+ # :limit => [1, 5.minutes] #=> Frequency(1,5.minutes) 1 per 5 minutes
28
+ #
29
+ # @raises ArgumentError
30
+ #
31
+ # @api public
32
+ #
33
+ # @return [Array[~Symbol,Hash]]
34
+ # The name, opts it was registered as
35
+ def register_threshold(threshold_name,opts={})
36
+ if threshold_name.is_a?(Hash)
37
+ raise ArgumentError, "Thresolds must be named!"
38
+ end
39
+
40
+ opts = THRESHOLD_DEFAULTS.merge(opts)
41
+
42
+ opts.each_key do |key|
43
+ raise(ArgumentError,
44
+ "You can only specify known threshold options (#{THRESHOLD_OPTIONS.join(', ')}). #{key} is invalid."
45
+ ) unless THRESHOLD_OPTIONS.include?(key)
46
+ end
47
+
48
+ #register it
49
+ @@_threshold_map[threshold_name] = opts
50
+ end
51
+
52
+ ##
53
+ # A succinct wrapper to bulk register thresholds on actions and check access to those thresholds
54
+ # in before filters. This method will register the threshold and create the before filters.
55
+ #
56
+ # The threshold names will be "#{controlLer_name}/#{action_name}" when actions are given.
57
+ #
58
+ # If not actions are specified the threshold will be named for the controller.
59
+ #
60
+ # @param *args [~Array]
61
+ # args array for handling array of action names and threshold options
62
+ # Threshold queues are keyed with the controller & action name, so each
63
+ # action will have its own queue
64
+ #
65
+ # @param opts [Hash]
66
+ # * :limit [Array[Fixnum,Fixnum,Symbol]] number of access per time interval before
67
+ # threshold constraints are applied
68
+ # Default [0,0.seconds] #Always
69
+ # :limit => [2,5,:minutes] #=> Frequency(2,5,:minutes) 2 per 5 minutes
70
+ # :limit => [1, 5.minutes] #=> Frequency(1,5.minutes) 1 per 5 minutes
71
+ #
72
+ # * :halt_with [String,Symbol,Proc] Halts the filter chain instead if the
73
+ # threshold is in effect
74
+ # takes same params as before filter's throw :halt
75
+ # not specifying :halt_with when the mode is :halt
76
+ # will result in: throw(:halt)
77
+ #
78
+ # * :params [Array[~Symbol]] Parameters to include in the threshold key
79
+ # :params => [:blog_id]
80
+ #
81
+ # * :if / :unless - Passed to :if / :unless on before filter
82
+ #
83
+ # @note
84
+ # using the class method threshold_actions registers the threshold
85
+ # (no need for a register_threshold statement) and creates a before filter
86
+ # for the given actions where the actual threshold check will take place
87
+ #
88
+ # @example
89
+ # Using threshold and the before filter it creates:
90
+ #
91
+ # #Create two action level thresholds
92
+ # class MyController < Application
93
+ # threshold_actions :index, :create, :limit => [5, 30.seconds]
94
+ #
95
+ # #equivalent to:
96
+ # register_threshold :"my_controller/index", :limit => [5, 30.seconds]
97
+ # before(nil,{:only => [:index]}) do
98
+ # permit_access? :"my_controller/index"
99
+ # end
100
+ # register_threshold :"my_controller/create", :limit => [5, 30.seconds]
101
+ # before(nil,{:only => [:create]}) do
102
+ # permit_access? :"my_controller/create"
103
+ # end
104
+ #
105
+ # #create a controller level threshold
106
+ # class MyController < Application
107
+ # threshold_actions :limit => [5000, 1.day]
108
+ #
109
+ # #equivalent to:
110
+ # register_threshold :my_controller, :limit => [5000, 1.day]
111
+ # before(nil,{}) do
112
+ # permit_access? :my_controller
113
+ # end
114
+ #
115
+ # #create 1 action level threshold with :unless statement and halt
116
+ # class MyController < Application
117
+ # threshold_actions :search, :limit => [10, 5.minutes],
118
+ # :unless => :is_admin?,
119
+ # :halt_with => "Too many searches"
120
+ #
121
+ # #equivalent to:
122
+ # register_threshold :"my_controller/search", :limit => [10, 5.minutes]
123
+ # before(nil,{:only => [:search], :unless => :is_admin?}) do
124
+ # if !permit_access?(:"my_controller/search")
125
+ # throw(:halt, "Too many searches")
126
+ # end
127
+ # end
128
+ #
129
+ # @api public
130
+ #
131
+ def threshold_actions(*args)
132
+ opts = args.last.is_a?(Hash) ? args.pop : {}
133
+ thresholds_to_register = args
134
+
135
+ #exctract :limit, :params
136
+ threshold_opts = {}
137
+ threshold_opts[:limit] = opts.delete(:limit) || [0, 0.seconds] #Always
138
+ threshold_opts[:params] = opts.delete(:params)
139
+
140
+ halt_with = opts.delete(:halt_with)
141
+
142
+ #get threshold supported before filter options
143
+ filter_opts = {}
144
+ filter_opts[:if] = opts.delete(:if) if opts[:if]
145
+ filter_opts[:unless] = opts.delete(:unless) if opts[:unless]
146
+
147
+ if thresholds_to_register.empty?
148
+ # Register a controller level threshold
149
+ self.register_threshold(controller_name,threshold_opts)
150
+
151
+ self.before(nil,filter_opts) do
152
+ if !permit_access?(controller_name) && halt_with
153
+ throw(:halt, halt_with)
154
+ end
155
+ end
156
+ else
157
+ #register a threshold for each action given
158
+ thresholds_to_register.each do |action_to_register|
159
+ tmp_threshold_name = "#{controller_name}/#{action_to_register}".to_sym
160
+
161
+ self.register_threshold(tmp_threshold_name,threshold_opts)
162
+
163
+ self.before(nil, filter_opts.merge({:only => [action_to_register]})) do
164
+ if !permit_access?(tmp_threshold_name) && halt_with
165
+ throw(:halt,halt_with)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ end #end class << self
173
+
174
+
175
+ ##
176
+ # Used for determining if a subsequent request would exceed the threshold
177
+ #
178
+ # Good for protecting a post with a form or display captcha/wait before
179
+ # the threshold is exceeded
180
+ #
181
+ # @note See READEME: will_permit_another? vs is_currently_exceeded?
182
+ #
183
+ # @param threshold_name [~Symbol] The threshold to look up
184
+ #
185
+ # @api public
186
+ #
187
+ # @param [Boolean]
188
+ def will_permit_another?(threshold_name=nil)
189
+ threshold_name ||= action_name
190
+ opts = @@_threshold_map[threshold_name]
191
+ curr_threshold_key = threshold_key(threshold_name)
192
+
193
+ # if opts[:limit] is not set that means the threshold hasn't been registered yet
194
+ # so permit access, the threshold will be registered once threshold() is called
195
+ # which is usually behind the post request this would be submitted to.
196
+ if opts[:limit]
197
+ frequency = if opts[:limit].is_a?(Array)
198
+ Frequency.new(*opts[:limit])
199
+ else
200
+ opts[:limit].clone
201
+ end
202
+ frequency.load! access_history(curr_threshold_key)
203
+
204
+ frequency.permit?
205
+ else
206
+ true
207
+ end
208
+ end
209
+
210
+ ##
211
+ # Is the threshold currenlty exceeded either by this request or a previous one
212
+ #
213
+ # Good for redirecting access during the current request
214
+ #
215
+ # @note See READEME: will_permit_another? vs is_currently_exceeded?
216
+ #
217
+ # @param threshold_name [~Symbol]
218
+ # current threshold key to lookup
219
+ #
220
+ # @api public
221
+ #
222
+ # @return [Boolean]
223
+ def is_currently_exceeded?(threshold_name=nil)
224
+ threshold_name ||= action_name
225
+ curr_threshold_key = threshold_key(threshold_name)
226
+ exceeded_thresholds.member? curr_threshold_key
227
+ end
228
+
229
+
230
+ ##
231
+ # Shortcut to session[:merb_threshold_waiting_period]
232
+ #
233
+ # @note
234
+ # values stored in here are keyed with #threshold_key
235
+ # waiting_period[your_threshold_name] is not guaranteed to work instead use
236
+ # waiting_period[threshold_key(your_threshold_name)]
237
+ #
238
+ # @see #threshold_key
239
+ #
240
+ # @api semi-public
241
+ #
242
+ # @return [Hash]
243
+ def waiting_period
244
+ session[:merb_threshold_waiting_period] ||= {}
245
+ end
246
+
247
+ ##
248
+ # get the key representation of the threshold name. Used to store data
249
+ # in session. This should be used whenever accessing data stored in the session
250
+ # hash.
251
+ #
252
+ # @note
253
+ # This is needed to support Params values as a part of the threshold name
254
+ #
255
+ # @param threshold_name [~Symbol] name of the threshold to get key for
256
+ #
257
+ # @api semi-public
258
+ #
259
+ # @return [~Symbol]
260
+ def threshold_key(threshold_name)
261
+ curr_threshold_key = threshold_name.to_s
262
+
263
+ # create key to lookup threshold data from users session
264
+ opts = @@_threshold_map[threshold_name]
265
+ if opts[:params]
266
+ opts[:params].each{ |param_key| curr_threshold_key += "/#{params[param_key]}" }
267
+ end
268
+
269
+ curr_threshold_key.to_sym
270
+ end
271
+
272
+ ##
273
+ # Is access permitted to the threshold protected resource.
274
+ #
275
+ # @param threshold_name [~Symbol] Name of threshold to monitor
276
+ #
277
+ # @api public
278
+ #
279
+ # @returns [Boolean] was the access permitted?
280
+ #
281
+ def permit_access?(threshold_name=nil)
282
+ threshold_name ||= "#{controller_name}/#{action_name}".to_sym
283
+
284
+ curr_threshold_key = threshold_key(threshold_name)
285
+ opts = @@_threshold_map[threshold_name]
286
+
287
+ if opts.nil?
288
+ raise Exception, "Threshold (#{threshold_name}) was not registered"
289
+ end
290
+
291
+ # keep track of thresholds access and if they were relaxed
292
+ @relaxed_thresholds ||= {}
293
+ @relaxed_thresholds[curr_threshold_key] = false
294
+
295
+ # may or may not be exceeded, but threshold was not relaxed
296
+ frequency = if opts[:limit].is_a?(Array)
297
+ Frequency.new(*opts[:limit])
298
+ else
299
+ opts[:limit].clone
300
+ end
301
+
302
+ frequency.load! access_history(curr_threshold_key)
303
+
304
+ # Is this request permitted?
305
+ if frequency.permit? && !is_currently_exceeded?(threshold_name)
306
+ # if it is also in the exceeded list
307
+ access_history(curr_threshold_key) << Time.now.to_i
308
+ @relaxed_thresholds[curr_threshold_key] = true
309
+ else
310
+ # if request wasn't permitted and isn't already marked exceeded, mark it
311
+ exceeded_thresholds << curr_threshold_key unless is_currently_exceeded?(threshold_name)
312
+
313
+ #set the time until the treshold expires
314
+ waiting_period[curr_threshold_key] = frequency.wait
315
+
316
+ # try to relax threshold via captcha if enabled, then via waiting
317
+ if Merb::Plugins.config[:merb_threshold][:recaptcha]
318
+ @relaxed_thresholds[curr_threshold_key] = relax_via_captcha!(curr_threshold_key)
319
+ end
320
+ @relaxed_thresholds[curr_threshold_key] ||= relax_via_waiting! curr_threshold_key
321
+ end
322
+
323
+ #Only keep the last n number of access where n is frequency.occurence
324
+ access_history(curr_threshold_key).replace frequency.current_events
325
+
326
+ return @relaxed_thresholds[curr_threshold_key]
327
+ end
328
+
329
+ ##
330
+ # Looks up the users access history
331
+ #
332
+ # @param curr_threshold_key [~Symbol]
333
+ # current threshold key to lookup
334
+ #
335
+ # @note
336
+ # this is a shortcut to the session hash, thus it needs the threshold_key not the
337
+ # threshold_name
338
+ #
339
+ # @see #threshold_key
340
+ #
341
+ # @api private
342
+ #
343
+ # @return [Array[Fixnum]]
344
+ def access_history(curr_threshold_key)
345
+ session[:merb_threshold_history] ||= {}
346
+ session[:merb_threshold_history][curr_threshold_key] ||= []
347
+ session[:merb_threshold_history][curr_threshold_key]
348
+ end
349
+
350
+ ##
351
+ # Gets a list of exceeded thresholds
352
+ #
353
+ # @note
354
+ # this is a shortcut to the session hash, thus it needs the threshold_key not the
355
+ # threshold_name
356
+ #
357
+ # @see #threshold_key
358
+ #
359
+ # @api private
360
+ #
361
+ # @return [Array[~Symbol]]
362
+ #
363
+ def exceeded_thresholds
364
+ session[:merb_threshold_exceeded_thresholds] ||= []
365
+ session[:merb_threshold_exceeded_thresholds]
366
+ end
367
+
368
+ protected
369
+
370
+ ##
371
+ # Determines if the user's request solved the captcha
372
+ #
373
+ # If the threshold was relaxed, this method resets exceeded threshold and access history
374
+ # for this key
375
+ #
376
+ # @param curr_threshold_key [~Symbol]
377
+ # current threshold key to lookup
378
+ #
379
+ # @note
380
+ # this deals primarily with the session hash, thus it needs the threshold_key not the
381
+ # threshold_name
382
+ #
383
+ # @see #threshold_key
384
+ #
385
+ # @api private
386
+ #
387
+ # @return [Boolean]
388
+ def relax_via_captcha!(curr_threshold_key)
389
+ if params[:recaptcha_challenge_field] && params[:recaptcha_response_field]
390
+ did_solve, captcha_error = ::RecaptchaClient.solve(request.remote_ip,
391
+ params[:recaptcha_challenge_field],
392
+ params[:recaptcha_response_field]
393
+ )
394
+
395
+ if did_solve
396
+ relax_threshold(curr_threshold_key)
397
+ else
398
+ @captcha_error = captcha_error
399
+ end
400
+
401
+ did_solve
402
+ else #no captcha data provided
403
+ false
404
+ end
405
+ end
406
+
407
+ ##
408
+ # Determines if the threshold was relaxed by the user waiting
409
+ #
410
+ # If the threshold was relaxed, this method resets exceeded threshold, wait time and access history
411
+ # for this key
412
+ #
413
+ # @param curr_threshold_key [~Symbol]
414
+ # current threshold key to lookup
415
+ #
416
+ # @note
417
+ # this deals primarily with the session hash, thus it needs the threshold_key not the
418
+ # threshold_name
419
+ #
420
+ # @see #threshold_key
421
+ #
422
+ # @api private
423
+ #
424
+ # @return [Boolean]
425
+ #
426
+ def relax_via_waiting!(curr_threshold_key)
427
+ last_access = access_history(curr_threshold_key).last
428
+ time_to_wait = (waiting_period[curr_threshold_key] || 0)
429
+
430
+ #if there was no previous acces, this didn't relax from waiting
431
+ return false if last_access.nil?
432
+
433
+ did_relax = (Time.now.to_i > (last_access + time_to_wait))
434
+
435
+ relax_threshold(curr_threshold_key) if did_relax
436
+
437
+ did_relax
438
+ end
439
+
440
+ ##
441
+ # Resets all tracked attributes on a threshold
442
+ #
443
+ # @param curr_threshold_key [~Symbol] they key to clear
444
+ #
445
+ def relax_threshold(curr_threshold_key)
446
+ exceeded_thresholds.delete curr_threshold_key
447
+ access_history(curr_threshold_key).clear
448
+ waiting_period.delete(curr_threshold_key)
449
+ end
450
+
451
+ end # end Merb::Controller
452
+ end # end Merb