fast_submission_protection 0.1.0

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 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