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 +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: []
|