simple-rack-bouncer 0.0.6
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/README.rdoc +57 -0
- data/lib/rack/simple_rack_bouncer.rb +86 -0
- data/lib/simple-rack-bouncer.rb +1 -0
- data/test/bouncer_test.rb +118 -0
- metadata +55 -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:
|
|
13
|
+
|
|
14
|
+
# Require the gem in your Gemfile and then run the bundle command
|
|
15
|
+
gem 'simple-rack-bouncer'
|
|
16
|
+
|
|
17
|
+
# Add the middleware to the stack and configure it
|
|
18
|
+
config.middleware.add Rack::SimpleRackBouncer, :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.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
class SimpleRackBouncer
|
|
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
|
+
@redirect_url = [options[:redirect]].flatten.compact
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(env)
|
|
20
|
+
dup._call(env)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _call(env)
|
|
24
|
+
if access_denied?(env)
|
|
25
|
+
deny_access
|
|
26
|
+
else
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def access_denied?(env)
|
|
32
|
+
ip_denied?(env['REMOTE_ADDR']) || user_agent_denied?(env['HTTP_USER_AGENT'])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ip_denied?(ip)
|
|
36
|
+
ip && @ip_address_checks.any? { |check| match_ip?(check, ip) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def match_ip?(check, ip)
|
|
40
|
+
case check
|
|
41
|
+
when Regexp
|
|
42
|
+
check =~ ip
|
|
43
|
+
when String
|
|
44
|
+
ip[0, check.size] == check
|
|
45
|
+
when Proc
|
|
46
|
+
check.call(ip)
|
|
47
|
+
else
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def user_agent_denied?(ua)
|
|
53
|
+
ua ||= ''
|
|
54
|
+
@user_agent_checks.any? { |check| match_user_agent?(check, ua) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def match_user_agent?(check, ua)
|
|
58
|
+
case check
|
|
59
|
+
when Regexp
|
|
60
|
+
check =~ ua
|
|
61
|
+
when String
|
|
62
|
+
ua == check
|
|
63
|
+
when Proc
|
|
64
|
+
check.call(ua)
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Used for testing
|
|
71
|
+
attr_reader :user_agent_checks
|
|
72
|
+
attr_reader :ip_address_checks
|
|
73
|
+
|
|
74
|
+
protected
|
|
75
|
+
|
|
76
|
+
def deny_access
|
|
77
|
+
return deny_access_via_redirect if @redirect_url
|
|
78
|
+
[403, {"Content-Type" => "text/html"}, ["<h1>403 Forbidden</h1>"]]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deny_access_via_redirect
|
|
82
|
+
[301, {"Location" => @redirect_url}, ["<h1>403 Forbidden</h1>"]]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'rack/simple_rack_bouncer'
|
|
@@ -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 = SimpleRackBouncer.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,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: simple-rack-bouncer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.6
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Ben Damman
|
|
9
|
+
- Xavier Defrang
|
|
10
|
+
autorequire:
|
|
11
|
+
bindir: bin
|
|
12
|
+
cert_chain: []
|
|
13
|
+
date: 2012-04-14 00:00:00.000000000 Z
|
|
14
|
+
dependencies: []
|
|
15
|
+
description: Simple rack middleware for access filtering by IP or User Agent string
|
|
16
|
+
email: ben.damman@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files:
|
|
20
|
+
- README.rdoc
|
|
21
|
+
files:
|
|
22
|
+
- README.rdoc
|
|
23
|
+
- lib/simple-rack-bouncer.rb
|
|
24
|
+
- lib/rack/simple_rack_bouncer.rb
|
|
25
|
+
- test/bouncer_test.rb
|
|
26
|
+
homepage: http://github.com/typesend/simple-rack-bouncer
|
|
27
|
+
licenses: []
|
|
28
|
+
post_install_message:
|
|
29
|
+
rdoc_options:
|
|
30
|
+
- --main
|
|
31
|
+
- README.rdoc
|
|
32
|
+
- --inline-source
|
|
33
|
+
- --charset=UTF-8
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
none: false
|
|
38
|
+
requirements:
|
|
39
|
+
- - ! '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
none: false
|
|
44
|
+
requirements:
|
|
45
|
+
- - ! '>='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubyforge_project:
|
|
50
|
+
rubygems_version: 1.8.22
|
|
51
|
+
signing_key:
|
|
52
|
+
specification_version: 3
|
|
53
|
+
summary: Simple rack middleware for access filtering by IP address or User Agent string
|
|
54
|
+
test_files:
|
|
55
|
+
- test/bouncer_test.rb
|