From: Pirmin Kalberer <pirmin.kalberer@gmail.com>
To: rack-devel@googlegroups.com
Subject: [Patch] Rack::Access - Limit access based on IP address
Date: Fri, 15 Jan 2010 00:05:30 +0100 [thread overview]
Message-ID: <201001150005.30796.pirmin.kalberer@gmail.com> (raw)
[-- Attachment #1: Type: text/plain, Size: 378 bytes --]
Hi all,
For implementing a monitoring rack middleware I had to limit access to certain
URL's based on IP adresses. The attached patch for rack-contrib implements
this. It borrows some ideas from rack-bug and code from Rack::URLMap...
-- Pull URL:
git://github.com/pka/rack-contrib.git
Pirmin Kalberer
Sourcepole - Linux & Open Source Solutions
http://www.sourcepole.com
[-- Attachment #2: rack_access.patch --]
[-- Type: text/x-patch, Size: 10111 bytes --]
diff --git a/README.rdoc b/README.rdoc
index 3583555..ca015ae 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -45,6 +45,7 @@ interface:
* Rack::AcceptFormat - Adds a format extension at the end of the URI when there is none, corresponding to the mime-type given in the Accept HTTP header.
* Rack::HostMeta - Configures /host-meta using a block
* Rack::Cookies - Adds simple cookie jar hash to env
+* Rack::Access - Limit access based on IP address
=== Use
diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb
index 6252b26..8905437 100644
--- a/lib/rack/contrib.rb
+++ b/lib/rack/contrib.rb
@@ -8,6 +8,7 @@ module Rack
end
autoload :AcceptFormat, "rack/contrib/accept_format"
+ autoload :Access, "rack/contrib/access"
autoload :BounceFavicon, "rack/contrib/bounce_favicon"
autoload :Cookies, "rack/contrib/cookies"
autoload :CSSHTTPRequest, "rack/contrib/csshttprequest"
diff --git a/lib/rack/contrib/access.rb b/lib/rack/contrib/access.rb
new file mode 100644
index 0000000..2f12065
--- /dev/null
+++ b/lib/rack/contrib/access.rb
@@ -0,0 +1,85 @@
+require "ipaddr"
+
+module Rack
+
+ ##
+ # Rack middleware for limiting access based on IP address
+ #
+ #
+ # === Options:
+ #
+ # path => ipmasks ipmasks: Array of remote addresses which are allowed to access
+ #
+ # === Examples:
+ #
+ # use Rack::Access, '/backend' => [ '127.0.0.1', '192.168.1.0/24' ]
+ #
+ #
+
+ class Access
+
+ attr_reader :options
+
+ def initialize(app, options = {})
+ @app = app
+ mapping = options.empty? ? {"/" => ["127.0.0.1"]} : options
+ @mapping = remap(mapping)
+ end
+
+ def remap(mapping)
+ mapping.map { |location, ipmasks|
+ if location =~ %r{\Ahttps?://(.*?)(/.*)}
+ host, location = $1, $2
+ else
+ host = nil
+ end
+
+ unless location[0] == ?/
+ raise ArgumentError, "paths need to start with /"
+ end
+ location = location.chomp('/')
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
+
+ ipmasks.collect! do |ipmask|
+ ipmask.is_a?(IPAddr) ? ipmask : IPAddr.new(ipmask)
+ end
+ [host, location, match, ipmasks]
+ }.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
+ end
+
+ def call(env)
+ @original_request = Request.new(env)
+ ipmasks = ipmasks_for_path(env)
+ return forbidden! unless ip_authorized?(ipmasks)
+ status, headers, body = @app.call(env)
+ [status, headers, body]
+ end
+
+ def ipmasks_for_path(env)
+ path = env["PATH_INFO"].to_s
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
+ @mapping.each do |host, location, match, ipmasks|
+ next unless (hHost == host || sName == host \
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
+ next unless path =~ match && rest = $1
+ next unless rest.empty? || rest[0] == ?/
+
+ return ipmasks
+ end
+ nil
+ end
+
+ def forbidden!
+ [403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, '']
+ end
+
+ def ip_authorized?(ipmasks)
+ return true if ipmasks.nil?
+
+ ipmasks.any? do |ip_mask|
+ ip_mask.include?(IPAddr.new(@original_request.ip))
+ end
+ end
+
+ end
+end
diff --git a/rack-contrib.gemspec b/rack-contrib.gemspec
index c21fa67..498b8d4 100644
--- a/rack-contrib.gemspec
+++ b/rack-contrib.gemspec
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.name = 'rack-contrib'
- s.version = '0.9.2'
- s.date = '2009-03-07'
+ s.version = '0.9.3'
+ s.date = '2010-01-10'
s.description = "Contributed Rack Middleware and Utilities"
s.summary = "Contributed Rack Middleware and Utilities"
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
Rakefile
lib/rack/contrib.rb
lib/rack/contrib/accept_format.rb
+ lib/rack/contrib/access.rb
lib/rack/contrib/backstage.rb
lib/rack/contrib/bounce_favicon.rb
lib/rack/contrib/callbacks.rb
@@ -49,6 +50,7 @@ Gem::Specification.new do |s|
test/Maintenance.html
test/mail_settings.rb
test/spec_rack_accept_format.rb
+ test/spec_rack_access.rb
test/spec_rack_backstage.rb
test/spec_rack_callbacks.rb
test/spec_rack_config.rb
diff --git a/test/spec_rack_access.rb b/test/spec_rack_access.rb
new file mode 100644
index 0000000..53a80a8
--- /dev/null
+++ b/test/spec_rack_access.rb
@@ -0,0 +1,154 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/contrib/access'
+
+context "Rack::Access" do
+
+ setup do
+ @app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, 'hello'] }
+ @mock_addr_1 = '111.111.111.111'
+ @mock_addr_2 = '192.168.1.222'
+ @mock_addr_localhost = '127.0.0.1'
+ @mock_addr_range = '192.168.1.0/24'
+ end
+
+ def mock_env(remote_addr, path = '/')
+ Rack::MockRequest.env_for(path, { 'REMOTE_ADDR' => remote_addr })
+ end
+
+ def middleware(options = {})
+ Rack::Access.new(@app, options)
+ end
+
+ specify "default configuration should deny non-local requests" do
+ app = middleware
+ status, headers, body = app.call(mock_env(@mock_addr_1))
+ status.should.equal 403
+ body.should.equal ''
+ end
+
+ specify "default configuration should allow requests from 127.0.0.1" do
+ app = middleware
+ status, headers, body = app.call(mock_env(@mock_addr_localhost))
+ status.should.equal 200
+ body.should.equal 'hello'
+ end
+
+ specify "should allow remote addresses in allow_ipmasking" do
+ app = middleware('/' => [@mock_addr_1])
+ status, headers, body = app.call(mock_env(@mock_addr_1))
+ status.should.equal 200
+ body.should.equal 'hello'
+ end
+
+ specify "should deny remote addresses not in allow_ipmasks" do
+ app = middleware('/' => [@mock_addr_1])
+ status, headers, body = app.call(mock_env(@mock_addr_2))
+ status.should.equal 403
+ body.should.equal ''
+ end
+
+ specify "should allow remote addresses in allow_ipmasks range" do
+ app = middleware('/' => [@mock_addr_range])
+ status, headers, body = app.call(mock_env(@mock_addr_2))
+ status.should.equal 200
+ body.should.equal 'hello'
+ end
+
+ specify "should deny remote addresses not in allow_ipmasks range" do
+ app = middleware('/' => [@mock_addr_range])
+ status, headers, body = app.call(mock_env(@mock_addr_1))
+ status.should.equal 403
+ body.should.equal ''
+ end
+
+ specify "should allow remote addresses in one of allow_ipmasking" do
+ app = middleware('/' => [@mock_addr_range, @mock_addr_localhost])
+
+ status, headers, body = app.call(mock_env(@mock_addr_2))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_localhost))
+ status.should.equal 200
+ body.should.equal 'hello'
+ end
+
+ specify "should deny remote addresses not in one of allow_ipmasks" do
+ app = middleware('/' => [@mock_addr_range, @mock_addr_localhost])
+ status, headers, body = app.call(mock_env(@mock_addr_1))
+ status.should.equal 403
+ body.should.equal ''
+ end
+
+ specify "handles paths correctly" do
+ app = middleware({
+ 'http://foo.org/bar' => [@mock_addr_localhost],
+ '/foo' => [@mock_addr_localhost],
+ '/foo/bar' => [@mock_addr_range, @mock_addr_localhost]
+ })
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/qux"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/bar"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/bar"))
+ status.should.equal 200
+ body.should.equal 'hello'
+ status, headers, body = app.call(mock_env(@mock_addr_2, "/foo/bar"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/bar/"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/bar/"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo///bar//quux"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo///bar//quux"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/foo/quux"))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/foo/quux"))
+ status.should.equal 200
+ body.should.equal 'hello'
+
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/bar"))
+ status.should.equal 200
+ body.should.equal 'hello'
+ status, headers, body = app.call(mock_env(@mock_addr_1, "/bar").merge('HTTP_HOST' => 'foo.org'))
+ status.should.equal 403
+ body.should.equal ''
+ status, headers, body = app.call(mock_env(@mock_addr_localhost, "/bar").merge('HTTP_HOST' => 'foo.org'))
+ status.should.equal 200
+ body.should.equal 'hello'
+ end
+end
next reply other threads:[~2010-01-15 5:21 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2010-01-14 23:05 Pirmin Kalberer [this message]
2010-01-15 12:34 ` [Patch] Rack::Access - Limit access based on IP address Ryan Tomayko
2010-01-19 18:58 ` yonghui
2010-01-19 20:08 ` Gaius
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-list from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://groups.google.com/group/rack-devel
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=201001150005.30796.pirmin.kalberer@gmail.com \
--to=rack-devel@googlegroups.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).