merb_threshold 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|