rack-devel archive mirror (unofficial) https://groups.google.com/group/rack-devel
 help / color / mirror / code / Atom feed
* [Patch] Rack::Access - Limit access based on IP address
@ 2010-01-14 23:05 Pirmin Kalberer
  2010-01-15 12:34 ` Ryan Tomayko
  0 siblings, 1 reply; 4+ messages in thread
From: Pirmin Kalberer @ 2010-01-14 23:05 UTC (permalink / raw)
  To: rack-devel

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

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [Patch] Rack::Access - Limit access based on IP address
  2010-01-14 23:05 [Patch] Rack::Access - Limit access based on IP address Pirmin Kalberer
@ 2010-01-15 12:34 ` Ryan Tomayko
  2010-01-19 18:58   ` yonghui
  0 siblings, 1 reply; 4+ messages in thread
From: Ryan Tomayko @ 2010-01-15 12:34 UTC (permalink / raw)
  To: rack-devel

On Thu, Jan 14, 2010 at 3:05 PM, Pirmin Kalberer
<pirmin.kalberer@gmail.com> wrote:
> 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

Merged.

Thanks,
Ryan

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: Rack::Access - Limit access based on IP address
  2010-01-15 12:34 ` Ryan Tomayko
@ 2010-01-19 18:58   ` yonghui
  2010-01-19 20:08     ` Gaius
  0 siblings, 1 reply; 4+ messages in thread
From: yonghui @ 2010-01-19 18:58 UTC (permalink / raw)
  To: Rack Development

Apache 2.2 renamed mod_access to mod_authz_host. Do we need a more
descriptive name for this middleware?

On Jan 15, 7:34 am, Ryan Tomayko <r...@tomayko.com> wrote:
> On Thu, Jan 14, 2010 at 3:05 PM, Pirmin Kalberer
>
> <pirmin.kalbe...@gmail.com> wrote:
> > 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
>
> Merged.
>
> Thanks,
> Ryan

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: Rack::Access - Limit access based on IP address
  2010-01-19 18:58   ` yonghui
@ 2010-01-19 20:08     ` Gaius
  0 siblings, 0 replies; 4+ messages in thread
From: Gaius @ 2010-01-19 20:08 UTC (permalink / raw)
  To: Rack Development

As implemented, it seems Rack::HostWhiteList or
Rack::Auth::HostWhiteList might be a good name. It would be possible
to modify the code to support both white- and black-lists. In that
case, Rack::Auth::HostAccess might work.

On Jan 19, 1:58 pm, yonghui <yonghui....@gmail.com> wrote:
> Apache 2.2 renamed mod_access to mod_authz_host. Do we need a more
> descriptive name for this middleware?
>
> On Jan 15, 7:34 am, Ryan Tomayko <r...@tomayko.com> wrote:
>
> > On Thu, Jan 14, 2010 at 3:05 PM, Pirmin Kalberer
>
> > <pirmin.kalbe...@gmail.com> wrote:
> > > 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
>
> > Merged.
>
> > Thanks,
> > Ryan
>
>

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2010-01-19 20:08 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2010-01-14 23:05 [Patch] Rack::Access - Limit access based on IP address Pirmin Kalberer
2010-01-15 12:34 ` Ryan Tomayko
2010-01-19 18:58   ` yonghui
2010-01-19 20:08     ` Gaius

Code repositories for project(s) associated with this inbox:

	https://80x24.org/mirrors/rack.git

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