Permalink
Browse files

Add Rack::RelativeRedirect

Transforms relative paths in redirects to absolute URLs.

This allows your web applications to use simple relative paths in
their redirects if they want, so they don't have to worry about
the url scheme, server name, or port.  Absolute URLs are not
modified.  This uses a sensible default based on the environment,
but gives the user full control by allowing them to specify a block
that provides the absolute part of the url (e.g. http://example.org).

Currently, this only takes affect if the response code is 301-303,
I'm not sure if other status codes should be considered, but they
should be easy to add if so.  Also,  this currently only considers
Locations starting with http:// or https:// as absolute URLs.  If
other protocol schemes should be considered, those can be added
later.

This code will fail if the Location includes the server name part
but not the protocol scheme (e.g. Location: //example.org/path).
If those should be allowed, we can do so, but that's also a valid
relative path, so it's not without its problems.

This implementation works with both relative and absolute paths.
If a relative path is given (not starting with a slash), it
is made relative to the request path.  I'm not sure if this works
perfectly, though it passes the specs I wrote.
  • Loading branch information...
1 parent 40dc4d5 commit 43a508f7bdf59c3d1aee2856d71f815ae2056cf1 @jeremyevans jeremyevans committed with rtomayko Feb 9, 2009
Showing with 127 additions and 0 deletions.
  1. +4 −0 README.rdoc
  2. +1 −0 lib/rack/contrib.rb
  3. +44 −0 lib/rack/contrib/relative_redirect.rb
  4. +78 −0 test/spec_rack_relative_redirect.rb
View
@@ -34,6 +34,10 @@ interface:
* Rack::CSSHTTPRequest - Adds CSSHTTPRequest support by encoding responses as
CSS for cross-site AJAX-style data loading
* Rack::Deflect - Helps protect against DoS attacks.
+* Rack::ResponseCache - Caches responses to requests without query strings
+ to Disk or a user provider Ruby object. Similar to Rails' page caching.
+* Rack::RelativeRedirect - Transforms relative paths in redirects to
+ absolute URLs.
=== Use
View
@@ -28,4 +28,5 @@ def self.release
autoload :Config, "rack/contrib/config"
autoload :NotFound, "rack/contrib/not_found"
autoload :ResponseCache, "rack/contrib/response_cache"
+ autoload :RelativeRedirect, "rack/contrib/relative_redirect"
end
@@ -0,0 +1,44 @@
+require 'rack'
+
+# Rack::RelativeRedirect is a simple middleware that converts relative paths in
+# redirects in absolute urls, so they conform to RFC2616. It allows the user to
+# specify the absolute path to use (with a sensible default), and handles
+# relative paths (those that don't start with a slash) as well.
+class Rack::RelativeRedirect
+ SCHEME_MAP = {'http'=>'80', 'https'=>'443'}
+ # The default proc used if a block is not provided to .new
+ # Just uses the url scheme of the request and the server name.
+ DEFAULT_ABSOLUTE_PROC = proc do |env, res|
+ port = env['SERVER_PORT']
+ scheme = env['rack.url_scheme']
+ "#{scheme}://#{env['SERVER_NAME']}#{":#{port}" unless SCHEME_MAP[scheme] == port}"
+ end
+
+ # Initialize a new RelativeRedirect object with the given arguments. Arguments:
+ # * app : The next middleware in the chain. This is always called.
+ # * &block : If provided, it is called with the environment and the response
+ # from the next middleware. It should return a string representing the scheme
+ # and server name (such as 'http://example.org').
+ def initialize(app, &block)
+ @app = app
+ @absolute_proc = block || DEFAULT_ABSOLUTE_PROC
+ end
+
+ # Call the next middleware with the environment. If the request was a
+ # redirect (response status 301, 302, or 303), and the location header does
+ # not start with an http or https url scheme, call the block provided by new
+ # and use that to make the Location header an absolute url. If the Location
+ # does not start with a slash, make location relative to the path requested.
+ def call(env)
+ res = @app.call(env)
+ if [301,302,303].include?(res[0]) and loc = res[1]['Location'] and !%r{\Ahttps?://}o.match(loc)
+ absolute = @absolute_proc.call(env, res)
+ res[1]['Location'] = if %r{\A/}.match(loc)
+ "#{absolute}#{loc}"
+ else
+ "#{absolute}#{File.dirname(Rack::Utils.unescape(env['PATH_INFO']))}/#{loc}"
+ end
+ end
+ res
+ end
+end
@@ -0,0 +1,78 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/contrib/relative_redirect'
+require 'fileutils'
+
+context Rack::RelativeRedirect do
+ def request(opts={}, &block)
+ @def_status = opts[:status] if opts[:status]
+ @def_location = opts[:location] if opts[:location]
+ yield Rack::MockRequest.new(Rack::RelativeRedirect.new(@def_app, &opts[:block])).get(opts[:path]||@def_path, opts[:headers]||{})
+ end
+
+ setup do
+ @def_path = '/path/to/blah'
+ @def_status = 301
+ @def_location = '/redirect/to/blah'
+ @def_app = lambda { |env| [@def_status, {'Location' => @def_location}, [""]]}
+ end
+
+ specify "should make the location url an absolute url if currently a relative url" do
+ request do |r|
+ r.status.should.equal(301)
+ r.headers['Location'].should.equal('http://example.org/redirect/to/blah')
+ end
+ request(:status=>302, :location=>'/redirect') do |r|
+ r.status.should.equal(302)
+ r.headers['Location'].should.equal('http://example.org/redirect')
+ end
+ end
+
+ specify "should use the request path if the relative url is given and doesn't start with a slash" do
+ request(:status=>303, :location=>'redirect/to/blah') do |r|
+ r.status.should.equal(303)
+ r.headers['Location'].should.equal('http://example.org/path/to/redirect/to/blah')
+ end
+ request(:status=>303, :location=>'redirect') do |r|
+ r.status.should.equal(303)
+ r.headers['Location'].should.equal('http://example.org/path/to/redirect')
+ end
+ end
+
+ specify "should use a given block to make the url absolute" do
+ request(:block=>proc{|env, res| "https://example.org"}) do |r|
+ r.status.should.equal(301)
+ r.headers['Location'].should.equal('https://example.org/redirect/to/blah')
+ end
+ request(:status=>303, :location=>'/redirect', :block=>proc{|env, res| "https://e.org:9999/blah"}) do |r|
+ r.status.should.equal(303)
+ r.headers['Location'].should.equal('https://e.org:9999/blah/redirect')
+ end
+ end
+
+ specify "should not modify the location url unless the response is a redirect" do
+ status = 200
+ @def_app = lambda { |env| [status, {'Content-Type' => "text/html"}, [""]]}
+ request do |r|
+ r.status.should.equal(200)
+ r.headers.should.not.include?('Location')
+ end
+ status = 404
+ @def_app = lambda { |env| [status, {'Content-Type' => "text/html", 'Location' => 'redirect'}, [""]]}
+ request do |r|
+ r.status.should.equal(404)
+ r.headers['Location'].should.equal('redirect')
+ end
+ end
+
+ specify "should not modify the location url if it is already an absolute url" do
+ request(:location=>'https://example.org/') do |r|
+ r.status.should.equal(301)
+ r.headers['Location'].should.equal('https://example.org/')
+ end
+ request(:status=>302, :location=>'https://e.org:9999/redirect') do |r|
+ r.status.should.equal(302)
+ r.headers['Location'].should.equal('https://e.org:9999/redirect')
+ end
+ end
+end

0 comments on commit 43a508f

Please sign in to comment.