xavier-bouncer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +57 -0
  2. data/lib/bouncer.rb +79 -0
  3. data/test/bouncer_test.rb +118 -0
  4. metadata +59 -0
@@ -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.
@@ -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