xavier-bouncer 0.0.1

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.
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