xavier-bouncer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +57 -0
- data/lib/bouncer.rb +79 -0
- data/test/bouncer_test.rb +118 -0
- metadata +59 -0
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= Bouncer
|
2
|
+
|
3
|
+
A simple blacklist based access filtering middleware for Rack.
|
4
|
+
|
5
|
+
It allows for easy filtering of IP addresses and User Agent strings.
|
6
|
+
This filter is primarily aimed at quickly blocking abusive users or bots.
|
7
|
+
It is suitable for websites with moderate traffic suffering from a handful of abusive clients and
|
8
|
+
will come handy for anyone who doesn't want to fiddle with obscure web server configuration directives.
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
|
12
|
+
For instance, to add this middleware to your Rails app, in <tt>environment.rb</tt>:
|
13
|
+
|
14
|
+
# Require the gem, use rake gems:install to install it
|
15
|
+
config.gem 'xavier-bouncer', :lib => 'bouncer', :source => 'http://gems.github.com'
|
16
|
+
|
17
|
+
# Add the middleware to the stack and configure it
|
18
|
+
config.middleware.add 'Bouncer', :deny_ip_address => ["1.2.3.4", /^10\.0\./], :deny_user_agent => /msnbot/
|
19
|
+
|
20
|
+
The black list can be either a single or an array with a combination of strings, regular expressions and Proc objects.
|
21
|
+
If any of the given conditions are met, the client will be greeted with an HTTP 403 response (access denied).
|
22
|
+
|
23
|
+
|
24
|
+
== Similar Middleware
|
25
|
+
|
26
|
+
If you are after more advanced features, you may want to have a look at:
|
27
|
+
|
28
|
+
- {HttpBL}[http://github.com/bpalmen/httpbl/tree/master], an advanced IP filtering middleware
|
29
|
+
- {rack-useragent}[http://github.com/bebanjo/rack-useragent/tree/master], another User Agent filter
|
30
|
+
- {rack-honeypot}[http://github.com/sunlightlabs/rack-honeypot/tree/master], a spambot trap
|
31
|
+
|
32
|
+
If you are seriously under attack, you may want to have a look at {ModSecurity}[http://www.modsecurity.org/].
|
33
|
+
|
34
|
+
== MIT Licence
|
35
|
+
|
36
|
+
Copyright (c) 2009 Xavier Defrang
|
37
|
+
|
38
|
+
Permission is hereby granted, free of charge, to any person
|
39
|
+
obtaining a copy of this software and associated documentation
|
40
|
+
files (the "Software"), to deal in the Software without
|
41
|
+
restriction, including without limitation the rights to use,
|
42
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
43
|
+
copies of the Software, and to permit persons to whom the
|
44
|
+
Software is furnished to do so, subject to the following
|
45
|
+
conditions:
|
46
|
+
|
47
|
+
The above copyright notice and this permission notice shall be
|
48
|
+
included in all copies or substantial portions of the Software.
|
49
|
+
|
50
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
51
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
52
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
53
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
54
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
55
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
56
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
57
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/bouncer.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
class Bouncer
|
3
|
+
|
4
|
+
# See configure for available options
|
5
|
+
def initialize(app, options = {})
|
6
|
+
@app = app
|
7
|
+
configure(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Accepted options are: :deny_user_agents and :deny_ip_address
|
11
|
+
# Both accept a single or an array of String, Regexp or Proc objects
|
12
|
+
# If a string is given, IP address will be matched by prefix (i.e. '127.0' will match '127.0.0.1' and '127.0.2.1')
|
13
|
+
def configure(options = {})
|
14
|
+
@user_agent_checks = [options[:deny_user_agent]].flatten.compact
|
15
|
+
@ip_address_checks = [options[:deny_ip_address]].flatten.compact
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
dup._call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
def _call(env)
|
23
|
+
if access_denied?(env)
|
24
|
+
deny_access
|
25
|
+
else
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def access_denied?(env)
|
31
|
+
ip_denied?(env['REMOTE_ADDR']) || user_agent_denied?(env['HTTP_USER_AGENT'])
|
32
|
+
end
|
33
|
+
|
34
|
+
def ip_denied?(ip)
|
35
|
+
ip && @ip_address_checks.any? { |check| match_ip?(check, ip) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def match_ip?(check, ip)
|
39
|
+
case check
|
40
|
+
when Regexp
|
41
|
+
check =~ ip
|
42
|
+
when String
|
43
|
+
ip[0, check.size] == check
|
44
|
+
when Proc
|
45
|
+
check.call(ip)
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_agent_denied?(ua)
|
52
|
+
ua ||= ''
|
53
|
+
@user_agent_checks.any? { |check| match_user_agent?(check, ua) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def match_user_agent?(check, ua)
|
57
|
+
case check
|
58
|
+
when Regexp
|
59
|
+
check =~ ua
|
60
|
+
when String
|
61
|
+
ua == check
|
62
|
+
when Proc
|
63
|
+
check.call(ua)
|
64
|
+
else
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Used for testing
|
70
|
+
attr_reader :user_agent_checks
|
71
|
+
attr_reader :ip_address_checks
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
def deny_access
|
76
|
+
[403, {"Content-Type" => "text/html"}, ["<h1>403 Forbidden</h1>"]]
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
|
2
|
+
require 'test/unit'
|
3
|
+
require File.join(File.dirname(__FILE__), '../lib/bouncer.rb')
|
4
|
+
|
5
|
+
class App
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
[200, {"Content-Type" => "text/plain"}, "OK"]
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class BouncerTest < Test::Unit::TestCase
|
14
|
+
|
15
|
+
EXAMPLE_CONFIGURATION = {
|
16
|
+
:deny_ip_address => ['127.0.0.1', /^10\.\d+\.\d+\.\d+/, lambda { |ip| ip =~ /\.13$/ } ],
|
17
|
+
:deny_user_agent => ["", "msnbot", /daodao/, lambda { |ua| ua.length == 3 } ],
|
18
|
+
}
|
19
|
+
|
20
|
+
def setup
|
21
|
+
@app = App.new
|
22
|
+
@bouncer = Bouncer.new(@app)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_default_options
|
26
|
+
default_status_assertions
|
27
|
+
assert_nothing_raised { @bouncer.call({}) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_configure_with_no_options
|
31
|
+
@bouncer.configure
|
32
|
+
default_status_assertions
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_configure
|
36
|
+
@bouncer.configure(EXAMPLE_CONFIGURATION)
|
37
|
+
assert_equal 3, @bouncer.ip_address_checks.size
|
38
|
+
assert_equal 4, @bouncer.user_agent_checks.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_match_ip
|
42
|
+
assert @bouncer.match_ip?("127.0.0.1", "127.0.0.1")
|
43
|
+
assert @bouncer.match_ip?("127.0", "127.0.0.1")
|
44
|
+
assert @bouncer.match_ip?("127.0", "127.0.2.1")
|
45
|
+
assert @bouncer.match_ip?(/^127/, "127.0.0.1")
|
46
|
+
assert @bouncer.match_ip?(/^127\.0\.0.\d+/, "127.0.0.1")
|
47
|
+
assert @bouncer.match_ip?(lambda { |ip| ip == '127.0.0.1' }, "127.0.0.1")
|
48
|
+
assert !@bouncer.match_ip?("127.0.0.1", "127.0.0.2")
|
49
|
+
assert !@bouncer.match_ip?("127.0", "127.1.0.0")
|
50
|
+
assert !@bouncer.match_ip?(/10\.0/, "127.1.0.0")
|
51
|
+
assert !@bouncer.match_ip?(lambda { |ip| false }, "127.0.0.1")
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_match_user_agent
|
55
|
+
assert @bouncer.match_user_agent?("daodao larbin@unspecified.email", "daodao larbin@unspecified.email")
|
56
|
+
assert !@bouncer.match_user_agent?("daodao", "daodao larbin@unspecified.email")
|
57
|
+
assert @bouncer.match_user_agent?(/daodao/, "daodao larbin@unspecified.email")
|
58
|
+
assert @bouncer.match_user_agent?(/larbin/, "daodao larbin@unspecified.email")
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_ip_denied
|
62
|
+
@bouncer.configure(EXAMPLE_CONFIGURATION)
|
63
|
+
assert @bouncer.ip_denied?('127.0.0.1')
|
64
|
+
assert @bouncer.ip_denied?('10.2.3.4')
|
65
|
+
assert @bouncer.ip_denied?('10.20.30.40')
|
66
|
+
assert !@bouncer.ip_denied?('127.0.0.2')
|
67
|
+
assert !@bouncer.ip_denied?(nil)
|
68
|
+
assert !@bouncer.ip_denied?('')
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_user_agent_denied
|
72
|
+
@bouncer.configure(EXAMPLE_CONFIGURATION)
|
73
|
+
assert @bouncer.user_agent_denied?('')
|
74
|
+
assert @bouncer.user_agent_denied?(nil)
|
75
|
+
assert @bouncer.user_agent_denied?('msnbot')
|
76
|
+
assert @bouncer.user_agent_denied?('daodao larbin@unspecified.email')
|
77
|
+
assert @bouncer.user_agent_denied?('daodao')
|
78
|
+
assert @bouncer.user_agent_denied?('123')
|
79
|
+
assert !@bouncer.user_agent_denied?('GoogleBot')
|
80
|
+
assert !@bouncer.user_agent_denied?('Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8')
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_allowed_request
|
84
|
+
@bouncer.configure(EXAMPLE_CONFIGURATION)
|
85
|
+
@env = {
|
86
|
+
'REMOTE_ADDR' => '1.2.3.4',
|
87
|
+
'PATH_INFO' => '/path',
|
88
|
+
'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8'
|
89
|
+
}
|
90
|
+
assert_equal 200, @bouncer.call(@env).first
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_denied_request
|
94
|
+
@bouncer.configure(EXAMPLE_CONFIGURATION)
|
95
|
+
@env = {
|
96
|
+
'REMOTE_ADDR' => '1.2.3.4',
|
97
|
+
'PATH_INFO' => '/path',
|
98
|
+
'HTTP_USER_AGENT' => 'msnbot'
|
99
|
+
}
|
100
|
+
assert_equal 403, @bouncer.call(@env).first
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_configuration_with_single_items
|
104
|
+
@bouncer.configure(:deny_ip_address => '127.0.0.1', :deny_user_agent => /msnbot/)
|
105
|
+
assert @bouncer.ip_denied?('127.0.0.1')
|
106
|
+
assert !@bouncer.ip_denied?('10.0.0.1')
|
107
|
+
assert @bouncer.user_agent_denied?('msnbot')
|
108
|
+
assert !@bouncer.user_agent_denied?('Safari')
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
def default_status_assertions
|
114
|
+
assert @bouncer.ip_address_checks.empty?
|
115
|
+
assert @bouncer.user_agent_checks.empty?
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xavier-bouncer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Defrang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Rack middleware for simple access filtering by IP address or User Agent
|
17
|
+
email: xavier.defrang@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- lib/bouncer.rb
|
27
|
+
- test/bouncer_test.rb
|
28
|
+
has_rdoc: true
|
29
|
+
homepage: http://github.com/xavier/bouncer
|
30
|
+
licenses:
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options:
|
33
|
+
- --main
|
34
|
+
- README.rdoc
|
35
|
+
- --inline-source
|
36
|
+
- --charset=UTF-8
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.3.5
|
55
|
+
signing_key:
|
56
|
+
specification_version: 2
|
57
|
+
summary: Rack middleware for simple access filtering by IP address or User Agent
|
58
|
+
test_files:
|
59
|
+
- test/bouncer_test.rb
|