fast_submission_protection 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ = 0.1.0
2
+ * Renamed to fast_submission_protection
3
+ * Raise an error to handle fast submission
4
+ * better design for starting and finishing submission from within different controllers
5
+
6
+ = 0.0.3
7
+ * Docfix
8
+
9
+ = 0.0.2
10
+ * Adds #reject_fast_create which has some minimal default behaviour, encourages users to override
11
+
12
+ = 0.0.1
13
+ * Initial Release
data/MIT-LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright 2012 Ian White
2
+
3
+ This plugin was developed by Ian White (http://github.com/ianwhite)
4
+ and Nicholas Rutherford (http://github.com/nruth) while working at
5
+ Distinctive Doors (http://distinctivedoors.co.uk) who have kindly
6
+ agreed to release this under the MIT-LICENSE.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the
10
+ "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish,
12
+ distribute, sublicense, and/or sell copies of the Software, and to
13
+ permit persons to whom the Software is furnished to do so, subject to
14
+ the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # FastSubmissionProtection
2
+
3
+ *This is experimental and the API is currently subject to sudden and massive change!*
4
+
5
+ [![Build Status](https://secure.travis-ci.org/i2w/fast_submission_protection.png?branch=master)](http://travis-ci.org/i2w/timed_spam_rejection)
6
+
7
+ ActionController plugin that facilitates rejecting spam based on how long the form submission took.
8
+
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.
10
+
11
+ ## Installation
12
+
13
+ In your Gemfile:
14
+
15
+ gem 'fast_submission_protection'
16
+
17
+ ## Example Usage
18
+
19
+ class FeedbackController < ApplicationController
20
+ protect_from_fast_submission # default delay is 5 seconds, protects create from fast submission
21
+ end
22
+
23
+ class CommentsController < ApplicationController
24
+ # protects a Comment#update from happening too quickly, and rescues with custom behaviour
25
+ protect_from_fast_submission :delay => 10, :start => [:edit, :update], :finish => [:update], :rescue => false
26
+
27
+ rescue_from FastSubmissionProtection::SubmissionTooFastError, :with => lambda {|c| c.redirect_to :edit, :alert => 'Don't comment in anger!' }
28
+ end
29
+
30
+ See `FastSubmissionProtection::Controller#protect_from_fast_submission` for more details.
31
+
32
+ ## Filters
33
+
34
+ You can start and finish the timed submission in different controllers, just set up the filters manually:
35
+
36
+ class WelcomeController < ApplicationController
37
+ before_filter FastSubmissionProtection::StartFilter.new('abused_form'), :only => :feedback_form
38
+ end
39
+
40
+ class FeedbackController < ApplicationController
41
+ before_filter FastSubmissionProtection::FinishFilter.new('abused_form'), :only => :feedback
42
+ end
43
+
44
+ ## Instance methods
45
+
46
+ You can start and finish at any point within a controller
47
+
48
+ start_timed_submission 'abused_form'
49
+ finish_timed_submission 'abused_form', 20 # raises FastSubmissionProtection::SubmissionTooFastError if the above line was < 20 seconds ago
50
+
51
+ Other methods, like reset timer, and clear timer are available on the timer object
52
+
53
+ submission_timer('abused_form') # => a FastSubmissionProtection::SubmissionTimer
54
+
55
+ ## Rescue
56
+
57
+ if you include FastSubmissionProtection::Rescue, the error is rescued with an error page with HTTP status 420 (enhance your calm).
58
+ This is included by default when you specify `protect_from_fast_submission`.
59
+
60
+ 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
+
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.
63
+
64
+ ## Development
65
+
66
+ Grab the project, use the last known good set of deps, and run the specs:
67
+
68
+ git clone http://github.com/i2w/fast_submission_protection
69
+ cd fast_submission_protection
70
+ cp Gemfile.lock.development Gemfile.lock
71
+ bundle
72
+ rake
73
+
74
+ ## License
75
+
76
+ This project uses the MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'TimedSpamRejection'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.md', 'CHANGELOG', 'MIT-LICENSE')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ Bundler::GemHelper.install_tasks
24
+
25
+ require 'rspec/core/rake_task'
26
+
27
+ RSpec::Core::RakeTask.new(:spec)
28
+
29
+ desc "Run the specs with simplecov"
30
+ task :simplecov => [:simplecov_env, :spec]
31
+ task :simplecov_env do ENV['SIMPLECOV'] = '1' end
32
+
33
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Enhance your calm (420)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <div class="dialog">
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>
23
+ <p>This is an anti-spam measure.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,113 @@
1
+ module FastSubmissionProtection
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # protects a create action from fast_submission
7
+ #
8
+ # This checks the time taken between the form being rendered (by new, or failed create), and
9
+ # the form being posted to the create action. If it is less than the specified time, an
10
+ # error is raised, which is rescued with a basic 420 error page (enhance your calm) which
11
+ # invites the user to click their back button, wait 5 seconds, and try again.
12
+ #
13
+ # Options:
14
+ # * :name: The name of the submission (default "#{controller_name}_create")
15
+ # * :delay: The time to wait (default 5 seconds)
16
+ # * :start: List of actions when the timer should be started (default [:new, :create])
17
+ # * :finish: List of actions when the timer should be finished (default [:create])
18
+ # * :rescue: Rescue SubmissionTooFast error with a 420.html error page (default true)
19
+ #
20
+ # If your submission starts in one controller and finishes in another, you can start the
21
+ # timer wherever you like, as follows
22
+ #
23
+ # # At the class level, ie specifying a filter where the submission ends
24
+ # before_filter FastSubmissionProtection::FinishFilter.new('abused_form_post'), :only => [:create]
25
+ # # and where the submission starts
26
+ # before_filter FastSubmissionProtection::StartFilter.new('abused_form_post'), :only => [:new]
27
+ #
28
+ # # At the instance level, wherever you want, perhaps in an action
29
+ # start_timed_submission('often_abused_form_post') # to start
30
+ #
31
+ # # later, somewhere else
32
+ # finish_timed_submission('often_abused_form_post') # to finsih, raises SubmissionTooFastError if too fast
33
+ def protect_from_fast_submission options = {}
34
+ delay = options[:delay]
35
+ start = options[:start] || [:new, :create]
36
+ finish = options[:finish] || [:create]
37
+ name = options[:name] || "#{controller_name}_#{Array(finish).join('_')}"
38
+
39
+ include Rescue unless options[:rescue] == false || self < Rescue
40
+
41
+ before_filter FinishFilter.new(name, delay), :only => finish
42
+ before_filter StartFilter.new(name), :only => start
43
+ end
44
+ end
45
+
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
64
+ end
65
+ end
66
+
67
+ protected
68
+ def submission_timer name, delay = nil
69
+ SubmissionTimer.new timed_submission_storage, name, delay
70
+ end
71
+
72
+ def protect_from_fast_submission?
73
+ request.post? || request.put?
74
+ end
75
+
76
+ def timed_submission_storage
77
+ session[:_fsp] ||= {}
78
+ end
79
+ end
80
+
81
+ class StartFilter < Struct.new(:name)
82
+ def filter controller
83
+ controller.start_timed_submission name
84
+ end
85
+ end
86
+
87
+ class FinishFilter < Struct.new(:name, :delay)
88
+ 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
96
+ end
97
+
98
+ attr_reader :name, :delay
99
+ end
100
+
101
+ module Rescue
102
+ extend ActiveSupport::Concern
103
+
104
+ included do
105
+ rescue_from FastSubmissionProtection::SubmissionTooFastError, :with => :render_fast_submission_protection_error
106
+ end
107
+
108
+ protected
109
+ def render_fast_submission_protection_error
110
+ render :template => 'fast_submission_protection/error', :layout => false, :status => 420
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,8 @@
1
+ module FastSubmissionProtection
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
5
+
6
+ ActiveSupport.on_load(:action_controller) do
7
+ include FastSubmissionProtection::Controller
8
+ end
@@ -0,0 +1,30 @@
1
+ module FastSubmissionProtection
2
+ class SubmissionTimer
3
+ class_attribute :delay, :clock
4
+ self.delay = 5
5
+ self.clock = Time
6
+
7
+ def initialize storage, key, delay = nil, clock = nil
8
+ @storage, @key = storage, key
9
+ @delay = delay || self.class.delay
10
+ @clock = clock || self.class.clock
11
+ end
12
+
13
+ def too_fast?
14
+ started = @storage[@key]
15
+ !started || (started + @delay > @clock.now)
16
+ end
17
+
18
+ def start
19
+ @storage[@key] ||= @clock.now
20
+ end
21
+
22
+ def restart
23
+ @storage[@key] = @clock.now
24
+ end
25
+
26
+ def clear
27
+ @storage.delete @key
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module FastSubmissionProtection
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'fast_submission_protection/submission_timer'
2
+ require 'fast_submission_protection/controller'
3
+ require 'fast_submission_protection/engine'
4
+ require 'fast_submission_protection/version'
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
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
+ describe 'A controller with protect_from_fast_submission' do
14
+ controller do
15
+ protect_from_fast_submission :name => 'test', :rescue => false
16
+
17
+ def new
18
+ render_new
19
+ end
20
+
21
+ def create
22
+ if params[:create_failed]
23
+ render_new
24
+ else
25
+ created
26
+ render_create
27
+ end
28
+ end
29
+
30
+ def index
31
+ render :nothing => true
32
+ end
33
+
34
+ def render_new
35
+ render :nothing => true
36
+ end
37
+
38
+ def render_create
39
+ render :nothing => true
40
+ end
41
+
42
+ def created
43
+ end
44
+ end
45
+
46
+ context 'post :create' do
47
+ subject { do_post }
48
+
49
+ def do_post *args
50
+ post :create, *args
51
+ end
52
+
53
+ # The only thing we're stubbing in this integration spec is the time
54
+ let(:clock) { double :now => Time.now }
55
+ before do FastSubmissionProtection::SubmissionTimer.clock = clock end
56
+
57
+ def seconds_pass amount
58
+ now = clock.now + amount.seconds
59
+ clock.stub(:now).and_return now
60
+ end
61
+
62
+ shared_examples_for 'a spammy form posting' do
63
+ it do expect{ subject }.to raise_error FastSubmissionProtection::SubmissionTooFastError end
64
+
65
+ it 'should not execute create' do
66
+ controller.should_not_receive(:created)
67
+ end
68
+ end
69
+
70
+ shared_examples_for 'a non spammy form posting' do
71
+ it 'should execute :create' do
72
+ controller.should_receive(:created)
73
+ subject
74
+ end
75
+ end
76
+
77
+ context 'when get :new is not the previous action' do
78
+ it_should_behave_like 'a spammy form posting'
79
+ end
80
+
81
+ context 'after get :new' do
82
+ before do
83
+ get :new
84
+ end
85
+
86
+ context 'when enough time has passed' do
87
+ before do seconds_pass(6) end
88
+
89
+ it_should_behave_like 'a non spammy form posting'
90
+
91
+ context 'but the create fails for another reason, and new is rendered' do
92
+ before do
93
+ do_post :create_failed => true
94
+ end
95
+
96
+ context 'and enough time passes' do
97
+ before do seconds_pass(6) end
98
+
99
+ it_should_behave_like 'a non spammy form posting'
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'and another action is requested in the meantime' do
105
+ before do
106
+ get :index
107
+ end
108
+
109
+ context 'and enough time has passed' do
110
+ before do seconds_pass(6) end
111
+
112
+ it_should_behave_like 'a non spammy form posting'
113
+ end
114
+ end
115
+
116
+ context 'when not enough time has passed' do
117
+ before do seconds_pass(4) end
118
+
119
+ it_should_behave_like 'a spammy form posting'
120
+
121
+ context 'and not enough time passes again' do
122
+ before do
123
+ do_post rescue FastSubmissionProtection::SubmissionTooFastError
124
+ seconds_pass(4)
125
+ end
126
+
127
+ it_should_behave_like 'a spammy form posting'
128
+
129
+ context 'but then enough time passes' do
130
+ before do
131
+ do_post rescue FastSubmissionProtection::SubmissionTooFastError
132
+ seconds_pass(6)
133
+ end
134
+
135
+ it_should_behave_like 'a non spammy form posting'
136
+
137
+ context 'but then I post again immediately' do
138
+ before do
139
+ do_post rescue FastSubmissionProtection::SubmissionTooFastError
140
+ seconds_pass(1)
141
+ end
142
+
143
+ it_should_behave_like 'a spammy form posting'
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,10 @@
1
+ if ENV['SIMPLECOV']
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter "_spec.rb"
5
+ end
6
+ end
7
+
8
+ require 'rails/all'
9
+ require 'rspec/rails'
10
+ require 'fast_submission_protection'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_submission_protection
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ian White
9
+ - Nicholas Rutherford
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-04-03 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ requirement: &70112464848300 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70112464848300
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec-rails
28
+ requirement: &70112464847440 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *70112464847440
37
+ description: Reject form submissions based on the time taken to submit them. Version
38
+ 0.1.0
39
+ email:
40
+ - ian@i2wdev.com
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - app/views/fast_submission_protection/error.html
46
+ - lib/fast_submission_protection/controller.rb
47
+ - lib/fast_submission_protection/engine.rb
48
+ - lib/fast_submission_protection/submission_timer.rb
49
+ - lib/fast_submission_protection/version.rb
50
+ - lib/fast_submission_protection.rb
51
+ - MIT-LICENSE
52
+ - Rakefile
53
+ - README.md
54
+ - CHANGELOG
55
+ - spec/controllers/controller_spec.rb
56
+ - spec/spec_helper.rb
57
+ homepage: http://github.com/i2w/timed_spam_rejection
58
+ licenses: []
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ segments:
70
+ - 0
71
+ hash: 3131108100916053125
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ segments:
79
+ - 0
80
+ hash: 3131108100916053125
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.10
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Reject form submissions based on the time taken to submit them
87
+ test_files:
88
+ - spec/controllers/controller_spec.rb
89
+ - spec/spec_helper.rb