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