wait_a_minute 0.0.1 → 0.0.2

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