fast_submission_protection 0.1.0 → 0.1.1

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