diff --git a/README.markdown b/README.markdown index 8820a13..5ced80b 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,6 @@ # rack-rewrite -A rack middleware for defining and applying rewrite rules. In many cases you +A rack middleware for defining and applying rewrite rules. In many cases you can get away with rack-rewrite instead of writing Apache mod_rewrite rules. ## Usage Examples @@ -12,7 +12,7 @@ can get away with rack-rewrite instead of writing Apache mod_rewrite rules. ## Usage Details ### Sample rackup file - + ```ruby gem 'rack-rewrite', '~> 1.2.1' require 'rack/rewrite' @@ -73,7 +73,7 @@ behaves the same as 303. ### Rebuild of existing site in a new technology -It's very common for sites built in older technologies to be rebuilt with the +It's very common for sites built in older technologies to be rebuilt with the latest and greatest. Let's consider a site that has already established quite a bit of "google juice." When we launch the new site, we don't want to lose that hard-earned reputation. By writing rewrite rules that issue 301's for @@ -124,11 +124,11 @@ RewriteRule ^.*$ /system/maintenance.html [L] ``` This rewrite rule says to render a maintenance page for all non-asset requests -if the maintenance file exists. In capistrano, you can quickly upload a +if the maintenance file exists. In capistrano, you can quickly upload a maintenance file using: `cap deploy:web:disable REASON=upgrade UNTIL=12:30PM` - + We can replace the mod_rewrite rules with the following Rack::Rewrite rule: ```ruby @@ -147,7 +147,7 @@ send_file /(.*)$(? Proc.new { |rack_env } ``` -For those using the oniguruma gem with their ruby 1.8 installation, you can +For those using the oniguruma gem with their ruby 1.8 installation, you can get away with: ```ruby @@ -161,9 +161,9 @@ send_file Oniguruma::ORegexp.new("(.*)$(? Proc.new { |rack_env| +x_send_file /.*/, maintenance_file, :if => Proc.new { |rack_env| File.exists?(maintenance_file) } ``` @@ -290,7 +290,7 @@ This will not match the relative URL /features but would match /features.xml. ### Keeping your querystring -When rewriting a URL, you may want to keep your querystring in tact (for +When rewriting a URL, you may want to keep your querystring in tact (for example if you're tracking traffic sources). You will need to include a capture group and substitution pattern in your rewrite rule to achieve this. @@ -303,7 +303,7 @@ will substitute the querystring back into the rewritten URL (via `$1`). ### Arbitrary Rewriting -All rules support passing a Proc as the second argument allowing you to +All rules support passing a Proc as the first or second argument allowing you to perform arbitrary rewrites. The following rule will rewrite all requests received between 12AM and 8AM to an unavailable page. @@ -313,6 +313,13 @@ received between 12AM and 8AM to an unavailable page. } ``` +This rule will redirect all requests paths starting with a current date +string to /today.html + +```ruby + r301 lambda { "/#{Time.current.strftime(%m%d%Y)}.html" }, '/today.html' +``` + ## Contribute rack-rewrite is maintained by [@travisjeffery](http://github.com/travisjeffery). diff --git a/lib/rack/rewrite/rule.rb b/lib/rack/rewrite/rule.rb index 66af707..016906b 100644 --- a/lib/rack/rewrite/rule.rb +++ b/lib/rack/rewrite/rule.rb @@ -99,7 +99,7 @@ def add_rule(method, from, to, options = {}) #:nodoc: # TODO: Break rules into subclasses class Rule #:nodoc: - attr_reader :rule_type, :from, :to, :options + attr_reader :rule_type, :to, :options def initialize(rule_type, from, to, options={}) #:nodoc: @rule_type, @from, @to, @options = rule_type, from, to, normalize_options(options) end @@ -111,6 +111,11 @@ def matches?(rack_env) #:nodoc: self.match_options?(rack_env) && string_matches?(path, self.from) end + def from + return @static_from if @static_from + @from.respond_to?(:call) ? @from.call : @static_from = @from + end + # Either (a) return a Rack response (short-circuiting the Rack stack), or # (b) alter env as necessary and return true def apply!(env) #:nodoc: diff --git a/test/rule_test.rb b/test/rule_test.rb index 4f2fa06..d950c88 100644 --- a/test/rule_test.rb +++ b/test/rule_test.rb @@ -1,9 +1,9 @@ require 'test_helper' class RuleTest < Test::Unit::TestCase - + TEST_ROOT = File.dirname(__FILE__) - + def self.should_pass_maintenance_tests context 'and the maintenance file does in fact exist' do setup { File.stubs(:exists?).returns(true) } @@ -16,14 +16,14 @@ def self.should_pass_maintenance_tests should('not match for a png file') { assert !@rule.matches?(rack_env_for('/images/sls.png')) } end end - + def self.negative_lookahead_supported? begin require 'oniguruma' rescue LoadError; end RUBY_VERSION =~ /^1\.9/ || Object.const_defined?(:Oniguruma) end - + def negative_lookahead_regexp if RUBY_VERSION =~ /^1\.9/ # have to use the constructor instead of the literal syntax b/c load errors occur in Ruby 1.8 @@ -32,7 +32,7 @@ def negative_lookahead_regexp Oniguruma::ORegexp.new("(.*)$(? '/john', 'QUERY_STRING' => 'show_bio=1'} assert_equal '/yair?show_bio=1', rule.apply!(env)[1]['Location'] end - + should 'keep the QUERY_STRING when a rewrite rule that requires a querystring matches a URL with a querystring' do rule = Rack::Rewrite::Rule.new(:rewrite, %r{/john(\?.*)}, '/yair$1') env = {'PATH_INFO' => '/john', 'QUERY_STRING' => 'show_bio=1'} @@ -64,7 +64,7 @@ def negative_lookahead_regexp assert_equal 'show_bio=1', env['QUERY_STRING'] assert_equal '/yair?show_bio=1', env['REQUEST_URI'] end - + should 'update the QUERY_STRING when a rewrite rule changes its value' do rule = Rack::Rewrite::Rule.new(:rewrite, %r{/(\w+)\?show_bio=(\d)}, '/$1?bio=$2') env = {'PATH_INFO' => '/john', 'QUERY_STRING' => 'show_bio=1'} @@ -81,21 +81,21 @@ def negative_lookahead_regexp assert_equal 'text/html', rule.apply!(env)[1]['Content-Type'] end end - + should 'set Content-Type header to text/css for a 301 and 302 request for a .css page' do supported_status_codes.each do |rule_type| rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.css') env = {'PATH_INFO' => '/abc'} assert_equal 'text/css', rule.apply!(env)[1]['Content-Type'] - end + end end - + should 'set additional headers for a 301 and 302 request' do [:r301, :r302].each do |rule_type| rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.css', {:headers => {'Cache-Control' => 'no-cache'}}) env = {'PATH_INFO' => '/abc'} assert_equal 'no-cache', rule.apply!(env)[1]['Cache-Control'] - end + end end should 'evaluate additional headers block once per redirect request' do @@ -119,7 +119,7 @@ def negative_lookahead_regexp assert_equal 'bar', rule.apply!(env)[1]['X-Foobar'] end end - + context 'Given an :x_send_file rule that matches' do setup do @file = File.join(TEST_ROOT, 'geminstaller.yml') @@ -127,23 +127,23 @@ def negative_lookahead_regexp env = {'PATH_INFO' => '/abc'} @response = @rule.apply!(env) end - + should 'return 200' do assert_equal 200, @response[0] end - + should 'return an X-Sendfile header' do assert @response[1].has_key?('X-Sendfile') end - + should 'return a Content-Type of text/yaml' do assert_equal 'text/yaml', @response[1]['Content-Type'] end - + should 'return the proper Content-Length' do assert_equal File.size(@file).to_s, @response[1]['Content-Length'] end - + should 'return additional headers' do assert_equal 'no-cache', @response[1]['Cache-Control'] end @@ -152,7 +152,7 @@ def negative_lookahead_regexp assert_equal [], @response[2] end end - + context 'Given a :send_file rule that matches' do setup do @file = File.join(TEST_ROOT, 'geminstaller.yml') @@ -160,27 +160,27 @@ def negative_lookahead_regexp env = {'PATH_INFO' => '/abc'} @response = @rule.apply!(env) end - + should 'return 200' do assert_equal 200, @response[0] end - + should 'not return an X-Sendfile header' do assert !@response[1].has_key?('X-Sendfile') end - + should 'return a Content-Type of text/yaml' do assert_equal 'text/yaml', @response[1]['Content-Type'] end - + should 'return the proper Content-Length' do assert_equal File.size(@file).to_s, @response[1]['Content-Length'] end - + should 'return additional headers' do assert_equal 'no-cache', @response[1]['Cache-Control'] end - + should 'return the contents of geminstaller.yml in an array for Ruby 1.9.2 compatibility' do assert_equal [File.read(@file)], @response[2] end @@ -196,7 +196,7 @@ def negative_lookahead_regexp end end - + context 'Rule#matches' do context 'Given rule with :not option which matches "from" string' do setup do @@ -209,16 +209,16 @@ def negative_lookahead_regexp assert @rule.matches?(rack_env_for("/features.xml")) end end - + context 'Given rule with :host option of testapp.com' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, /^\/features/, '/facial_features', :host => 'testapp.com') end - + should 'match PATH_INFO of /features and HOST of testapp.com' do assert @rule.matches?(rack_env_for("/features", 'SERVER_NAME' => 'testapp.com', "SERVER_PORT" => "8080")) end - + should 'not match PATH_INFO of /features and HOST of nottestapp.com' do assert ! @rule.matches?(rack_env_for("/features", 'SERVER_NAME' => 'nottestapp.com', "SERVER_PORT" => "8080")) end @@ -231,39 +231,39 @@ def negative_lookahead_regexp assert !@rule.matches?(rack_env_for("/features", "SERVER_NAME" => "127.0.0.1", "SERVER_PORT" => "8080", "HTTP_X_FORWARDED_HOST" => "nottestapp.com")) end end - + context 'Given rule with :method option of POST' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, '/features', '/facial_features', :method => 'POST') end - + should 'match PATH_INFO of /features and REQUEST_METHOD of POST' do assert @rule.matches?(rack_env_for("/features", 'REQUEST_METHOD' => 'POST')) end - + should 'not match PATH_INFO of /features and REQUEST_METHOD of DELETE' do assert ! @rule.matches?(rack_env_for("/features", 'REQUEST_METHOD' => 'DELETE')) end end - + context 'Given any rule with a "from" string of /features' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, '/features', '/facial_features') end - + should 'match PATH_INFO of /features' do assert @rule.matches?(rack_env_for("/features")) end - + should 'not match PATH_INFO of /features.xml' do assert !@rule.matches?(rack_env_for("/features.xml")) end - + should 'not match PATH_INFO of /my_features' do assert !@rule.matches?(rack_env_for("/my_features")) end end - + context 'Given a rule with the ^ operator' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, %r{^/jason}, '/steve') @@ -271,66 +271,66 @@ def negative_lookahead_regexp should 'match with the ^ operator if match is at the beginning of the path' do assert @rule.matches?(rack_env_for('/jason')) end - + should 'not match with the ^ operator when match is deeply nested' do assert !@rule.matches?(rack_env_for('/foo/bar/jason')) end end - + context 'Given any rule with a "from" regular expression of /features(.*)' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, %r{/features(.*)}, '/facial_features$1') end - + should 'match PATH_INFO of /features' do assert @rule.matches?(rack_env_for("/features")) end - + should 'match PATH_INFO of /features.xml' do assert @rule.matches?(rack_env_for('/features.xml')) end - + should 'match PATH_INFO of /features/1' do assert @rule.matches?(rack_env_for('/features/1')) end - + should 'match PATH_INFO of /features?filter_by=name' do assert @rule.matches?(rack_env_for('/features?filter_by_name=name')) end - + should 'match PATH_INFO of /features/1?hide_bio=1' do assert @rule.matches?(rack_env_for('/features/1?hide_bio=1')) end end - + context 'Given a rule with a guard that checks for the presence of a file' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, %r{(.)*}, '/maintenance.html', lambda { |rack_env| File.exists?('maintenance.html') }) end - + context 'when the file exists' do setup do File.stubs(:exists?).returns(true) end - + should 'match' do assert @rule.matches?(rack_env_for('/anything/should/match')) end end - + context 'when the file does not exist' do setup do File.stubs(:exists?).returns(false) end - + should 'not match' do assert !@rule.matches?(rack_env_for('/nothing/should/match')) end end end - + context 'Given the capistrano maintenance.html rewrite rule given in our README' do setup do @rule = Rack::Rewrite::Rule.new(:rewrite, /.*/, '/system/maintenance.html', lambda { |rack_env| @@ -340,7 +340,7 @@ def negative_lookahead_regexp end should_pass_maintenance_tests end - + if negative_lookahead_supported? context 'Given the negative lookahead regular expression version of the capistrano maintenance.html rewrite rule given in our README' do setup do @@ -351,43 +351,57 @@ def negative_lookahead_regexp should_pass_maintenance_tests end end - + context 'Given the CNAME alternative rewrite rule in our README' do setup do @rule = Rack::Rewrite::Rule.new(:r301, %r{.*}, 'http://mynewdomain.com$&', lambda {|rack_env| rack_env['SERVER_NAME'] != 'mynewdomain.com' }) end - + should 'match requests for domain myolddomain.com and redirect to mynewdomain.com' do env = {'PATH_INFO' => '/anything', 'QUERY_STRING' => 'abc=1', 'SERVER_NAME' => 'myolddomain.com'} assert @rule.matches?(env) rack_response = @rule.apply!(env) assert_equal 'http://mynewdomain.com/anything?abc=1', rack_response[1]['Location'] end - + should 'not match requests for domain mynewdomain.com' do assert !@rule.matches?({'PATH_INFO' => '/anything', 'SERVER_NAME' => 'mynewdomain.com'}) end end + + context 'Given a lambda matcher' do + setup do + @rule = Rack::Rewrite::Rule.new(:r302, ->{ Thread.current[:test_matcher] }, '/today' ) + end + should 'call the lambda and match appropriately' do + Thread.current[:test_matcher] = '/abcd' + assert @rule.matches?(rack_env_for("/abcd")) + assert !@rule.matches?(rack_env_for("/DEFG")) + Thread.current[:test_matcher] = /DEFG$/ + assert !@rule.matches?(rack_env_for("/abcd")) + assert @rule.matches?(rack_env_for("/DEFG")) + end + end end - + context 'Rule#interpret_to' do should 'return #to when #from is a string' do rule = Rack::Rewrite::Rule.new(:rewrite, '/abc', '/def') assert_equal '/def', rule.send(:interpret_to, rack_env_for('/abc')) end - + should 'replace $1 on a match' do rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)}, '/people/$1') assert_equal '/people/1', rule.send(:interpret_to, rack_env_for("/person_1")) end - + should 'be able to catch querystrings with a regexp match' do rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)(.*)}, '/people/$1$2') assert_equal '/people/1?show_bio=1', rule.send(:interpret_to, rack_env_for('/person_1?show_bio=1')) end - + should 'be able to make 10 replacements' do # regexp to reverse 10 characters rule = Rack::Rewrite::Rule.new(:rewrite, %r{(\w)(\w)(\w)(\w)(\w)(\w)(\w)(\w)(\w)(\w)}, '$10$9$8$7$6$5$4$3$2$1') @@ -413,15 +427,23 @@ def negative_lookahead_regexp rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)(.*)}, lambda {|match, env| "people-#{match[1].to_i * 3}#{match[2]}"}) assert_equal 'people-3?show_bio=1', rule.send(:interpret_to, rack_env_for('/person_1?show_bio=1')) end + + should 'call to with from lambda match data' do + rule = Rack::Rewrite::Rule.new(:rewrite, ->{ Thread.current[:test_matcher]}, ->(match, env){ match[1][0] }) + Thread.current[:test_matcher] = /^\/(alpha|beta|gamma)$/ + assert_equal 'b', rule.send(:interpret_to, rack_env_for('/beta')) + Thread.current[:test_matcher] = /^\/(zulu)/ + assert_equal 'z', rule.send(:interpret_to, rack_env_for('/zulu')) + end end - + context 'Mongel 1.2.0.pre2 edge case: root url with a query string' do should 'handle a nil PATH_INFO variable without errors' do rule = Rack::Rewrite::Rule.new(:r301, '/a', '/') assert_equal '?exists', rule.send(:build_path_from_env, {'QUERY_STRING' => 'exists'}) end end - + def rack_env_for(url, options = {}) components = url.split('?') {'PATH_INFO' => components[0], 'QUERY_STRING' => components[1] || ''}.merge(options)