wait_a_minute 0.0.1 → 0.0.2

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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in wait_a_minute.gemspec
4
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 György (George) Schreiber (gyDotschreiberAtmobilityDothu)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,77 @@
1
+ == WaitAMinute
2
+
3
+ A simple, application level DOS (Denial-Of-Service) protection tool for
4
+ small Rails applications.
5
+
6
+ == How does it work?
7
+
8
+ Once WaitAMinute gem is installed and configured in a Rails application, it
9
+ will run a before_filter on *every* call to any subclasses of ActionController.
10
+
11
+ The before hook checks if the IP is a 'friendly' one (eg. NewRelic monitoring),
12
+ meaning it is allowed no matter what.
13
+ For 'non-friendly' IP addresses, the before filter checks if the IP is not already
14
+ banned within a minute (hence the name of the gem), if not, it checks the number
15
+ of previous requests from the given address within a configurable floating timeframe
16
+ and allows the request to be served only if the address did not exceed the allowed
17
+ maximum requests within the timeframe. WaitAMinute then stores the request IP and
18
+ the timestamp along with a bit indicating if the request was refused or not.
19
+
20
+ If the IP is banned, WaitAMinute renders a page with HTTP status 503 telling the
21
+ server is too busy to handle the request and that the user should retry after a minute.
22
+
23
+ == Installation
24
+
25
+ add the gem to your Gemfile then
26
+ # bundle install
27
+
28
+ == Configuration
29
+
30
+ in your application's root directory,
31
+
32
+ run
33
+ # rails g wait_a_minute:install
34
+
35
+ then
36
+ # rake db:migrate
37
+
38
+ finally revise and tweak
39
+ config/initializers/wait_a_minute.rb
40
+
41
+ WaitAMinute.lookback_interval - the floating timeframe size,
42
+ eg. 2.minutes
43
+
44
+ WaitAMinute.maximum_requests - the max number of requests from a single IP
45
+ within the timeframe, eg. 24 - along with the above it allows a request
46
+ every 5 seconds from a single IP address
47
+
48
+ WaitAMinute.debug - set to true for having IP filtering logged
49
+
50
+ WaitAMinute.layout - if some layout is needed around the error page, specify it
51
+ here /for best performance it is not recommended, we want banned IP's to use
52
+ the least resources/
53
+
54
+ WaitAMinute.allowed_ips - an array of strings with IP addresses that never should
55
+ be banned, eg, ['127.0.0.1'] (once tried that it works ok on the development box,
56
+ likely want the local developer to pass through always)
57
+
58
+ == Customization
59
+
60
+ create app/views/wait_a_minute/wait_a_minute.html.erb and customize to your
61
+ liking to override the default error page for banned IP addresses.
62
+
63
+ == Maintenance
64
+
65
+ from time to time WaitAMinute.cleanup should be called from a scheduled
66
+ script to flush obsolete request logs in order to keep an optimal
67
+ ActiveRecord performance in its filtering operations.
68
+
69
+ == Further considerations
70
+
71
+ as the piece of software works with the REMOTE_ADDR of the request, it is only
72
+ suitable in environments where it reflects the original request address.
73
+ (eg. it won't work in an environment where a load balancer replaces the
74
+ REMOTE_ADDR address in the request)
75
+
76
+
77
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,5 @@
1
+ <h1>Wait a minute!</h1>
2
+ <p>
3
+ The server is too busy to serve your request. Please try again in a minute.
4
+ <br />
5
+ </p>
@@ -0,0 +1,10 @@
1
+ # Wait A Minute options
2
+
3
+ # allowing a request every 5 seconds per IP
4
+ WaitAMinute.lookback_interval = 2.minutes
5
+ WaitAMinute.maximum_requests = 24
6
+ WaitAMinute.debug = true
7
+ #WaitAMinute.layout = 'application' # using no layout greatly improves performance & response time
8
+ #WaitAMinute.allowed_ips = ['127.0.0.1']
9
+
10
+
@@ -0,0 +1,22 @@
1
+ class CreateWaitAMinuteRequestLogs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :wait_a_minute_request_logs, :id => false do |t|
4
+ t.string :ip
5
+ #t.string :url
6
+ t.boolean :refused, :nil => false, :default => false
7
+ t.datetime :created_at, :nil => false
8
+
9
+ #t.timestamps
10
+ end
11
+ add_index :wait_a_minute_request_logs, :ip
12
+ #add_index :wait_a_minute_request_logs, :url
13
+ add_index :wait_a_minute_request_logs, :created_at
14
+ add_index :wait_a_minute_request_logs, [:ip, :refused]
15
+ add_index :wait_a_minute_request_logs, [:ip, :created_at]
16
+ add_index :wait_a_minute_request_logs, [:ip, :created_at, :refused]
17
+ end
18
+
19
+ def self.down
20
+ drop_table :wait_a_minute_request_logs
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module WaitAMinute
2
+ module Generators
3
+ class InitializerGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ desc "Creates a WaitAMinute initializer in your application."
7
+ class_option :orm
8
+
9
+ def copy_initializer
10
+ template "initializer.rb", "config/initializers/wait_a_minute.rb"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,18 @@
1
+ module WaitAMinute
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ desc "Creates a WaitAMinute initializer and migration in your application."
7
+ class_option :orm
8
+
9
+ def copy_initializer
10
+ template 'initializer.rb', "config/initializers/wait_a_minute.rb"
11
+ end
12
+
13
+ def copy_migration
14
+ template 'migration.rb', "db/migrate/#{Time.new.strftime("%Y%m%d%H%M%S")}_create_wait_a_minute.rb"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module WaitAMinute
2
+ module Generators
3
+ class MigrationGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ desc "Creates a WaitAMinute migration in your application."
7
+ class_option :orm
8
+
9
+ def copy_migration
10
+ template 'migration.rb', "db/migrate/#{Time.new.strftime("%Y%m%d%H%M%S")}_create_wait_a_minute.rb"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module WaitAMinute
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_record'
2
+ require 'active_support/configurable'
3
+
4
+ module WaitAMinute
5
+ class WaitAMinuteRequestLog < ActiveRecord::Base
6
+
7
+ def self.allow_request?(req = nil)
8
+ return false unless req && (req.is_a?(ActionDispatch::Request) || req.is_a?(ActionController::TestRequest))
9
+ allow = false
10
+ ip = req.env['REMOTE_ADDR']
11
+ if WaitAMinute.allowed_ips.include?(ip)
12
+ Rails.logger.debug("IP #{ip} PASSING THROUGH WITHOUT REQUEST QUOTA CHECK...") if WaitAMinute.debug
13
+ allow = true
14
+ else
15
+ interval = WaitAMinute.lookback_interval
16
+ rinterval = 1.minutes
17
+ limit = WaitAMinute.maximum_requests
18
+
19
+ reqcount = nil
20
+ allow = self.where(:ip => ip, :refused => true).where('created_at > ?', (Time.new - rinterval) ).count == 0
21
+ allow &= (limit > (reqcount = self.where(:ip => ip).where('created_at > ?', (Time.new - interval) ).count)) if allow
22
+ newreq = self.create( :ip => ip, :refused => !allow )
23
+
24
+ Rails.logger.debug("IP #{ip} HAD #{reqcount} REQUESTS IN TIME WINDOW (max #{WaitAMinute.maximum_requests})...") if WaitAMinute.debug && !reqcount.nil?
25
+ Rails.logger.debug("IP #{ip} HAD ALREADY REFUSED REQUESTS IN TIME WINDOW...HE WON'T STOP") if WaitAMinute.debug && reqcount.nil?
26
+ end
27
+ allow
28
+ end
29
+
30
+ def self.cleanup
31
+ sql = ActiveRecord::Base.connection()
32
+
33
+ sql.execute "SET autocommit=0"
34
+ sql.begin_db_transaction
35
+ sql.delete("DELETE FROM `#{self.name.to_s.underscore.downcase.gsub(/[a-z0-9_]+\//,'').pluralize}` WHERE created_at < '#{Time.new - WaitAMinute.lookback_interval}'")
36
+ sql.commit_db_transaction
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_support/core_ext/numeric/time'
2
+ require 'active_support/dependencies'
3
+
4
+ module WaitAMinute
5
+ # The interval for looking back
6
+ mattr_accessor :lookback_interval
7
+ @@lookback_interval = 2.minutes
8
+
9
+ # The maximum allowed requests in lookback_interval
10
+ mattr_accessor :maximum_requests
11
+ @@maximum_requests = 12 # that means a request every 10 seconds on average, that's pretty high from a human behind the IP
12
+
13
+ # Debug mode (puts some info in Rails log)
14
+ mattr_accessor :debug
15
+ @@debug = false # that means a request every 10 seconds on average, that's pretty high from a human behind the IP
16
+
17
+ # The desired layout for the 503 error page
18
+ mattr_accessor :layout
19
+ @@layout = nil # serving a simple page by default
20
+
21
+ # Array of strings containing IP addresses that are allowed to pass through
22
+ mattr_accessor :allowed_ips
23
+ @@allowed_ips = []
24
+
25
+
26
+
27
+ autoload :WaitAMinuteRequestLog, File.dirname(__FILE__) + '/wait_a_minute/wait_a_minute_request_log.rb'
28
+
29
+ require "rails"
30
+
31
+ class Engine < Rails::Engine
32
+ # we have a view
33
+ end
34
+
35
+ def self.cleanup
36
+ WaitAMinuteRequestLog.cleanup
37
+ end
38
+
39
+
40
+ # WaitAMinute::ControllerHelpers
41
+ module ControllerHelpers #:nodoc
42
+ # this will be called as a before filter
43
+ def prevent_dos
44
+ render_dos unless WaitAMinuteRequestLog.allow_request?(request)
45
+ end
46
+
47
+ # this will be called if IP exceeds allowed calls
48
+ def render_dos
49
+ respond_to do |type|
50
+ type.html { render :template => "wait_a_minute/wait_a_minute", :status => 503, :layout => WaitAMinute.layout }
51
+ type.all { render :nothing => true, :status => 503 }
52
+ end
53
+ true
54
+ end
55
+
56
+ end # WaitAMinute::ControllerHelpers
57
+
58
+ end # WaitAMinute
59
+
60
+
61
+ # include view helpers in your actions
62
+ ActionController::Base.module_eval do
63
+ include WaitAMinute::ControllerHelpers
64
+ before_filter :prevent_dos
65
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "wait_a_minute/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "wait_a_minute"
7
+ s.version = WaitAMinute::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["George Schreiber"]
10
+ s.email = ["gy.schreiber@mobility.hu"]
11
+ s.homepage = ""
12
+ s.summary = %q{A very simple DOS (Denial-Of-Service) attack prevention gem}
13
+ s.description = %q{By including this in your app, it can track requests per IP address and refuse processing the request if there were too many requests recently from the given IP address.}
14
+
15
+ s.rubyforge_project = "wait_a_minute"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "rspec", "2.1.0"
23
+ s.add_dependency "rails", ">=3.0.1"
24
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wait_a_minute
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - George Schreiber
@@ -59,8 +59,21 @@ extensions: []
59
59
 
60
60
  extra_rdoc_files: []
61
61
 
62
- files: []
63
-
62
+ files:
63
+ - Gemfile
64
+ - MIT-LICENSE
65
+ - README
66
+ - Rakefile
67
+ - app/views/wait_a_minute/wait_a_minute.html.erb
68
+ - lib/generators/templates/initializer.rb
69
+ - lib/generators/templates/migration.rb
70
+ - lib/generators/wait_a_minute/initializer_generator.rb
71
+ - lib/generators/wait_a_minute/install_generator.rb
72
+ - lib/generators/wait_a_minute/migration_generator.rb
73
+ - lib/wait_a_minute.rb
74
+ - lib/wait_a_minute/version.rb
75
+ - lib/wait_a_minute/wait_a_minute_request_log.rb
76
+ - wait_a_minute.gemspec
64
77
  has_rdoc: true
65
78
  homepage: ""
66
79
  licenses: []