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 +20 -0
- data/README +225 -0
- data/Rakefile +62 -0
- data/TODO +4 -0
- data/lib/merb_threshold.rb +35 -0
- data/lib/merb_threshold/controller/merb_controller.rb +452 -0
- data/lib/merb_threshold/frequency.rb +165 -0
- data/lib/merb_threshold/helpers/recaptcha_helper.rb +70 -0
- data/lib/merb_threshold/helpers/wait_helper.rb +56 -0
- data/lib/merb_threshold/merbtasks.rb +40 -0
- data/lib/merb_threshold/per.rb +13 -0
- data/lib/merb_threshold/recaptcha_client.rb +47 -0
- data/lib/merb_threshold/templates/_recaptcha_partial.html.erb +16 -0
- data/lib/merb_threshold/templates/_wait_partial.html.erb +3 -0
- data/spec/controller/merb_controller_spec.rb +271 -0
- data/spec/frequency_spec.rb +136 -0
- data/spec/helpers/recaptcha_helper_spec.rb +29 -0
- data/spec/helpers/wait_helper_spec.rb +32 -0
- data/spec/per_spec.rb +9 -0
- data/spec/recaptcha_client_spec.rb +35 -0
- data/spec/spec_helper.rb +56 -0
- metadata +90 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
module Merb
|
2
|
+
module Threshold
|
3
|
+
|
4
|
+
##
|
5
|
+
#
|
6
|
+
# @note
|
7
|
+
# * time is treated as relative from 'now'
|
8
|
+
# * nowhere are events sorted within this class so they should
|
9
|
+
# be added in their proper order
|
10
|
+
# * Why? its easy to do this from the controller naturally since time is linear (right?)
|
11
|
+
# and its faster not having to worry about sorting whenever an occurrence is added
|
12
|
+
#
|
13
|
+
class Frequency
|
14
|
+
attr_reader :interval, :occurrence, :units, :period, :events
|
15
|
+
|
16
|
+
# Frequency.new(5, 30, :seconds) => "5 times per 30 seconds"
|
17
|
+
# Frequency.new(1, 3.minutes) => "1 time per 180 seconds"
|
18
|
+
def initialize(occ,int,unts = nil)
|
19
|
+
@occurrence = occ
|
20
|
+
@interval = int
|
21
|
+
|
22
|
+
# All test are done with the period (reduced to seconds)
|
23
|
+
# 50.minutes #=> 50.send :minutes => 3000 seconds
|
24
|
+
if unts
|
25
|
+
@period = interval.send unts
|
26
|
+
@units = unts
|
27
|
+
else #no units default :seconds
|
28
|
+
@period = interval
|
29
|
+
cast_units
|
30
|
+
end
|
31
|
+
|
32
|
+
#If the period is zero, never permit?
|
33
|
+
if period == 0
|
34
|
+
@occurrence = 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# tests if the frequency would permit the additional occurence or if it
|
40
|
+
# would exceed the frequency. Histories can be loaded with frequency#load
|
41
|
+
#
|
42
|
+
# @see #load
|
43
|
+
#
|
44
|
+
# @return [Boolean]
|
45
|
+
#
|
46
|
+
def permit?
|
47
|
+
(current_events.length < occurrence)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# How long until the resource is freely available
|
52
|
+
#
|
53
|
+
# @return [~Numeric]
|
54
|
+
def wait
|
55
|
+
num_evts = current_events.length
|
56
|
+
if num_evts == 0 || num_evts < occurrence
|
57
|
+
return 0
|
58
|
+
else #How long until the oldest falls off?
|
59
|
+
# originally had now - period > @mm.first but +1 all over the place was
|
60
|
+
# retareded:
|
61
|
+
# Want: now - period >= @mm.first
|
62
|
+
# now - period + x == @mm.first
|
63
|
+
# => x == @mm.first + period - now
|
64
|
+
return (current_events.first + period - Time.now.to_i)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Loads a history of events
|
70
|
+
#
|
71
|
+
# @param evts [~Array[Fixnum]] list of timestamps
|
72
|
+
#
|
73
|
+
# @note
|
74
|
+
# Should be loaded presorted (threshold should always stored sorted min=>high)
|
75
|
+
# an array is used rather than a set because duplicates may be needed
|
76
|
+
# (concurrent access times)
|
77
|
+
#
|
78
|
+
def load(evts)
|
79
|
+
@events ||= []
|
80
|
+
@events += evts
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# clears current events and sets
|
85
|
+
#
|
86
|
+
# @param evts [~Array[Fixnum]] list of timestamps
|
87
|
+
#
|
88
|
+
# @see #load
|
89
|
+
#
|
90
|
+
#
|
91
|
+
def load!(evts)
|
92
|
+
@events = evts
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# flushes the events array
|
97
|
+
#
|
98
|
+
def flush
|
99
|
+
@events = []
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# adds a single event, this always adds and does not perform a permit? first
|
104
|
+
# @param evt [Fixnum] Timestamp
|
105
|
+
#
|
106
|
+
# @return [Array[Fixnum]]
|
107
|
+
def add(evt)
|
108
|
+
@events ||= []
|
109
|
+
@events << evt
|
110
|
+
@events
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# The rate of occurences in seconds
|
115
|
+
# returns the frequency for events that happened over period
|
116
|
+
#
|
117
|
+
# @see #events
|
118
|
+
# @see #load
|
119
|
+
# @see #period
|
120
|
+
#
|
121
|
+
# @return [~Numeric]
|
122
|
+
#
|
123
|
+
def rate
|
124
|
+
current_events.length / period.to_f
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Casts frequency units when not specified
|
129
|
+
#
|
130
|
+
def cast_units
|
131
|
+
case @interval
|
132
|
+
when 0..120 # :seconds
|
133
|
+
@units = (@interval != 1 ? :seconds : :second)
|
134
|
+
when 121..3600 # :minutes
|
135
|
+
@interval = @interval / 60.0
|
136
|
+
@units = (@interval != 1 ? :minutes : :minute)
|
137
|
+
else # :hours
|
138
|
+
@interval = @interval / 3600.0
|
139
|
+
@units = (@interval != 1 ? :hours : :hour)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Describe the frequency
|
145
|
+
#
|
146
|
+
# @return [String]
|
147
|
+
def to_s
|
148
|
+
@to_s ||= "#{occurrence} time#{'s' if occurrence != 1} per #{interval} #{units}"
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Get list of events for current period
|
153
|
+
#
|
154
|
+
# @return [Array[Fixnum]]
|
155
|
+
#
|
156
|
+
def current_events
|
157
|
+
if @events
|
158
|
+
@events.find_all{ |evt| evt >= (Time.now.to_i - period) }
|
159
|
+
else
|
160
|
+
@events ||= []
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Merb
|
2
|
+
module Threshold
|
3
|
+
module Helpers
|
4
|
+
##
|
5
|
+
# Display a captcha if the threshold has been exceeded
|
6
|
+
#
|
7
|
+
# @param threshold_name [~Symbol] key to look up
|
8
|
+
#
|
9
|
+
# @params opts [Hash] passed to partial as 'threshold_options'
|
10
|
+
# Pass any params intended for RecaptchaOptions
|
11
|
+
# Additionally may pass:
|
12
|
+
# :
|
13
|
+
# :partial => "./path/to/alternate/partial"
|
14
|
+
# :ssl => TRue|False
|
15
|
+
# :partial_opts => {} #options to pass to partial()
|
16
|
+
# #Theses keys are deleted before being passed to RecapthaOptions
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
def captcha(threshold_name = nil, opts={})
|
20
|
+
if threshold_name.is_a?(Hash)
|
21
|
+
opts = threshold_name
|
22
|
+
threshold_name = action_name
|
23
|
+
end
|
24
|
+
|
25
|
+
curr_threshold_key = threshold_key(threshold_name)
|
26
|
+
|
27
|
+
# Has the thresholded resource been accessed during this request
|
28
|
+
# if so
|
29
|
+
# if it was relaxed
|
30
|
+
# dont show partial
|
31
|
+
# else
|
32
|
+
# show partial
|
33
|
+
# else #resource wasn't access
|
34
|
+
# if permit_another?
|
35
|
+
# dont show partial
|
36
|
+
# else
|
37
|
+
# show partial
|
38
|
+
#
|
39
|
+
@show_captcha = if @relaxed_thresholds && @relaxed_thresholds.key?(curr_threshold_key)
|
40
|
+
if @relaxed_thresholds[curr_threshold_key]
|
41
|
+
false #dont show partial, it was relaxed
|
42
|
+
else
|
43
|
+
true #show partial, threshold exceeded
|
44
|
+
end
|
45
|
+
else
|
46
|
+
!will_permit_another?(threshold_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# if it won't permit another, show the captcha
|
50
|
+
if @show_captcha
|
51
|
+
_src_uri = opts.delete(:ssl) ? RecaptchaClient::API_SSL_SERVER : RecaptchaClient::API_SERVER
|
52
|
+
|
53
|
+
_encoded_key = escape_html(RecaptchaClient.public_key)
|
54
|
+
|
55
|
+
_recaptcha_partial = (opts.delete(:partial) || Merb::Plugins.config[:merb_threshold][:captcha_partial])
|
56
|
+
_partial_opts = opts.delete(:partial_opts) || {}
|
57
|
+
|
58
|
+
_partial_opts.merge!({
|
59
|
+
:src_uri => _src_uri,
|
60
|
+
:encoded_key => _encoded_key,
|
61
|
+
:threshold_options => opts,
|
62
|
+
:captcha_error => escape_html(@captcha_error.to_s)
|
63
|
+
})
|
64
|
+
|
65
|
+
partial(_recaptcha_partial, _partial_opts)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Merb
|
2
|
+
module Threshold
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
##
|
6
|
+
# Display a wait message if the threshold has been exceeded
|
7
|
+
#
|
8
|
+
# @param threshold_name [~Symbol] key to look up
|
9
|
+
#
|
10
|
+
# @params opts [Hash] passed to partial as 'threshold_options'
|
11
|
+
# :partial_opts => {} #options to pass to partial()
|
12
|
+
# @return [String]
|
13
|
+
def wait(threshold_name = nil,opts={})
|
14
|
+
if threshold_name.is_a?(Hash)
|
15
|
+
opts = threshold_name
|
16
|
+
threshold_name = action_name
|
17
|
+
end
|
18
|
+
|
19
|
+
curr_threshold_key = threshold_key(threshold_name)
|
20
|
+
|
21
|
+
# Has the thresholded resource been accessed during this request
|
22
|
+
# if so
|
23
|
+
# if it was relaxed
|
24
|
+
# dont show partial
|
25
|
+
# else
|
26
|
+
# show partial
|
27
|
+
# else #resource wasn't access
|
28
|
+
# if permit_another?
|
29
|
+
# dont show partial
|
30
|
+
# else
|
31
|
+
# show partial
|
32
|
+
#
|
33
|
+
@show_wait = if @relaxed_thresholds && @relaxed_thresholds.key?(curr_threshold_key)
|
34
|
+
if @relaxed_thresholds[curr_threshold_key]
|
35
|
+
false #dont show partial, it was relaxed
|
36
|
+
else
|
37
|
+
true #show partial, threshold exceeded
|
38
|
+
end
|
39
|
+
else #wasn't accessed, will it permit another?
|
40
|
+
!will_permit_another?(threshold_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
# if it wont permit another show wait
|
44
|
+
if @show_wait
|
45
|
+
_wait_partial = opts.delete(:partial) || Merb::Plugins.config[:merb_threshold][:wait_partial]
|
46
|
+
_partial_opts = opts.delete(:partial_opts) || {}
|
47
|
+
_partial_opts.merge!({
|
48
|
+
:seconds_to_wait => (waiting_period[threshold_key(threshold_name)] || 0)
|
49
|
+
})
|
50
|
+
partial(_wait_partial,_partial_opts)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
namespace :merb_threshold do
|
3
|
+
desc "Generate captcha partial OUT=./path/to/partials"
|
4
|
+
task :generate_captcha do
|
5
|
+
from = File.dirname(__FILE__)
|
6
|
+
to = File.join(ENV['OUT'],'_recaptcha_partial.html.erb')
|
7
|
+
FileUtils.cp File.join(from,'templates','_recaptcha_partial.html.erb'), to
|
8
|
+
|
9
|
+
if File.exist? to
|
10
|
+
puts "Captcha partial created: #{to}"
|
11
|
+
else
|
12
|
+
puts "Could not create partial: #{to}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate wait partial OUT=./path/to/partials"
|
17
|
+
task :generate_wait do
|
18
|
+
from = File.dirname(__FILE__)
|
19
|
+
to = File.join(ENV['OUT'],'_wait_partial.html.erb')
|
20
|
+
FileUtils.cp File.join(from,'templates','_wait_partial.html.erb'), to
|
21
|
+
|
22
|
+
if File.exist? to
|
23
|
+
puts "Wait partial created: #{to}"
|
24
|
+
else
|
25
|
+
puts "Could not create partial: #{to}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
namespace :audit do
|
31
|
+
desc "Print out all thresholds"
|
32
|
+
task :thresholds => :merb_env do
|
33
|
+
puts "Thresholds:"
|
34
|
+
Merb::Controller.send(:class_variable_get, "@@_threshold_map").each do |name,opts|
|
35
|
+
limit = opts[:limit].is_a?(Array) ?
|
36
|
+
Merb::Threshold::Frequency.new(*opts[:limit]) : opts[:limit]
|
37
|
+
puts " ~ #{name}: #{limit.to_s}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Merb
|
4
|
+
module Threshold
|
5
|
+
class RecaptchaClient
|
6
|
+
API_SERVER = "http://api.recaptcha.net"
|
7
|
+
API_SSL_SERVER = "https://api-secure.recaptcha.net"
|
8
|
+
API_VERIFY_SERVER = "http://api-verify.recaptcha.net"
|
9
|
+
|
10
|
+
def self.public_key
|
11
|
+
@@public_key
|
12
|
+
end
|
13
|
+
def self.public_key=(key)
|
14
|
+
@@public_key = key
|
15
|
+
end
|
16
|
+
def self.private_key
|
17
|
+
@@private_key
|
18
|
+
end
|
19
|
+
def self.private_key=(key)
|
20
|
+
@@private_key = key
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Attempt to solve the captcha
|
25
|
+
#
|
26
|
+
# @param ip [String] remote ip address
|
27
|
+
# @param challenge [String] captcha challenge
|
28
|
+
# @param response [String] captcha response
|
29
|
+
#
|
30
|
+
# @return [Array[Boolean,String]]
|
31
|
+
#
|
32
|
+
def self.solve(ip,challenge,response)
|
33
|
+
response = Net::HTTP.post_form(URI.parse(API_VERIFY_SERVER + '/verify'),{
|
34
|
+
:privatekey => @@private_key,
|
35
|
+
:remoteip => ip, #request.remote_ip,
|
36
|
+
:challenge => challenge, #params[:recaptcha_challenge_field],
|
37
|
+
:response => response #params[:recaptcha_response_field]
|
38
|
+
})
|
39
|
+
|
40
|
+
answer, error = response.body.split.map { |s| s.chomp }
|
41
|
+
|
42
|
+
[ !!(answer=='true'), error ]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<script type="text/javascript">
|
2
|
+
var RecaptchaOptions = <%= threshold_options.to_json %>
|
3
|
+
</script>
|
4
|
+
|
5
|
+
<script type="text/javascript" src="<%=src_uri-%>/challenge?k=<%=encoded_key-%>&error=<%=captcha_error-%>">
|
6
|
+
|
7
|
+
</script>
|
8
|
+
|
9
|
+
<noscript>
|
10
|
+
<iframe src="<%=src_uri-%>/noscript?k=<%=encoded_key-%>&error=<%=captcha_error-%>" height="300" width="500" frameborder="0">
|
11
|
+
</iframe>
|
12
|
+
<br>
|
13
|
+
<textarea name="recaptcha_challenge_field" rows="3" cols="40">
|
14
|
+
</textarea>
|
15
|
+
<input type="hidden" name="recaptcha_response_field" value="manual_challenge">
|
16
|
+
</noscript>
|
@@ -0,0 +1,271 @@
|
|
1
|
+
describe Merb::Controller do
|
2
|
+
before(:all) do
|
3
|
+
class OtherController < Merb::Controller
|
4
|
+
def index; "Index"; end
|
5
|
+
end
|
6
|
+
|
7
|
+
class TestController < Merb::Controller
|
8
|
+
def index; "Index"; end
|
9
|
+
def create; "Create"; end
|
10
|
+
def destroy; "Destroy"; end
|
11
|
+
def blog; "Blah blah"; end
|
12
|
+
|
13
|
+
GhettoSessionStore = {}
|
14
|
+
#Why is this ghetto session hack here? Because I spent like an hour trying to figure
|
15
|
+
# out how to get access to cookie based sessions in rspecs without starting a damn server
|
16
|
+
# up and couldn't figure it out. Feel free to 'fix' this.
|
17
|
+
def session
|
18
|
+
GhettoSessionStore[params[:session_id]] ||={}
|
19
|
+
GhettoSessionStore[params[:session_id]]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
after(:each) do
|
25
|
+
TestController._before_filters.clear
|
26
|
+
TestController.send :class_variable_set, "@@_threshold_map", Mash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should respond to #waiting_period' do
|
30
|
+
TestController.new('').should respond_to(:waiting_period)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should respond to TestController.register_threshold' do
|
34
|
+
TestController.should respond_to(:register_threshold)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should register the threshold with Merb::Controller so any threshold is accessible by all controllers' do
|
38
|
+
TestController.register_threshold(:cross_controller, :limit => 1.per(30.seconds))
|
39
|
+
@thresholds = OtherController.send :class_variable_get, "@@_threshold_map"
|
40
|
+
@thresholds.key?(:cross_controller).should be(true)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should respond to TestController.threshold_actions' do
|
44
|
+
TestController.should respond_to(:threshold_actions)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should respond to #permit_access?' do
|
48
|
+
TestController.new('').should respond_to(:permit_access?)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should respond to #is_currently_exceeded?' do
|
52
|
+
TestController.new('').should respond_to(:is_currently_exceeded?)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should respond to #access_history' do
|
56
|
+
TestController.new('').should respond_to(:access_history)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should define THRESHOLD_OPTIONS' do
|
60
|
+
defined?(Merb::Controller::THRESHOLD_OPTIONS).should == "constant"
|
61
|
+
Merb::Controller::THRESHOLD_OPTIONS.should be_instance_of(Array)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should raise an exception if a threshold is not named' do
|
65
|
+
lambda{
|
66
|
+
TestController.register_threshold :limit => 1.per(30.seconds)
|
67
|
+
}.should raise_error(ArgumentError)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should raise an exception if an invalid option is passed' do
|
71
|
+
lambda{
|
72
|
+
TestController.register_threshold :create, :band => "stixx"
|
73
|
+
}.should raise_error(ArgumentError)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should define THRESHOLD_DEFAULTS' do
|
77
|
+
defined?(Merb::Controller::THRESHOLD_DEFAULTS).should == "constant"
|
78
|
+
Merb::Controller::THRESHOLD_DEFAULTS.should be_instance_of(Hash)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should wrap calls to before filter with threshold' do
|
82
|
+
class TestController
|
83
|
+
threshold_actions :index
|
84
|
+
end
|
85
|
+
TestController._before_filters.first.last[:only].member?("index")
|
86
|
+
TestController._before_filters.first.first.should be_instance_of(Proc)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should consider a captch invalid if the challenge was not submitted' do
|
90
|
+
pending
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should be able to determine if a captcha is valid or not' do
|
94
|
+
TestController.threshold_actions :index, :limit => 1.per(1.week)
|
95
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"submitting_captcha"})
|
96
|
+
|
97
|
+
# Logic here is that recaptcha, guarantees when a captcha is solved, just need to confirm
|
98
|
+
# that the api can be contacted
|
99
|
+
@response = dispatch_to(TestController, :index,{
|
100
|
+
:session_id => "submitting_captcha",
|
101
|
+
:recaptcha_challenge_field => "bad challenge",
|
102
|
+
:recaptcha_response_field => "bad response"
|
103
|
+
})
|
104
|
+
|
105
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(true)
|
106
|
+
@response.instance_variable_get("@captcha_error").should_not be(nil)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should be able to relax a threshold by waiting' do
|
110
|
+
unless ENV['SKIP_WAIT']
|
111
|
+
TestController.threshold_actions :index, :limit => 1.per(2.seconds)
|
112
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout"})
|
113
|
+
|
114
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(false)
|
115
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout"})
|
116
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(true)
|
117
|
+
|
118
|
+
@response.waiting_period[:"test_controller/index"].should be(2)
|
119
|
+
|
120
|
+
puts "Waiting a few seconds for timeout test..."
|
121
|
+
sleep(4)
|
122
|
+
|
123
|
+
@response = dispatch_to(TestController, :index,{
|
124
|
+
:session_id=>"allow_wait_timeout",
|
125
|
+
:debug => "true"
|
126
|
+
})
|
127
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(false)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should be able to relax a threshold that halts by waiting' do
|
132
|
+
unless ENV['SKIP_WAIT']
|
133
|
+
TestController.threshold_actions :index, :limit => [1,2.seconds], :halt_with => "Too many requests!"
|
134
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
|
135
|
+
|
136
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(false)
|
137
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
|
138
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(true)
|
139
|
+
|
140
|
+
@response.waiting_period[:"test_controller/index"].should be(2)
|
141
|
+
|
142
|
+
puts "Waiting a few seconds for timeout test..."
|
143
|
+
sleep(4)
|
144
|
+
|
145
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"allow_wait_timeout_w_halt"})
|
146
|
+
@response.is_currently_exceeded?(:"test_controller/index").should be(false)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should be able to throw(:halt) when a threshold is exceeded' do
|
151
|
+
TestController.threshold_actions :create,
|
152
|
+
:halt_with => "Access Denied",
|
153
|
+
:limit => [1,30.minutes]
|
154
|
+
|
155
|
+
dispatch_to(TestController, :create,{:session_id=>"throw_halt_test"})
|
156
|
+
|
157
|
+
@response = dispatch_to(TestController, :create,{:session_id=>"throw_halt_test"})
|
158
|
+
@response.body.should == "Access Denied"
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should set the wait time when the threshold has been exceeded and in wait mode' do
|
162
|
+
TestController.threshold_actions :create, :limit => [1,30.minutes]
|
163
|
+
@response = dispatch_to(TestController, :create, {:session_id => "wait_bitch"})
|
164
|
+
@response = dispatch_to(TestController, :create, {:session_id => "wait_bitch"})
|
165
|
+
|
166
|
+
@response.waiting_period[:"test_controller/create"].should be(30.minutes)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should be able to determine if a threshold has been exceeded' do
|
170
|
+
TestController.threshold_actions :create, :limit => 1.per(30.minutes)
|
171
|
+
|
172
|
+
dispatch_to(TestController, :create, {:session_id => "threshold_exceed?"})
|
173
|
+
@response = dispatch_to(TestController, :create, {:session_id => "threshold_exceed?"})
|
174
|
+
|
175
|
+
@response.is_currently_exceeded?(:"test_controller/create").should be(true)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should be able to specify params as portions of the key in Controller.threshold' do
|
179
|
+
TestController.threshold_actions :blog,
|
180
|
+
:params => [:blog_id],
|
181
|
+
:limit => [1,30.minutes]
|
182
|
+
|
183
|
+
@response = dispatch_to(TestController, :blog, {
|
184
|
+
:session_id => "params_in_key",
|
185
|
+
:blog_id => 35,
|
186
|
+
:username => "awesome_user"
|
187
|
+
})
|
188
|
+
|
189
|
+
@response.access_history(:"test_controller/blog/35").should have(1).history
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should add an access time when the threshold has not been exceeded' do
|
193
|
+
TestController.threshold_actions :create, :limit => [30,1.minute]
|
194
|
+
30.times do |access_counter|
|
195
|
+
@response = dispatch_to(TestController, :create,{:session_id=>"record_access_to_resource"})
|
196
|
+
@response.access_history(:"test_controller/create").should have(access_counter + 1).accesses
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'should not add an access time when the threshold has been exceeded' do
|
201
|
+
TestController.threshold_actions :create, :limit => [30,1.minute]
|
202
|
+
30.times do |access_counter|
|
203
|
+
@response = dispatch_to(TestController, :create,{:session_id=>"record_access_whilst_not_exceeded"})
|
204
|
+
@response.access_history(:"test_controller/create").should have(access_counter + 1).accesses
|
205
|
+
end
|
206
|
+
|
207
|
+
@response = dispatch_to(TestController, :create,{:session_id=>"record_access_whilst_not_exceeded"})
|
208
|
+
@response.permit_access?(:"test_controller/create").should be(false)
|
209
|
+
@response.access_history(:"test_controller/create").should have(30).accesses
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should be able to determine if it can permit another access' do
|
213
|
+
TestController.threshold_actions :index, :limit => 2.per(30.seconds)
|
214
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
|
215
|
+
@response.will_permit_another?(:"test_controller/index").should be(true)
|
216
|
+
@response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
|
217
|
+
@response.will_permit_another?(:"test_controller/index").should be(false)
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should check the threshold with the action name if not provided' do
|
221
|
+
pending
|
222
|
+
#TestController.threshold_actions :index, :limit => 1.per(30.minutes)
|
223
|
+
#@response = dispatch_to(TestController, :index,{:session_id=>"default_to_action_name"})
|
224
|
+
#need an action to call permit_access?
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should captcha everytime if the :limit is not set' do
|
228
|
+
pending
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'should never wait if the :limit is not set' do
|
232
|
+
pending
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should apply the threshold to the controller when not specified' do
|
236
|
+
TestController.threshold_actions :limit => [10,30,:seconds]
|
237
|
+
dispatch_to(TestController, :index,{
|
238
|
+
:session_id=>"threshold_all_actions_test"
|
239
|
+
}).access_history(:test_controller).should have(1).accesses
|
240
|
+
|
241
|
+
dispatch_to(TestController, :create,{
|
242
|
+
:session_id=>"threshold_all_actions_test"
|
243
|
+
}).access_history(:test_controller).should have(2).accesses
|
244
|
+
|
245
|
+
dispatch_to(TestController, :destroy,{
|
246
|
+
:session_id=>"threshold_all_actions_test"
|
247
|
+
}).access_history(:test_controller).should have(3).accesses
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'should only apply the threshold to the action(s) specified' do
|
251
|
+
TestController.threshold_actions :destroy
|
252
|
+
TestController._before_filters.first.last[:only].length.should be(1)
|
253
|
+
TestController._before_filters.first.last[:only].first.should == "destroy"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Given same rule 2 per 30 seconds for index, create, destroy
|
257
|
+
# each should maintain its own history of accesses
|
258
|
+
#
|
259
|
+
it 'when multiple actions are specified their access histories should be kept separate' do
|
260
|
+
TestController.threshold_actions :index, :create, :limit => [2, 30, :seconds]
|
261
|
+
|
262
|
+
dispatch_to(TestController, :index,{:session_id=>"separate_history_test"})
|
263
|
+
@response1=dispatch_to(TestController, :index,{:session_id=>"separate_history_test"})
|
264
|
+
@response2=dispatch_to(TestController, :create,{:session_id=>"separate_history_test"})
|
265
|
+
@response3=dispatch_to(TestController, :destroy,{:session_id=>"separate_history_test"})
|
266
|
+
|
267
|
+
@response1.session[:merb_threshold_history][:"test_controller/index"].should have(2).request
|
268
|
+
@response2.session[:merb_threshold_history][:"test_controller/create"].should have(1).request
|
269
|
+
@response3.session[:merb_threshold_history][:"test_controller/destroy"].should be_nil
|
270
|
+
end
|
271
|
+
end
|