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 +4 -0
- data/MIT-LICENSE +20 -0
- data/README +77 -0
- data/Rakefile +2 -0
- data/app/views/wait_a_minute/wait_a_minute.html.erb +5 -0
- data/lib/generators/templates/initializer.rb +10 -0
- data/lib/generators/templates/migration.rb +22 -0
- data/lib/generators/wait_a_minute/initializer_generator.rb +15 -0
- data/lib/generators/wait_a_minute/install_generator.rb +18 -0
- data/lib/generators/wait_a_minute/migration_generator.rb +14 -0
- data/lib/wait_a_minute/version.rb +3 -0
- data/lib/wait_a_minute/wait_a_minute_request_log.rb +39 -0
- data/lib/wait_a_minute.rb +65 -0
- data/wait_a_minute.gemspec +24 -0
- metadata +18 -5
data/Gemfile
ADDED
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,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,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:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
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: []
|