merb_threshold 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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