fast_submission_protection 0.1.0 → 0.1.1

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/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ = 0.1.1
2
+ * Supply an error template which includes the delay
3
+ * Adds config.action_controller.allow_fast_submission_protection (off by default in test mode)
4
+ * Stabilise the API
5
+
1
6
  = 0.1.0
2
7
  * Renamed to fast_submission_protection
3
8
  * Raise an error to handle fast submission
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # FastSubmissionProtection
2
2
 
3
- *This is experimental and the API is currently subject to sudden and massive change!*
4
-
5
3
  [![Build Status](https://secure.travis-ci.org/i2w/fast_submission_protection.png?branch=master)](http://travis-ci.org/i2w/timed_spam_rejection)
6
4
 
7
- ActionController plugin that facilitates rejecting spam based on how long the form submission took.
5
+ ActionController engine that facilitates rejecting spam based on how long the form submission took.
8
6
 
9
- This plugin was developed by [Ian White](http://github.com/ianwhite) and [Nicholas Rutherford](http://github.com/nruth) while working at [Distinctive Doors](http://distinctivedoors.co.uk) who have kindly agreed to release this under the MIT-LICENSE.
7
+ This was developed by [Ian White](http://github.com/ianwhite) and [Nicholas Rutherford](http://github.com/nruth) while working at [Distinctive Doors](http://distinctivedoors.co.uk) who have kindly agreed to release this under the MIT-LICENSE.
10
8
 
11
9
  ## Installation
12
10
 
@@ -16,15 +14,20 @@ In your Gemfile:
16
14
 
17
15
  ## Example Usage
18
16
 
17
+ Specify `protect_from_fast_submission` to protect a create action from being submitted in less than 5 seconds. The default protection is
18
+ to render a HTTP 420 page (see Rescue below).
19
+
19
20
  class FeedbackController < ApplicationController
20
- protect_from_fast_submission # default delay is 5 seconds, protects create from fast submission
21
+ protect_from_fast_submission
21
22
  end
22
23
 
24
+ You can change the delay, start and finish actions, and the rescue behaviour
25
+
23
26
  class CommentsController < ApplicationController
24
- # protects a Comment#update from happening too quickly, and rescues with custom behaviour
25
27
  protect_from_fast_submission :delay => 10, :start => [:edit, :update], :finish => [:update], :rescue => false
26
28
 
27
- rescue_from FastSubmissionProtection::SubmissionTooFastError, :with => lambda {|c| c.redirect_to :edit, :alert => 'Don't comment in anger!' }
29
+ rescue_from FastSubmissionProtection::SubmissionTooFastError,
30
+ :with => lambda {|c| c.redirect_to :edit, :alert => 'Don't comment in anger!' }
28
31
  end
29
32
 
30
33
  See `FastSubmissionProtection::Controller#protect_from_fast_submission` for more details.
@@ -43,14 +46,27 @@ You can start and finish the timed submission in different controllers, just set
43
46
 
44
47
  ## Instance methods
45
48
 
46
- You can start and finish at any point within a controller
49
+ Or, for the most fine-grained control, you can start and finish at any point
50
+
51
+ # within a controller instance
52
+ self.submission_timer('abused_form').start
53
+
54
+ # some point later, where controller is any controller instance
55
+ # will raise FastSubmissionProtection::SubmissionTooFastError if the above call was < 20 seconds ago
56
+ controller.submission_timer('abused_form', 20).finish
57
+
58
+
59
+ ## Configuration
47
60
 
48
- start_timed_submission 'abused_form'
49
- finish_timed_submission 'abused_form', 20 # raises FastSubmissionProtection::SubmissionTooFastError if the above line was < 20 seconds ago
61
+ By default `fast_submission_protection` is off in `test` mode. You can configure it in environment files as follows:
62
+
63
+ config.action_controller.allow_fast_submission_protection = false
50
64
 
51
- Other methods, like reset timer, and clear timer are available on the timer object
65
+ You can also configure it on a per controller basis:
52
66
 
53
- submission_timer('abused_form') # => a FastSubmissionProtection::SubmissionTimer
67
+ class MyController < ApplicationController
68
+ self.allow_fast_submission_protection = true
69
+ end
54
70
 
55
71
  ## Rescue
56
72
 
@@ -59,7 +75,7 @@ This is included by default when you specify `protect_from_fast_submission`.
59
75
 
60
76
  The default error page resides in 'views/fast_submission_protection/error'. Simply add this page to your views directory to use a custom page.
61
77
 
62
- Another option is to do something like put a message in the flash, and re-render the new page. Simply rescue_from FastSubmissionProtection::SubmissionTimer.
78
+ Another option is to do something like put a message in the flash, and re-render the new page. Simply rescue_from FastSubmissionProtection::SubmissionTooFastError.
63
79
 
64
80
  ## Development
65
81
 
@@ -19,7 +19,7 @@
19
19
  <body>
20
20
  <div class="dialog">
21
21
  <h1>The request you made was too fast.</h1>
22
- <p>Click your browser's back button to return to the previous page, wait 5 seconds, and try the request again.</p>
22
+ <p>Click your browser's back button to return to the previous page, wait <%= exception.delay %> seconds, and try the request again.</p>
23
23
  <p>This is an anti-spam measure.</p>
24
24
  </div>
25
25
  </body>
@@ -26,10 +26,10 @@ module FastSubmissionProtection
26
26
  # before_filter FastSubmissionProtection::StartFilter.new('abused_form_post'), :only => [:new]
27
27
  #
28
28
  # # At the instance level, wherever you want, perhaps in an action
29
- # start_timed_submission('often_abused_form_post') # to start
29
+ # submission_timer('often_abused_form_post').start # to start
30
30
  #
31
31
  # # later, somewhere else
32
- # finish_timed_submission('often_abused_form_post') # to finsih, raises SubmissionTooFastError if too fast
32
+ # submission_timer('often_abused_form_post').finish # to finsih, raises SubmissionTooFastError if too fast
33
33
  def protect_from_fast_submission options = {}
34
34
  delay = options[:delay]
35
35
  start = options[:start] || [:new, :create]
@@ -44,35 +44,24 @@ module FastSubmissionProtection
44
44
  end
45
45
 
46
46
  included do
47
- hide_action :start_timed_submission, :finish_timed_submission
48
- end
49
-
50
- def start_timed_submission name
51
- submission_timer(name).start
52
- end
53
-
54
- def finish_timed_submission name, delay = nil
55
- if protect_from_fast_submission?
56
- timer = submission_timer(name, delay)
57
- if timer.too_fast?
58
- logger.warn "WARNING: timed submission too fast" if logger
59
- timer.restart
60
- raise SubmissionTooFastError.new(name, delay)
61
- else
62
- timer.clear
63
- end
47
+ hide_action :submission_timer, :protect_from_fast_submission?
48
+
49
+ # Controls whether fast submission protection is turned on or not. Turned off by default only in test mode.
50
+ config_accessor :allow_fast_submission_protection
51
+ if allow_fast_submission_protection.nil?
52
+ self.allow_fast_submission_protection = (Rails.env != 'test')
64
53
  end
65
54
  end
66
55
 
67
- protected
68
56
  def submission_timer name, delay = nil
69
57
  SubmissionTimer.new timed_submission_storage, name, delay
70
58
  end
71
59
 
72
60
  def protect_from_fast_submission?
73
- request.post? || request.put?
61
+ allow_fast_submission_protection && (request.post? || request.put?)
74
62
  end
75
63
 
64
+ protected
76
65
  def timed_submission_storage
77
66
  session[:_fsp] ||= {}
78
67
  end
@@ -80,22 +69,16 @@ module FastSubmissionProtection
80
69
 
81
70
  class StartFilter < Struct.new(:name)
82
71
  def filter controller
83
- controller.start_timed_submission name
72
+ controller.submission_timer(name).start
84
73
  end
85
74
  end
86
75
 
87
76
  class FinishFilter < Struct.new(:name, :delay)
88
77
  def filter controller
89
- controller.finish_timed_submission name, delay
90
- end
91
- end
92
-
93
- class FastSubmissionProtection::SubmissionTooFastError < RuntimeError
94
- def initialize name, delay
95
- @name, @delay = name, delay
78
+ if controller.protect_from_fast_submission?
79
+ controller.submission_timer(name, delay).finish
80
+ end
96
81
  end
97
-
98
- attr_reader :name, :delay
99
82
  end
100
83
 
101
84
  module Rescue
@@ -106,8 +89,8 @@ module FastSubmissionProtection
106
89
  end
107
90
 
108
91
  protected
109
- def render_fast_submission_protection_error
110
- render :template => 'fast_submission_protection/error', :layout => false, :status => 420
92
+ def render_fast_submission_protection_error exception
93
+ render :template => 'fast_submission_protection/error', :locals => {:exception => exception}, :layout => false, :status => 420
111
94
  end
112
95
  end
113
96
  end
@@ -1,4 +1,11 @@
1
1
  module FastSubmissionProtection
2
+ class SubmissionTooFastError < RuntimeError
3
+ attr_reader :name, :delay
4
+ def initialize name, delay
5
+ @name, @delay = name, delay
6
+ end
7
+ end
8
+
2
9
  class SubmissionTimer
3
10
  class_attribute :delay, :clock
4
11
  self.delay = 5
@@ -9,22 +16,28 @@ module FastSubmissionProtection
9
16
  @delay = delay || self.class.delay
10
17
  @clock = clock || self.class.clock
11
18
  end
12
-
13
- def too_fast?
14
- started = @storage[@key]
15
- !started || (started + @delay > @clock.now)
16
- end
17
19
 
18
20
  def start
19
21
  @storage[@key] ||= @clock.now
20
22
  end
21
23
 
22
24
  def restart
23
- @storage[@key] = @clock.now
25
+ clear
26
+ start
24
27
  end
25
28
 
26
29
  def clear
27
30
  @storage.delete @key
28
31
  end
32
+
33
+ def finish
34
+ started = @storage[@key]
35
+ if (started && (started + @delay <= @clock.now))
36
+ clear
37
+ else
38
+ restart
39
+ raise SubmissionTooFastError.new(@key, @delay)
40
+ end
41
+ end
29
42
  end
30
43
  end
@@ -1,3 +1,3 @@
1
1
  module FastSubmissionProtection
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -1,18 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
- module FastSubmissionProtection
4
- class Application < Rails::Application
5
- config.i18n.default_locale = :en
6
- end
7
- end
8
-
9
- class ApplicationController < ActionController::Base
10
- include Rails.application.routes.url_helpers
11
- end
12
-
13
3
  describe 'A controller with protect_from_fast_submission' do
14
4
  controller do
15
- protect_from_fast_submission :name => 'test', :rescue => false
5
+ self.allow_fast_submission_protection = true # test mode turns this off be default
6
+
7
+ protect_from_fast_submission :name => 'a_submission'
16
8
 
17
9
  def new
18
10
  render_new
@@ -60,16 +52,20 @@ describe 'A controller with protect_from_fast_submission' do
60
52
  end
61
53
 
62
54
  shared_examples_for 'a spammy form posting' do
63
- it do expect{ subject }.to raise_error FastSubmissionProtection::SubmissionTooFastError end
55
+ it 'should render the fast_submission_protection error page' do
56
+ subject
57
+ controller.should render_template('fast_submission_protection/error')
58
+ end
64
59
 
65
- it 'should not execute create' do
66
- controller.should_not_receive(:created)
60
+ it 'should not have created' do
61
+ controller.should_not_receive :created
62
+ subject
67
63
  end
68
64
  end
69
65
 
70
66
  shared_examples_for 'a non spammy form posting' do
71
- it 'should execute :create' do
72
- controller.should_receive(:created)
67
+ it 'should have created successfully' do
68
+ controller.should_receive :created
73
69
  subject
74
70
  end
75
71
  end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastSubmissionProtection::StartFilter do
4
+ describe '.new <submission_name>' do
5
+ subject { described_class.new('name') }
6
+
7
+ it "is equal to another StartFilter with the same name (for filter chain manipulation purposes)" do
8
+ subject.should_not == described_class.new('foo')
9
+ subject.should == described_class.new('name')
10
+ end
11
+
12
+ it '#filter <controller> starts a submission_timer for <name>' do
13
+ controller, timer = double, double
14
+
15
+ controller.should_receive(:submission_timer).with('name').and_return(timer)
16
+ timer.should_receive(:start)
17
+ subject.filter controller
18
+ end
19
+ end
20
+ end
21
+
22
+ describe FastSubmissionProtection::FinishFilter do
23
+ describe '.new <submission_name>, <delay>' do
24
+ subject { filter }
25
+
26
+ let(:filter) { described_class.new('name', 20) }
27
+
28
+ it "is equal to another FinishFilter with the same name and delay (for filter chain manipulation purposes)" do
29
+ subject.should_not == described_class.new('foo', 20)
30
+ subject.should_not == described_class.new('name', 15)
31
+ subject.should == described_class.new('name', 20)
32
+ end
33
+
34
+ describe '#filter <controller>' do
35
+ subject { filter.filter controller }
36
+
37
+ context 'when controller doesn\'t protect_from_fast_submission' do
38
+ let(:controller) { double :protect_from_fast_submission? => false }
39
+
40
+ it 'doesn\'t do anything' do
41
+ subject
42
+ end
43
+ end
44
+
45
+ context 'when controller protect_from_fast_submission?' do
46
+ let(:controller) { double :protect_from_fast_submission? => true }
47
+
48
+ it 'finishes a submission_timer for <name>, <delay>' do
49
+ controller.should_receive(:submission_timer).with('name', 20).and_return(timer = double)
50
+ timer.should_receive(:finish)
51
+ subject
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastSubmissionProtection::SubmissionTimer do
4
+ describe '.new <storage>, <key>, <delay>, <clock>' do
5
+ subject { timer }
6
+
7
+ let(:timer) { FastSubmissionProtection::SubmissionTimer.new storage, key, delay, clock }
8
+ let(:storage) { Hash.new }
9
+ let(:key) { 'the_key' }
10
+ let(:delay) { 10 }
11
+ let(:clock) { double :now => now }
12
+ let(:now) { Time.now }
13
+
14
+ describe '#start' do
15
+ subject { timer.start }
16
+
17
+ it 'starts the timer (stores the clock\'s current time (#now) for the key)' do
18
+ subject
19
+ storage[key].should == now
20
+ end
21
+
22
+ context 'when the timer is started' do
23
+ before { timer.start }
24
+
25
+ it "doesn\'t do anything (it doesn't store a later time)" do
26
+ earlier = now
27
+ clock.stub(:now).and_return now + 1.hour
28
+ subject
29
+ storage[key].should == earlier
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#clear' do
35
+ it 'clears the timer (clears any time stored for the key)' do
36
+ timer.start
37
+ timer.clear
38
+ storage.should_not have_key(key)
39
+ end
40
+ end
41
+
42
+ describe '#restart' do
43
+ it 'clears and starts the timer (clears any storage and stores the clock\'s current time for the key)' do
44
+ timer.start
45
+ clock.stub(:now).and_return(later = now + 1.hour)
46
+ timer.restart
47
+ storage[key].should == later
48
+ end
49
+ end
50
+
51
+ describe '#finish' do
52
+ subject { timer.finish }
53
+
54
+ context 'when the timer isn\'t started' do
55
+ it { expect{ subject }.to raise_error(FastSubmissionProtection::SubmissionTooFastError) }
56
+
57
+ it 'should start the timer' do
58
+ subject rescue nil
59
+ storage[key].should == now
60
+ end
61
+ end
62
+
63
+ context 'when the timer is started' do
64
+ before { timer.start }
65
+
66
+ context 'and not enough time has passed' do
67
+ before { clock.stub(:now).and_return now + delay - 1}
68
+
69
+ it { expect{ subject }.to raise_error(FastSubmissionProtection::SubmissionTooFastError) }
70
+
71
+ it 'should restart the timer' do
72
+ subject rescue nil
73
+ storage[key].should == now + delay - 1
74
+ end
75
+ end
76
+
77
+ context 'and enough time has passed' do
78
+ before { clock.stub(:now).and_return now + delay }
79
+
80
+ it { expect{ subject }.to_not raise_error }
81
+
82
+ it 'should clear the storage' do
83
+ subject
84
+ storage.should_not have_key(key)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'when <delay> is nil (default)' do
91
+ let(:delay) { nil }
92
+
93
+ it 'uses the class self.delay' do
94
+ FastSubmissionProtection::SubmissionTimer.should_receive(:delay)
95
+ subject
96
+ end
97
+ end
98
+
99
+ context 'when <clock> is nil (default)' do
100
+ let(:clock) { nil }
101
+
102
+ it 'uses the class self.clock' do
103
+ FastSubmissionProtection::SubmissionTimer.should_receive(:clock)
104
+ subject
105
+ end
106
+ end
107
+ end
108
+ end
data/spec/spec_helper.rb CHANGED
@@ -5,6 +5,26 @@ if ENV['SIMPLECOV']
5
5
  end
6
6
  end
7
7
 
8
+ ENV['RAILS_ENV'] = 'test'
9
+
8
10
  require 'rails/all'
11
+ require 'rspec'
9
12
  require 'rspec/rails'
10
- require 'fast_submission_protection'
13
+ require_relative '../lib/fast_submission_protection'
14
+
15
+ class Rails::Application::Configuration
16
+ def database_configuration
17
+ {'test' => {'adapter' => 'sqlite3', 'database' => ":memory:"}}
18
+ end
19
+ end
20
+
21
+ module FastSubmissionProtection
22
+ class Application < Rails::Application
23
+ config.active_support.deprecation = :stderr
24
+ end
25
+ end
26
+
27
+ class ApplicationController < ActionController::Base
28
+ end
29
+
30
+ FastSubmissionProtection::Application.initialize!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_submission_protection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-04-03 00:00:00.000000000 Z
13
+ date: 2012-04-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
17
- requirement: &70112464848300 !ruby/object:Gem::Requirement
17
+ requirement: &70119054331820 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '3'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70112464848300
25
+ version_requirements: *70119054331820
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rspec-rails
28
- requirement: &70112464847440 !ruby/object:Gem::Requirement
28
+ requirement: &70119054331160 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,16 +33,27 @@ dependencies:
33
33
  version: '2'
34
34
  type: :development
35
35
  prerelease: false
36
- version_requirements: *70112464847440
36
+ version_requirements: *70119054331160
37
+ - !ruby/object:Gem::Dependency
38
+ name: sqlite3
39
+ requirement: &70119054330560 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *70119054330560
37
48
  description: Reject form submissions based on the time taken to submit them. Version
38
- 0.1.0
49
+ 0.1.1
39
50
  email:
40
51
  - ian@i2wdev.com
41
52
  executables: []
42
53
  extensions: []
43
54
  extra_rdoc_files: []
44
55
  files:
45
- - app/views/fast_submission_protection/error.html
56
+ - app/views/fast_submission_protection/error.html.erb
46
57
  - lib/fast_submission_protection/controller.rb
47
58
  - lib/fast_submission_protection/engine.rb
48
59
  - lib/fast_submission_protection/submission_timer.rb
@@ -52,7 +63,9 @@ files:
52
63
  - Rakefile
53
64
  - README.md
54
65
  - CHANGELOG
55
- - spec/controllers/controller_spec.rb
66
+ - spec/controllers/integration_spec.rb
67
+ - spec/fast_submission_protection/filters_spec.rb
68
+ - spec/fast_submission_protection/submission_timer_spec.rb
56
69
  - spec/spec_helper.rb
57
70
  homepage: http://github.com/i2w/timed_spam_rejection
58
71
  licenses: []
@@ -68,7 +81,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
81
  version: '0'
69
82
  segments:
70
83
  - 0
71
- hash: 3131108100916053125
84
+ hash: -2459182796522294369
72
85
  required_rubygems_version: !ruby/object:Gem::Requirement
73
86
  none: false
74
87
  requirements:
@@ -77,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
90
  version: '0'
78
91
  segments:
79
92
  - 0
80
- hash: 3131108100916053125
93
+ hash: -2459182796522294369
81
94
  requirements: []
82
95
  rubyforge_project:
83
96
  rubygems_version: 1.8.10
@@ -85,5 +98,7 @@ signing_key:
85
98
  specification_version: 3
86
99
  summary: Reject form submissions based on the time taken to submit them
87
100
  test_files:
88
- - spec/controllers/controller_spec.rb
101
+ - spec/controllers/integration_spec.rb
102
+ - spec/fast_submission_protection/filters_spec.rb
103
+ - spec/fast_submission_protection/submission_timer_spec.rb
89
104
  - spec/spec_helper.rb