├── VERSION ├── .ruby-version ├── .rvmrc ├── lib ├── rack-rewrite.rb └── rack │ ├── rewrite │ ├── version.rb │ ├── yaml_rule_set.rb │ └── rule.rb │ └── rewrite.rb ├── .document ├── .gitignore ├── Gemfile ├── test ├── geminstaller.yml ├── rules.yml ├── test_helper.rb ├── yaml_rule_set_test.rb ├── rack-rewrite_test.rb └── rule_test.rb ├── RELEASING ├── TODO ├── Rakefile ├── LICENSE ├── rack-rewrite.gemspec ├── History.rdoc └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 1.5.1 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p247 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use @rack-rewrite 2 | -------------------------------------------------------------------------------- /lib/rack-rewrite.rb: -------------------------------------------------------------------------------- 1 | require 'rack/rewrite' -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | rack-rewrite-*.gem 7 | Gemfile.lock 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'rake' 7 | end 8 | -------------------------------------------------------------------------------- /lib/rack/rewrite/version.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Rewrite 3 | VERSION = File.read File.join(File.expand_path("..", __FILE__), "..", "..", "..", "VERSION") 4 | end 5 | end -------------------------------------------------------------------------------- /test/geminstaller.yml: -------------------------------------------------------------------------------- 1 | gems: 2 | - name: shoulda 3 | version: '= 2.10.3' 4 | - name: mocha 5 | version: '= 0.9.8' 6 | - name: rack 7 | version: '= 1.1.0' 8 | # - name: oniguruma 9 | # version: '= 1.1.0' 10 | -------------------------------------------------------------------------------- /test/rules.yml: -------------------------------------------------------------------------------- 1 | - 2 | method: r301 3 | from: '/abc' 4 | to : '/def' 5 | - 6 | method: r301 7 | from: !ruby/regexp '/(.*)/abc/' 8 | to : '$1/regexed_path' 9 | - 10 | method: r301 11 | from: '/withhost' 12 | to : '/anotherhost' 13 | options : 14 | host : 'example.com' 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | Bundler.require :default, :development 5 | 6 | require 'test/unit' 7 | 8 | class Test::Unit::TestCase 9 | end 10 | 11 | def rack_env_for(url, options = {}) 12 | components = url.split('?') 13 | {'PATH_INFO' => components[0], 'QUERY_STRING' => components[1] || ''}.merge(options) 14 | end 15 | 16 | def supported_status_codes 17 | [:r301, :r302, :r303, :r307] 18 | end -------------------------------------------------------------------------------- /RELEASING: -------------------------------------------------------------------------------- 1 | Bump VERSION 2 | Update version references in README 3 | Update History.rdoc with relevant details 4 | Manually edit file list in .gemspec if necessary. 5 | Commit these edits in its own commit "Bumping to version x.x.x" 6 | Tag the new version (git tag -a vX.X.X && git push --tags) 7 | Build and push the gem (gem build rack-rewrite.gemspec && gem push rack-rewrite-X.X.X.gem) 8 | Bump the gem version to the next dot release with a prerelease (1.1.1 --> 1.1.2.a) 9 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * OUTSTANDING 2 | * Add :host support to restrict which URL's a rewrite rule matches [10/15/09] 3 | * Add support for specifying a config file instead of passing a block (e.g. config/rewrite.rb) [10/15/09] 4 | * Provide testing helpers (e.g. should_rewrite) to facilitate straightforward testing. [10/25/09] 5 | * Allow rules to return arbitrary html (e.g. the contents of the maintenance page) [10/25/09] 6 | 7 | * COMPLETED 8 | * Better message than "Redirecting..." -- how about html that says where it's being redirected to? [10/16/09] 9 | * Add :if => lambda support for arbitrary conditional rule application (this will allow us to do the capistrano maintenance page w/o apache's mod_rewrite) [10/15/09] 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/testtask' 4 | require 'rdoc/task' 5 | 6 | Rake::TestTask.new(:test) do |test| 7 | test.libs << 'lib' << 'test' << '.' 8 | test.pattern = 'test/**/*_test.rb' 9 | test.verbose = true 10 | end 11 | 12 | begin 13 | require 'rcov/rcovtask' 14 | Rcov::RcovTask.new do |test| 15 | test.libs << 'test' 16 | test.pattern = 'test/**/*_test.rb' 17 | test.verbose = true 18 | end 19 | rescue LoadError 20 | task :rcov do 21 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 22 | end 23 | end 24 | 25 | task :default => :test 26 | 27 | Rake::RDocTask.new do |rdoc| 28 | if File.exist?('VERSION') 29 | version = File.read('VERSION') 30 | else 31 | version = "" 32 | end 33 | 34 | rdoc.rdoc_dir = 'rdoc' 35 | rdoc.title = "rack-rewrite #{version}" 36 | rdoc.rdoc_files.include('README*') 37 | rdoc.rdoc_files.include('History.rdoc') 38 | rdoc.rdoc_files.include('lib/**/*.rb') 39 | end 40 | -------------------------------------------------------------------------------- /lib/rack/rewrite/yaml_rule_set.rb: -------------------------------------------------------------------------------- 1 | require 'rack/mime' 2 | require 'yaml' 3 | 4 | module Rack 5 | class Rewrite 6 | class YamlRuleSet 7 | 8 | attr_reader :rules 9 | 10 | # Provides a method for setting the rewrite rules in a yaml file. 11 | # 12 | # Relys on Yaml to correctly produce ruby types like regex and then pushes 13 | # those values into a ruleset - giving the same result as if the DSL was 14 | # used. 15 | 16 | def initialize(options) 17 | @options = options 18 | @rules = generate_rules(load_rules) 19 | end 20 | 21 | def load_rules 22 | YAML.safe_load ::File.open(@options[:file_name]), permitted_classes: [Regexp] 23 | end 24 | 25 | def generate_rules(yaml) 26 | yaml.map do |rule| 27 | options = rule["options"] || {} 28 | options.keys.each do |key| 29 | options[(key.to_sym rescue key) || key] = options.delete(key) 30 | end 31 | Rule.new(rule["method"].to_sym, rule["from"], rule["to"], options) 32 | end 33 | end 34 | 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 — John Trupiano, Travis Jeffery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/rack/rewrite.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | autoload :RuleSet, 'rack/rewrite/rule' 3 | autoload :VERSION, 'rack/rewrite/version' 4 | 5 | # A rack middleware for defining and applying rewrite rules. In many cases you 6 | # can get away with rack-rewrite instead of writing Apache mod_rewrite rules. 7 | class Rewrite 8 | def initialize(app, given_options = {}, &rule_block) 9 | options = { 10 | :klass => RuleSet, 11 | :options => {} 12 | }.merge(given_options) 13 | @app = app 14 | @rule_set = options[:klass].new(options[:options]) 15 | @rule_set.instance_eval(&rule_block) if block_given? 16 | end 17 | 18 | def call(env) 19 | if matched_rule = find_first_matching_rule(env) 20 | rack_response = matched_rule.apply!(env) 21 | # Don't invoke the app if applying the rule returns a rack response 22 | return rack_response unless rack_response === true 23 | end 24 | @app.call(env) 25 | end 26 | 27 | private 28 | def find_first_matching_rule(env) #:nodoc: 29 | @rule_set.rules.detect { |rule| rule.matches?(env) } 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/yaml_rule_set_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'lib/rack/rewrite/yaml_rule_set' 3 | 4 | 5 | class YamlRuleSetTest < Test::Unit::TestCase 6 | 7 | TEST_ROOT = File.dirname(__FILE__) 8 | 9 | 10 | context "When used through a rack app" do 11 | 12 | setup do 13 | @file_name = File.join(TEST_ROOT, 'rules.yml') 14 | @app = Class.new { def call(app); true; end }.new 15 | end 16 | 17 | should 'be initialized when the app is created' do 18 | Rack::Rewrite::YamlRuleSet.expects(:new).with(all_of({:file_name => @file_name})) 19 | @rack = Rack::Rewrite.new(@app, 20 | :klass => Rack::Rewrite::YamlRuleSet, 21 | :options => {:file_name => @file_name} 22 | ) 23 | end 24 | 25 | end 26 | 27 | context "When given some rules" do 28 | 29 | setup do 30 | @file_name = File.join(TEST_ROOT, 'rules.yml') 31 | @rule_set = Rack::Rewrite::YamlRuleSet.new(:file_name => @file_name) 32 | end 33 | 34 | should "correctly load up 3 rules" do 35 | assert_equal 3, @rule_set.rules.length 36 | end 37 | 38 | should "correctly perform a regexed rule" do 39 | env = rack_env_for("/something/abc") 40 | rule = @rule_set.rules.detect{|a| a.matches?(env)} 41 | assert_not_nil rule 42 | assert_equal '/something/regexed_path', rule.apply!(env)[1]['Location'] 43 | end 44 | 45 | should "correctly apply host option" do 46 | env = rack_env_for("/withhost") 47 | rule = @rule_set.rules.detect{|a| a.matches?(env)} 48 | assert_nil rule 49 | 50 | env = rack_env_for("/withhost", 'SERVER_NAME' => 'example.com', "SERVER_PORT" => "8080") 51 | rule = @rule_set.rules.detect{|a| a.matches?(env)} 52 | assert_not_nil rule 53 | assert_equal '/anotherhost', rule.apply!(env)[1]['Location'] 54 | end 55 | 56 | end 57 | end -------------------------------------------------------------------------------- /rack-rewrite.gemspec: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'rack-rewrite' 5 | s.version = File.read('VERSION') 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Travis Jeffery", "John Trupiano"] 9 | s.date = Date.today.to_s 10 | s.description = %q{A rack middleware for enforcing rewrite rules. In many cases you can get away with rack-rewrite instead of writing Apache mod_rewrite rules.} 11 | s.email = %q{travisjeffery@gmail.com} 12 | s.licenses = ["MIT"] 13 | s.extra_rdoc_files = [ 14 | "LICENSE", 15 | "History.rdoc", 16 | ] 17 | s.files = [ 18 | "History.rdoc", 19 | "LICENSE", 20 | "README.markdown", 21 | "Rakefile", 22 | "VERSION", 23 | "Gemfile", 24 | "lib/rack-rewrite.rb", 25 | "lib/rack/rewrite.rb", 26 | "lib/rack/rewrite/rule.rb", 27 | "lib/rack/rewrite/version.rb", 28 | "lib/rack/rewrite/yaml_rule_set.rb", 29 | "rack-rewrite.gemspec", 30 | "test/geminstaller.yml", 31 | "test/rack-rewrite_test.rb", 32 | "test/rule_test.rb", 33 | "test/test_helper.rb" 34 | ] 35 | s.homepage = %q{http://github.com/jtrupiano/rack-rewrite} 36 | s.rdoc_options = ["--charset=UTF-8"] 37 | s.require_paths = ["lib"] 38 | s.rubyforge_project = %q{johntrupiano} 39 | s.rubygems_version = %q{1.3.7} 40 | s.summary = %q{A rack middleware for enforcing rewrite rules} 41 | s.test_files = [ 42 | "test/rack-rewrite_test.rb", 43 | "test/geminstaller.yml", 44 | "test/rack-rewrite_test.rb", 45 | "test/rule_test.rb", 46 | "test/test_helper.rb" 47 | ] 48 | 49 | s.add_development_dependency 'bundler' 50 | s.add_development_dependency 'shoulda', '~> 2.10.2' 51 | s.add_development_dependency 'mocha', '~> 0.9.7' 52 | s.add_development_dependency 'rack' 53 | 54 | if s.respond_to? :specification_version then 55 | s.specification_version = 3 56 | end 57 | end 58 | 59 | -------------------------------------------------------------------------------- /History.rdoc: -------------------------------------------------------------------------------- 1 | == 1.5.1 / 2014-12-31 2 | * Maintenance 3 | * Fix content-length being set (#66) 4 | 5 | == 1.5.0 / 2014-01-09 6 | * API 7 | * Add send_data (#51) 8 | 9 | == 1.4.1 / 2013-11-07 10 | * API 11 | * Add support for external loaders (#50) 12 | 13 | === 1.4.01 / 2013-09-13 14 | * API 15 | * Add support for lambda matchers 16 | * Maintenance 17 | * Update README for usage with Rails 4 and threading, :scheme option 18 | 19 | === 1.2.1 / 2011-09-20 20 | * Maintenance 21 | * Use Rack::Request to match the host 22 | 23 | === 1.2.0 / 2011-09-20 24 | * API 25 | * :headers option to send additional headers with the response 26 | 27 | === 1.1.0 / 2011-08-15 28 | * API 29 | * :host and :method option to match SERVER_NAME and REQUEST_METHOD env params. 30 | * :not option to negative match against path. 31 | * Maintenance 32 | * Refactored internals a bit. 33 | 34 | === 1.0.2 / 2010-10-01 35 | * Maintenance 36 | * :send_file rules return content in an Array for Ruby 1.9.2 compatibility 37 | 38 | === 1.0.1 / 2010-09-17 39 | * Maintenance 40 | * Set Content-Type based on file extension of file/location being redirected to. Addresses GitHub Issue #8. 41 | 42 | === 1.0.0 / 2010-05-13 43 | * API 44 | * Fix rack 1.1.0 / rails3 compatibility by eliminating reliance on REQUEST_URI env param. Paths are now constructed with PATH_INFO and QUERY_STRING 45 | * Follow rack directory/require convention: require 'rack/rewrite' instead of 'rack-rewrite' 46 | * Include an HTML anchor tag linked to where the URL being redirected to in the body of 301's and 302's 47 | 48 | === 0.2.1 / 2010-01-06 49 | * API 50 | * Implement $& substitution pattern (thanks to {Ben Brinckerhoff}[http://github.com/bhb]) 51 | 52 | * Maintenance 53 | * Ignore empty captures instead of failing during subsitution (thanks to {Ben Brinckerhoff}[http://github.com/bhb]) 54 | * Play nice with Rack::Test requests which only set PATH_INFO and not REQUEST_URI (thanks to {@docunext}[http://github.com/docunext]) 55 | * Use QUERY_STRING instead of QUERYSTRING as per Rack spec. Closes Issue #1. 56 | 57 | === 0.2.0 / 2009-11-14 58 | * API 59 | * Allow Proc's to be be passed as the 'to' argument to rule declarations 60 | * Introduce rule guard support using :if => Proc.new option. 61 | * :send_file and :x_send_file rules 62 | * proxy rack_env to rule guards for arbitrary rule writing 63 | 64 | * Documentation 65 | * Add example of writing capistrano maintenance page rewrite rules 66 | * Add examples of rule guards and arbitrary rewriting 67 | * Add examples of :send_file and :x_send_file rules 68 | 69 | === 0.1.3 / 2009-11-14 70 | * Maintenance 71 | * Ensure Content-Type header is set for 301's and 302's (thanks to Sebastian Röbke) 72 | * Documentation 73 | * Add HISTORY.rdoc 74 | 75 | === 0.1.2 / 2009-10-13 76 | 77 | * Initial Feature Set 78 | * :r301, :r302 and :redirect are supported in the rewrite DSL 79 | * Regex matching/substitution patterns supported in rules 80 | -------------------------------------------------------------------------------- /test/rack-rewrite_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RackRewriteTest < Test::Unit::TestCase 4 | 5 | def call_args(overrides={}) 6 | {'REQUEST_URI' => '/wiki/Yair_Flicker', 'PATH_INFO' => '/wiki/Yair_Flicker', 'QUERY_STRING' => ''}.merge(overrides) 7 | end 8 | 9 | def call_args_no_req(overrides={}) 10 | {'PATH_INFO' => '/wiki/Yair_Flicker', 'QUERY_STRING' => ''}.merge(overrides) 11 | end 12 | 13 | def self.should_not_halt 14 | should "not halt the rack chain" do 15 | @app.expects(:call).once 16 | @rack.call(call_args) 17 | end 18 | end 19 | 20 | def self.should_be_a_rack_response 21 | should 'be a rack a response' do 22 | ret = @rack.call(call_args) 23 | assert ret.is_a?(Array), 'return value is not a valid rack response' 24 | assert_equal 3, ret.size, 'should have 3 arguments' 25 | end 26 | end 27 | 28 | def self.should_halt 29 | should "should halt the rack chain" do 30 | @app.expects(:call).never 31 | @rack.call(call_args) 32 | end 33 | should_be_a_rack_response 34 | end 35 | 36 | def self.should_location_redirect_to(location, code) 37 | should "respond with http status code #{code}" do 38 | ret = @rack.call(call_args) 39 | assert_equal code, ret[0] 40 | end 41 | should 'send a location header' do 42 | ret = @rack.call(call_args) 43 | assert_equal location, ret[1]['Location'], 'Location is incorrect' 44 | end 45 | end 46 | 47 | context 'Given an app' do 48 | setup do 49 | @app = Class.new { def call(app); true; end }.new 50 | end 51 | 52 | context 'when no rewrite rule matches' do 53 | setup { 54 | @rack = Rack::Rewrite.new(@app) 55 | } 56 | should_not_halt 57 | end 58 | 59 | [301, 302, 303, 307].each do |status| 60 | context "when a #{status} rule matches" do 61 | setup { 62 | @rack = Rack::Rewrite.new(@app) do 63 | send("r#{status}", '/wiki/Yair_Flicker', '/yair') 64 | end 65 | } 66 | should_halt 67 | should_location_redirect_to('/yair', status) 68 | end 69 | end 70 | 71 | [[:p, 301], [:moved_permanently, 301], [:found, 302], [:see_other, 303], [:t, 307], [:temporary_redirect, 307]].each do |rule| 72 | context "when a #{rule.first} rule matches" do 73 | setup { 74 | @rack = Rack::Rewrite.new(@app) do 75 | send(rule.first, '/wiki/Yair_Flicker', '/yair') 76 | end 77 | } 78 | should_halt 79 | should_location_redirect_to('/yair', rule.last) 80 | end 81 | end 82 | 83 | context 'when a rewrite rule matches' do 84 | setup { 85 | @rack = Rack::Rewrite.new(@app) do 86 | rewrite '/wiki/Yair_Flicker', '/john' 87 | end 88 | } 89 | should_not_halt 90 | 91 | context 'the env' do 92 | setup do 93 | @initial_args = call_args.dup 94 | @rack.call(@initial_args) 95 | end 96 | 97 | should "set PATH_INFO to '/john'" do 98 | assert_equal '/john', @initial_args['PATH_INFO'] 99 | end 100 | should "set REQUEST_URI to '/john'" do 101 | assert_equal '/john', @initial_args['REQUEST_URI'] 102 | end 103 | should "set QUERY_STRING to ''" do 104 | assert_equal '', @initial_args['QUERY_STRING'] 105 | end 106 | end 107 | end 108 | 109 | context 'when a rewrite rule matches but there is no REQUEST_URI set' do 110 | setup { 111 | @rack = Rack::Rewrite.new(@app) do 112 | rewrite '/wiki/Yair_Flicker', '/john' 113 | end 114 | } 115 | should_not_halt 116 | 117 | context 'the env' do 118 | setup do 119 | @initial_args = call_args_no_req.dup 120 | @rack.call(@initial_args) 121 | end 122 | 123 | should "set PATH_INFO to '/john'" do 124 | assert_equal '/john', @initial_args['PATH_INFO'] 125 | end 126 | should "set REQUEST_URI to '/john'" do 127 | assert_equal '/john', @initial_args['REQUEST_URI'] 128 | end 129 | should "set QUERY_STRING to ''" do 130 | assert_equal '', @initial_args['QUERY_STRING'] 131 | end 132 | end 133 | end 134 | 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/rack/rewrite/rule.rb: -------------------------------------------------------------------------------- 1 | require 'rack/mime' 2 | 3 | module Rack 4 | class Rewrite 5 | class RuleSet 6 | attr_reader :rules 7 | def initialize(options = {})#:nodoc: 8 | @rules = [] 9 | end 10 | 11 | protected 12 | # We're explicitly defining private functions for our DSL rather than 13 | # using method_missing 14 | 15 | # Creates a rewrite rule that will simply rewrite the REQUEST_URI, 16 | # PATH_INFO, and QUERY_STRING headers of the Rack environment. The 17 | # user's browser will continue to show the initially requested URL. 18 | # 19 | # rewrite '/wiki/John_Trupiano', '/john' 20 | # rewrite %r{/wiki/(\w+)_\w+}, '/$1' 21 | # rewrite %r{(.*)}, '/maintenance.html', :if => lambda { File.exists?('maintenance.html') } 22 | def rewrite(*args) 23 | add_rule :rewrite, *args 24 | end 25 | 26 | # Creates a redirect rule that will send a 301 when matching. 27 | # 28 | # r301 '/wiki/John_Trupiano', '/john' 29 | # r301 '/contact-us.php', '/contact-us' 30 | # 31 | # You can use +moved_permanently+ or just +p+ instead of +r301+. 32 | def r301(*args) 33 | add_rule :r301, *args 34 | end 35 | 36 | alias :moved_permanently :r301 37 | alias :p :r301 38 | 39 | # Creates a redirect rule that will send a 302 when matching. 40 | # 41 | # r302 '/wiki/John_Trupiano', '/john' 42 | # r302 '/wiki/(.*)', 'http://www.google.com/?q=$1' 43 | # 44 | # You can use +found+ instead of +r302+. 45 | def r302(*args) 46 | add_rule :r302, *args 47 | end 48 | 49 | alias :found :r302 50 | 51 | # Creates a redirect rule that will send a 303 when matching. 52 | # 53 | # r303 '/wiki/John_Trupiano', '/john' 54 | # r303 '/wiki/(.*)', 'http://www.google.com/?q=$1' 55 | # 56 | # You can use +see_other+ instead of +r303+. 57 | def r303(*args) 58 | add_rule :r303, *args 59 | end 60 | 61 | alias :see_other :r303 62 | 63 | # Creates a redirect rule that will send a 307 when matching. 64 | # 65 | # r307 '/wiki/John_Trupiano', '/john' 66 | # r307 '/wiki/(.*)', 'http://www.google.com/?q=$1' 67 | # 68 | # You can use +temporary_redirect+ or +t+ instead of +r307+. 69 | def r307(*args) 70 | add_rule :r307, *args 71 | end 72 | 73 | alias :temporary_redirect :r307 74 | alias :t :r307 75 | 76 | # Creates a rule that will render a file if matched. 77 | # 78 | # send_file /*/, 'public/system/maintenance.html', 79 | # :if => Proc.new { File.exists?('public/system/maintenance.html') } 80 | def send_file(*args) 81 | add_rule :send_file, *args 82 | end 83 | 84 | # Creates a rule that will render a file using x-send-file 85 | # if matched. 86 | # 87 | # x_send_file /*/, 'public/system/maintenance.html', 88 | # :if => Proc.new { File.exists?('public/system/maintenance.html') } 89 | def x_send_file(*args) 90 | add_rule :x_send_file, *args 91 | end 92 | 93 | # Creates a rule taht will render the raw data if matched 94 | # send_data /*/, 'public/system/maintenance.html', 95 | # :if => Proc.new { File.exists?('public/system/maintenance.html') } 96 | def send_data(*args) 97 | add_rule :send_data, *args 98 | end 99 | 100 | private 101 | def add_rule(method, from, to, options = {}) #:nodoc: 102 | @rules << Rule.new(method.to_sym, from, to, options) 103 | end 104 | 105 | end 106 | 107 | # TODO: Break rules into subclasses 108 | class Rule #:nodoc: 109 | attr_reader :rule_type, :to, :options 110 | def initialize(rule_type, from, to, options={}) #:nodoc: 111 | @rule_type, @from, @to, @options = rule_type, from, to, normalize_options(options) 112 | end 113 | 114 | def matches?(rack_env) #:nodoc: 115 | return false if options[:if].respond_to?(:call) && !options[:if].call(rack_env) 116 | path = build_path_from_env(rack_env) 117 | 118 | self.match_options?(rack_env) && string_matches?(path, self.from) 119 | end 120 | 121 | def from 122 | return @static_from if @static_from 123 | @from.respond_to?(:call) ? @from.call : @static_from = @from 124 | end 125 | 126 | # Either (a) return a Rack response (short-circuiting the Rack stack), or 127 | # (b) alter env as necessary and return true 128 | def apply!(env) #:nodoc: 129 | interpreted_to = self.interpret_to(env) 130 | additional_headers = {} 131 | if @options[:headers] 132 | if @options[:headers].respond_to?(:call) 133 | additional_headers = @options[:headers].call || {} 134 | else 135 | additional_headers = @options[:headers] || {} 136 | end 137 | end 138 | status = @options[:status] || 200 139 | case self.rule_type 140 | when :r301 141 | [301, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]] 142 | when :r302 143 | [302, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]] 144 | when :r303 145 | [303, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]] 146 | when :r307 147 | [307, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]] 148 | when :rewrite 149 | # return [200, {}, {:content => env.inspect}] 150 | env['REQUEST_URI'] = interpreted_to 151 | if q_index = interpreted_to.index('?') 152 | env['PATH_INFO'] = interpreted_to[0..q_index-1] 153 | env['QUERY_STRING'] = interpreted_to[q_index+1..interpreted_to.size-1] 154 | else 155 | env['PATH_INFO'] = interpreted_to 156 | env['QUERY_STRING'] = '' 157 | end 158 | true 159 | when :send_file 160 | [status, { 161 | 'Content-Length' => ::File.size(interpreted_to).to_s, 162 | 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to)) 163 | }.merge!(additional_headers), [::File.read(interpreted_to)]] 164 | when :x_send_file 165 | [status, { 166 | 'X-Sendfile' => interpreted_to, 167 | 'Content-Length' => ::File.size(interpreted_to).to_s, 168 | 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to)) 169 | }.merge!(additional_headers), []] 170 | when :send_data 171 | [status, { 172 | 'Content-Length' => interpreted_to.bytesize, 173 | 'Content-Type' => 'text/html', 174 | }.merge!(additional_headers), [interpreted_to]] 175 | else 176 | raise Exception.new("Unsupported rule: #{self.rule_type}") 177 | end 178 | end 179 | 180 | protected 181 | def interpret_to(env) #:nodoc: 182 | path = build_path_from_env(env) 183 | return interpret_to_proc(path, env) if self.to.is_a?(Proc) 184 | return computed_to(path) if compute_to?(path) 185 | self.to 186 | end 187 | 188 | def is_a_regexp?(obj) 189 | obj.is_a?(Regexp) || (Object.const_defined?(:Oniguruma) && obj.is_a?(Oniguruma::ORegexp)) 190 | end 191 | 192 | def match_options?(env, path = build_path_from_env(env)) 193 | matches = [] 194 | request = Rack::Request.new(env) 195 | 196 | # negative matches 197 | matches << !string_matches?(path, options[:not]) if options[:not] 198 | 199 | # positive matches 200 | matches << string_matches?(env['REQUEST_METHOD'], options[:method]) if options[:method] 201 | matches << string_matches?(request.host, options[:host]) if options[:host] 202 | matches << string_matches?(request.scheme, options[:scheme]) if options[:scheme] 203 | 204 | matches.all? 205 | end 206 | 207 | private 208 | def normalize_options(arg) 209 | options = arg.respond_to?(:call) ? {:if => arg} : arg 210 | options.symbolize_keys! if options.respond_to? :symbolize_keys! 211 | options.freeze 212 | end 213 | 214 | def interpret_to_proc(path, env) 215 | return self.to.call(match(path), env) if self.from.is_a?(Regexp) 216 | self.to.call(self.from, env) 217 | end 218 | 219 | def compute_to?(path) 220 | self.is_a_regexp?(from) && match(path) 221 | end 222 | 223 | def match(path) 224 | self.from.match(path) 225 | end 226 | 227 | def string_matches?(string, matcher) 228 | if self.is_a_regexp?(matcher) 229 | string =~ matcher 230 | elsif matcher.is_a?(String) 231 | string == matcher 232 | elsif matcher.is_a?(Symbol) 233 | string.downcase == matcher.to_s.downcase 234 | else 235 | false 236 | end 237 | end 238 | 239 | def computed_to(path) 240 | # is there a better way to do this? 241 | computed_to = self.to.dup 242 | computed_to.gsub!("$&",match(path).to_s) 243 | (match(path).size - 1).downto(1) do |num| 244 | computed_to.gsub!("$#{num}", match(path)[num].to_s) 245 | end 246 | return computed_to 247 | end 248 | 249 | # Construct the URL (without domain) from PATH_INFO and QUERY_STRING 250 | def build_path_from_env(env) 251 | path = env['PATH_INFO'] || '' 252 | path += "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].nil? || env['QUERY_STRING'].empty? 253 | path 254 | end 255 | 256 | def redirect_message(location) 257 | %Q(Redirecting to #{location}) 258 | end 259 | end 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # rack-rewrite 2 | 3 | A rack middleware for defining and applying rewrite rules. In many cases you 4 | can get away with rack-rewrite instead of writing Apache mod_rewrite rules. 5 | 6 | ## Usage Examples 7 | 8 | * [Rack::Rewrite for Site Maintenance and Downtime](http://blog.smartlogicsolutions.com/2009/11/16/rack-rewrite-for-site-maintenance-and-downtime/) 9 | * [Rack::Rewrite + Google Analytics Makes Site Transitions Seamless](http://blog.smartlogicsolutions.com/2009/11/24/rack-rewrite-google-analytics-makes-site-transitions-seamless/) 10 | * [Rack::Rewrite for serving gzipped pipeline assets on Heroku](https://gist.github.com/eliotsykes/6049536) 11 | 12 | ## Usage Details 13 | 14 | ### Sample rackup file 15 | 16 | ```ruby 17 | # config.ru 18 | gem 'rack-rewrite', '~> 1.5.0' 19 | require 'rack/rewrite' 20 | use Rack::Rewrite do 21 | rewrite '/wiki/John_Trupiano', '/john' 22 | r301 '/wiki/Yair_Flicker', '/yair' 23 | r302 '/wiki/Greg_Jastrab', '/greg' 24 | r301 %r{/wiki/(\w+)_\w+}, '/$1' 25 | end 26 | ``` 27 | 28 | ### Sample usage in a rails app 29 | 30 | Rails 3 or less: 31 | 32 | ```ruby 33 | # config/application.rb 34 | config.middleware.insert_before(Rack::Lock, Rack::Rewrite) do 35 | rewrite '/wiki/John_Trupiano', '/john' 36 | r301 '/wiki/Yair_Flicker', '/yair' 37 | r302 '/wiki/Greg_Jastrab', '/greg' 38 | r301 %r{/wiki/(\w+)_\w+}, '/$1' 39 | end 40 | ``` 41 | 42 | Rails 4+ or if you use `config.threadsafe`, you'll need to `insert_before(Rack::Runtime, Rack::Rewrite)` as `Rack::Lock` does not exist when `config.allow_concurrency == true`: 43 | 44 | ```ruby 45 | config.middleware.insert_before(Rack::Runtime, Rack::Rewrite) do 46 | rewrite '/wiki/John_Trupiano', '/john' 47 | r301 '/wiki/Yair_Flicker', '/yair' 48 | r302 '/wiki/Greg_Jastrab', '/greg' 49 | r301 %r{/wiki/(\w+)_\w+}, '/$1' 50 | end 51 | ``` 52 | 53 | Or insert Rack::Rewrite to the top of the stack: 54 | 55 | ``` ruby 56 | config.middleware.insert 0, 'Rack::Rewrite' {} 57 | ``` 58 | 59 | ## Redirection codes 60 | 61 | All redirect status codes from the [HTTP spec](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) are supported: 62 | 63 | * 301 moved permanently 64 | * 302 found 65 | * 303 see other 66 | * 307 temporary redirect 67 | 68 | These translate to the following methods inside the Rack::Rewrite block: 69 | 70 | ```ruby 71 | r301 '/wiki/John_Trupiano', '/john' 72 | moved_permanently '/wiki/John_Trupiano', '/john' 73 | p '/wiki/John_Trupiano', '/john' # shortcut alias 74 | 75 | r302 '/wiki/John_Trupiano', '/john' 76 | found '/wiki/John_Trupiano', '/john' 77 | 78 | r303 '/wiki/John_Trupiano', '/john' 79 | see_other '/wiki/John_Trupiano', '/john' 80 | 81 | r307 '/wiki/John_Trupiano', '/john' 82 | temporary_redirect '/wiki/John_Trupiano', '/john' 83 | t '/wiki/John_Trupiano', '/john' # shortcut alias 84 | ``` 85 | 86 | The 303 and 307 codes were added to the HTTP spec to make unambiguously clear 87 | what clients should do with the request method. 303 means that the new request 88 | should always be made via GET. 307 means that the new request should use the 89 | same method as the original request. Status code 302 was left as it is, since 90 | it was already in use by the time these issues came to light. In practice it 91 | behaves the same as 303. 92 | 93 | ## Use Cases 94 | 95 | ### Rebuild of existing site in a new technology 96 | 97 | It's very common for sites built in older technologies to be rebuilt with the 98 | latest and greatest. Let's consider a site that has already established quite 99 | a bit of "google juice." When we launch the new site, we don't want to lose 100 | that hard-earned reputation. By writing rewrite rules that issue 301's for 101 | old URL's, we can "transfer" that google ranking to the new site. An example 102 | rule might look like: 103 | 104 | ```ruby 105 | r301 '/contact-us.php', '/contact-us' 106 | r301 '/wiki/John_Trupiano', '/john' 107 | ``` 108 | 109 | ### Retiring old routes 110 | 111 | As a web application evolves you will undoubtedly reach a point where you need 112 | to change the name of something (a model, e.g.). This name change will 113 | typically require a similar change to your routing. The danger here is that 114 | any URL's previously generated (in a transactional email for instance) will 115 | have the URL hard-coded. In order for your rails app to continue to serve 116 | this URL, you'll need to add an extra entry to your routes file. 117 | Alternatively, you could use rack-rewrite to redirect or pass through requests 118 | to these routes and keep your routes.rb clean. 119 | 120 | ```ruby 121 | rewrite %r{/features(.*)}, '/facial_features$1' 122 | ``` 123 | 124 | ### CNAME alternative 125 | 126 | In the event that you do not control your DNS, you can leverage Rack::Rewrite 127 | to redirect to a canonical domain. In the following rule we utilize the 128 | $& substitution operator to capture the entire request URI. 129 | 130 | ```ruby 131 | r301 %r{.*}, 'http://mynewdomain.com$&', :if => Proc.new {|rack_env| 132 | rack_env['SERVER_NAME'] != 'mynewdomain.com' 133 | } 134 | ``` 135 | 136 | ### Site Maintenance 137 | 138 | Most capistrano users will be familiar with the following Apache rewrite rules: 139 | 140 | ``` 141 | RewriteCond %{REQUEST_URI} !\.(css|jpg|png)$ 142 | RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f 143 | RewriteCond %{SCRIPT_FILENAME} !maintenance.html 144 | RewriteRule ^.*$ /system/maintenance.html [L] 145 | ``` 146 | 147 | This rewrite rule says to render a maintenance page for all non-asset requests 148 | if the maintenance file exists. In capistrano, you can quickly upload a 149 | maintenance file using: 150 | 151 | `cap deploy:web:disable REASON=upgrade UNTIL=12:30PM` 152 | 153 | We can replace the mod_rewrite rules with the following Rack::Rewrite rule: 154 | 155 | ```ruby 156 | maintenance_file = File.join(RAILS_ROOT, 'public', 'system', 'maintenance.html') 157 | send_file /.*/, maintenance_file, :if => Proc.new { |rack_env| 158 | File.exists?(maintenance_file) && rack_env['PATH_INFO'] !~ /\.(css|jpg|png)/ 159 | } 160 | ``` 161 | 162 | If you're running Ruby 1.9, this rule is simplified: 163 | 164 | ```ruby 165 | maintenance_file = File.join(RAILS_ROOT, 'public', 'system', 'maintenance.html') 166 | send_file /(.*)$(? Proc.new { |rack_env| 167 | File.exists?(maintenance_file) 168 | } 169 | ``` 170 | 171 | For those using the oniguruma gem with their ruby 1.8 installation, you can 172 | get away with: 173 | 174 | ```ruby 175 | maintenance_file = File.join(RAILS_ROOT, 'public', 'system', 'maintenance.html') 176 | send_file Oniguruma::ORegexp.new("(.*)$(? Proc.new { |rack_env| 177 | File.exists?(maintenance_file) 178 | } 179 | ``` 180 | 181 | ## Rewrite Rules 182 | 183 | ### :rewrite 184 | 185 | Calls to #rewrite will simply update the PATH_INFO, QUERY_STRING and 186 | REQUEST_URI HTTP header values and pass the request onto the next chain in 187 | the Rack stack. The URL that a user's browser will show will not be changed. 188 | See these examples: 189 | 190 | ```ruby 191 | rewrite '/wiki/John_Trupiano', '/john' # [1] 192 | rewrite %r{/wiki/(\w+)_\w+}, '/$1' # [2] 193 | ``` 194 | 195 | For [1], the user's browser will continue to display /wiki/John_Trupiano, but 196 | the actual HTTP header values for PATH_INFO and REQUEST_URI in the request 197 | will be changed to /john for subsequent nodes in the Rack stack. Rails 198 | reads these headers to determine which routes will match. 199 | 200 | Rule [2] showcases the use of regular expressions and substitutions. [2] is a 201 | generalized version of [1] that will match any /wiki/FirstName_LastName URL's 202 | and rewrite them as the first name only. This is an actual catch-all rule we 203 | applied when we rebuilt our website in September 2009 204 | ( http://www.smartlogicsolutions.com ). 205 | 206 | ### :r301, :r302, :r303, :r307 207 | 208 | Calls to #r301 and #r302 have the same signature as #rewrite. The difference, 209 | however, is that these actually short-circuit the rack stack and send back 210 | their respective status codes. See these examples: 211 | 212 | ```ruby 213 | r301 '/wiki/John_Trupiano', '/john' # [1] 214 | r301 %r{/wiki/(.*)}, 'http://www.google.com/?q=$1' # [2] 215 | ``` 216 | 217 | Recall that rules are interpreted from top to bottom. So you can install 218 | "default" rewrite rules if you like. [2] is a sample default rule that 219 | will redirect all other requests to the wiki to a google search. 220 | 221 | ### :send_file, :x_send_file, :send_data 222 | 223 | Calls to #send_file and #x_send_file and #send_data also have the same signature as #rewrite. 224 | If the rule matches, the 'to' parameter is interpreted as a path to a file 225 | to be rendered instead of passing the application call up the rack stack. 226 | 227 | ```ruby 228 | send_file /*/, 'public/spammers.htm', :if => Proc.new { |rack_env| 229 | rack_env['HTTP_REFERER'] =~ 'spammers.com' 230 | } 231 | x_send_file /^blog\/.*/, 'public/blog_offline.htm', :if => Proc.new { |rack_env| 232 | File.exists?('public/blog_offline.htm') 233 | } 234 | send_data /^blog\/.*/, 'public/blog_offline.png', :if => Proc.new { |rack_env| 235 | File.exists?('public/blog_offline.htm') 236 | } 237 | ``` 238 | 239 | ## Options Parameter 240 | 241 | Each rewrite rule takes an optional options parameter. The following options 242 | are supported. 243 | 244 | ### :host 245 | 246 | Using the :host option you can match requests to a specific hostname. 247 | 248 | ```ruby 249 | r301 "/features", "/facial_features", :host => "facerecognizer.com" 250 | ``` 251 | This rule will only match when the hostname is "facerecognizer.com". 252 | 253 | The :host option accepts a symbol, string, or regexp. 254 | 255 | ### :headers 256 | 257 | Using the :headers option you can set custom response headers e.g. for HTTP 258 | caching instructions. 259 | 260 | ```ruby 261 | r301 "/features", "/facial_features", :headers => {'Cache-Control' => 'no-cache'} 262 | ``` 263 | 264 | Please be aware that the :headers value above is evaluated only once at app boot and shared amongst all matching requests. 265 | 266 | Use a Proc as the :headers option if you wish to determine the additional headers at request-time. For example: 267 | 268 | ```ruby 269 | # We want the Expires value to always be 1 year in the future from now. If 270 | # we didn't use a Proc here, then the Expires value would be set just once 271 | # at app startup. The Proc will be evaluated for each matching request. 272 | send_file /^.+\.(?:ico|jpg|jpeg|png|gif|)$/, 273 | 'public/$&', 274 | :headers => lambda { { 'Expires' => 1.year.from_now.httpdate } } 275 | ``` 276 | 277 | ### :scheme 278 | 279 | Using the :scheme option you can restrict the matching of a rule by the protocol of a given request. 280 | 281 | ```ruby 282 | # Redirect all http traffic to https 283 | r301 %r{.*}, 'https://www.example.tld$&', :scheme => 'http' 284 | ``` 285 | 286 | The :scheme option accepts a symbol, string, or regexp. 287 | 288 | ### :method 289 | 290 | Using the :method option you can restrict the matching of a rule by the HTTP 291 | method of a given request. 292 | 293 | ```ruby 294 | # redirect GET's one way 295 | r301 "/players", "/current_players", :method => :get 296 | 297 | # and redirect POST's another way 298 | r302 "/players", "/no_longer_available.html?message=No&longer&supported", :method => :post 299 | ``` 300 | 301 | The :method option accepts a symbol, string, or regexp. 302 | 303 | ### :if 304 | 305 | Using the :if option you can define arbitrary rule guards. Guards are any 306 | object responding to #call that return true or false indicating whether the 307 | rule matches. The following example demonstrates how the presence of a 308 | maintenance page on the filesystem can be utilized to take your site(s) offline. 309 | 310 | ```ruby 311 | maintenance_file = File.join(RAILS_ROOT, 'public', 'system', 'maintenance.html') 312 | x_send_file /.*/, maintenance_file, :if => Proc.new { |rack_env| 313 | File.exists?(maintenance_file) 314 | } 315 | ``` 316 | 317 | ### :not 318 | 319 | Using the :not option you can negatively match against the path. This can 320 | be useful when writing a regular expression match is difficult. 321 | 322 | ```ruby 323 | rewrite %r{^\/features}, '/facial_features', :not => '/features' 324 | ``` 325 | 326 | This will not match the relative URL /features but would match /features.xml. 327 | 328 | ## Tips 329 | 330 | ### Keeping your querystring 331 | 332 | When rewriting a URL, you may want to keep your querystring intact (for 333 | example if you're tracking traffic sources). You will need to include a 334 | capture group and substitution pattern in your rewrite rule to achieve this. 335 | 336 | ```ruby 337 | rewrite %r{/wiki/John_Trupiano(\?.*)?}, '/john$1' 338 | ``` 339 | 340 | This rule will store the querystring in a capture group (via `(?.*)` ) and 341 | will substitute the querystring back into the rewritten URL (via `$1`). 342 | 343 | ### Arbitrary Rewriting 344 | 345 | All rules support passing a Proc as the first or second argument allowing you to 346 | perform arbitrary rewrites. The following rule will rewrite all requests 347 | received between 12AM and 8AM to an unavailable page. 348 | 349 | ```ruby 350 | rewrite %r{(.*)}, lambda { |match, rack_env| 351 | Time.now.hour < 8 ? "/unavailable.html" : match[1] 352 | } 353 | ``` 354 | 355 | This rule will redirect all requests paths starting with a current date 356 | string to /today.html 357 | 358 | ```ruby 359 | r301 lambda { "/#{Time.current.strftime(%m%d%Y)}.html" }, '/today.html' 360 | ``` 361 | 362 | 363 | ##Alternative loaders 364 | 365 | rack-rewrite can also be driven by external loaders. Bundled with this library is a loader for YAML files. 366 | 367 | ``` 368 | config.middleware.insert_before(Rack::Lock, Rack::Rewrite, 369 | :klass => Rack::Rewrite::YamlRuleSet, 370 | :options => {:file_name => @file_name}) 371 | ``` 372 | 373 | Using syntax like 374 | 375 | ``` 376 | - 377 | method: r301 378 | from: !ruby/regexp '/(.*)/print' 379 | to : '$1/printer_friendly' 380 | options : 381 | host : 'example.com' 382 | ``` 383 | 384 | Any class can be used here as long as: 385 | 386 | - the class take an options hash 387 | - `#rules` returns an array of `Rack::Rewrite::Rule` instances 388 | 389 | ## Contribute 390 | 391 | rack-rewrite is maintained by [@travisjeffery](http://github.com/travisjeffery). 392 | 393 | Here's the most direct way to get your work merged into the project. 394 | 395 | - Fork the project 396 | - Clone down your fork 397 | - Create a feature branch 398 | - Hack away and add tests, not necessarily in that order 399 | - Make sure everything still passes by running tests 400 | - If necessary, rebase your commits into logical chunks without errors 401 | - Push the branch up to your fork 402 | - Send a pull request for your branch 403 | 404 | ## Copyright 405 | 406 | Copyright (c) 2012 — John Trupiano, Travis Jeffery. See LICENSE for details. 407 | -------------------------------------------------------------------------------- /test/rule_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RuleTest < Test::Unit::TestCase 4 | 5 | TEST_ROOT = File.dirname(__FILE__) 6 | 7 | def self.should_pass_maintenance_tests 8 | context 'and the maintenance file does in fact exist' do 9 | setup { File.stubs(:exists?).returns(true) } 10 | 11 | should('match for the root') { assert @rule.matches?(rack_env_for('/')) } 12 | should('match for a regular rails route') { assert @rule.matches?(rack_env_for('/users/1')) } 13 | should('match for an html page') { assert @rule.matches?(rack_env_for('/index.html')) } 14 | should('not match for a css file') { assert !@rule.matches?(rack_env_for('/stylesheets/style.css')) } 15 | should('not match for a jpg file') { assert !@rule.matches?(rack_env_for('/images/sls.jpg')) } 16 | should('not match for a png file') { assert !@rule.matches?(rack_env_for('/images/sls.png')) } 17 | end 18 | end 19 | 20 | def self.negative_lookahead_supported? 21 | begin 22 | require 'oniguruma' 23 | rescue LoadError; end 24 | RUBY_VERSION =~ /^1\.9/ || Object.const_defined?(:Oniguruma) 25 | end 26 | 27 | def negative_lookahead_regexp 28 | if RUBY_VERSION =~ /^1\.9/ 29 | # have to use the constructor instead of the literal syntax b/c load errors occur in Ruby 1.8 30 | Regexp.new("(.*)$(? '/abc'} 41 | assert_equal rule.send(:interpret_to, '/abc'), rule.apply!(env)[1]['Location'] 42 | end 43 | end 44 | 45 | supported_status_codes.each do |rule_type| 46 | should "include a link to the result of #interpret_to for a #{rule_type}" do 47 | rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def') 48 | env = {'PATH_INFO' => '/abc'} 49 | assert_match /\/def/, rule.apply!(env)[2][0] 50 | end 51 | end 52 | 53 | should 'keep the QUERY_STRING when a 301 rule matches a URL with a querystring' do 54 | rule = Rack::Rewrite::Rule.new(:r301, %r{/john(.*)}, '/yair$1') 55 | env = {'PATH_INFO' => '/john', 'QUERY_STRING' => 'show_bio=1'} 56 | assert_equal '/yair?show_bio=1', rule.apply!(env)[1]['Location'] 57 | end 58 | 59 | should 'keep the QUERY_STRING when a rewrite rule that requires a querystring matches a URL with a querystring' do 60 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/john(\?.*)}, '/yair$1') 61 | env = {'PATH_INFO' => '/john', 'QUERY_STRING' => 'show_bio=1'} 62 | rule.apply!(env) 63 | assert_equal '/yair', env['PATH_INFO'] 64 | assert_equal 'show_bio=1', env['QUERY_STRING'] 65 | assert_equal '/yair?show_bio=1', env['REQUEST_URI'] 66 | end 67 | 68 | should 'update the QUERY_STRING when a rewrite rule changes its value' do 69 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/(\w+)\?show_bio=(\d)}, '/$1?bio=$2') 70 | env = {'PATH_INFO' => '/john', 'QUERY_STRING' => 'show_bio=1'} 71 | rule.apply!(env) 72 | assert_equal '/john', env['PATH_INFO'] 73 | assert_equal 'bio=1', env['QUERY_STRING'] 74 | assert_equal '/john?bio=1', env['REQUEST_URI'] 75 | end 76 | 77 | should 'set Content-Type header to text/html for a 301 and 302 request for a .html page' do 78 | supported_status_codes.each do |rule_type| 79 | rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.html') 80 | env = {'PATH_INFO' => '/abc'} 81 | assert_equal 'text/html', rule.apply!(env)[1]['Content-Type'] 82 | end 83 | end 84 | 85 | should 'set Content-Type header to text/css for a 301 and 302 request for a .css page' do 86 | supported_status_codes.each do |rule_type| 87 | rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.css') 88 | env = {'PATH_INFO' => '/abc'} 89 | assert_equal 'text/css', rule.apply!(env)[1]['Content-Type'] 90 | end 91 | end 92 | 93 | should 'set additional headers for a 301 and 302 request' do 94 | [:r301, :r302].each do |rule_type| 95 | rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.css', {:headers => {'Cache-Control' => 'no-cache'}}) 96 | env = {'PATH_INFO' => '/abc'} 97 | assert_equal 'no-cache', rule.apply!(env)[1]['Cache-Control'] 98 | end 99 | end 100 | 101 | should 'evaluate additional headers block once per redirect request' do 102 | [:r301, :r302].each do |rule_type| 103 | header_val = 'foo' 104 | rule = Rack::Rewrite::Rule.new(rule_type, %r{/abc}, '/def.css', {:headers => lambda { {'X-Foobar' => header_val} } }) 105 | env = {'PATH_INFO' => '/abc'} 106 | assert_equal 'foo', rule.apply!(env)[1]['X-Foobar'] 107 | header_val = 'bar' 108 | assert_equal 'bar', rule.apply!(env)[1]['X-Foobar'] 109 | end 110 | end 111 | 112 | should 'evaluate additional headers block once per send file request' do 113 | [:send_file, :x_send_file].each do |rule_type| 114 | header_val = 'foo' 115 | rule = Rack::Rewrite::Rule.new(rule_type, /.*/, File.join(TEST_ROOT, 'geminstaller.yml'), {:headers => lambda { {'X-Foobar' => header_val} } }) 116 | env = {'PATH_INFO' => '/abc'} 117 | assert_equal 'foo', rule.apply!(env)[1]['X-Foobar'] 118 | header_val = 'bar' 119 | assert_equal 'bar', rule.apply!(env)[1]['X-Foobar'] 120 | end 121 | end 122 | 123 | context 'Given an :x_send_file rule that matches' do 124 | setup do 125 | @file = File.join(TEST_ROOT, 'geminstaller.yml') 126 | @rule = Rack::Rewrite::Rule.new(:x_send_file, /.*/, @file, :headers => {'Cache-Control' => 'no-cache'}) 127 | env = {'PATH_INFO' => '/abc'} 128 | @response = @rule.apply!(env) 129 | end 130 | 131 | should 'return 200' do 132 | assert_equal 200, @response[0] 133 | end 134 | 135 | should 'return an X-Sendfile header' do 136 | assert @response[1].has_key?('X-Sendfile') 137 | end 138 | 139 | should 'return a Content-Type of text/yaml' do 140 | assert_equal 'text/yaml', @response[1]['Content-Type'] 141 | end 142 | 143 | should 'return the proper Content-Length' do 144 | assert_equal File.size(@file).to_s, @response[1]['Content-Length'] 145 | end 146 | 147 | should 'return additional headers' do 148 | assert_equal 'no-cache', @response[1]['Cache-Control'] 149 | end 150 | 151 | should 'return empty content' do 152 | assert_equal [], @response[2] 153 | end 154 | end 155 | 156 | context 'Given a :send_file rule that matches' do 157 | setup do 158 | @file = File.join(TEST_ROOT, 'geminstaller.yml') 159 | @rule = Rack::Rewrite::Rule.new(:send_file, /.*/, @file, :headers => {'Cache-Control' => 'no-cache'}) 160 | env = {'PATH_INFO' => '/abc'} 161 | @response = @rule.apply!(env) 162 | end 163 | 164 | should 'return 200' do 165 | assert_equal 200, @response[0] 166 | end 167 | 168 | should 'not return an X-Sendfile header' do 169 | assert !@response[1].has_key?('X-Sendfile') 170 | end 171 | 172 | should 'return a Content-Type of text/yaml' do 173 | assert_equal 'text/yaml', @response[1]['Content-Type'] 174 | end 175 | 176 | should 'return the proper Content-Length' do 177 | assert_equal File.size(@file).to_s, @response[1]['Content-Length'] 178 | end 179 | 180 | should 'return additional headers' do 181 | assert_equal 'no-cache', @response[1]['Cache-Control'] 182 | end 183 | 184 | should 'return the contents of geminstaller.yml in an array for Ruby 1.9.2 compatibility' do 185 | assert_equal [File.read(@file)], @response[2] 186 | end 187 | end 188 | 189 | should 'return proper status for send_file or x_send_file if specified' do 190 | [:send_file, :x_send_file].each do |rule_type| 191 | file = File.join(TEST_ROOT, 'geminstaller.yml') 192 | rule = Rack::Rewrite::Rule.new(rule_type, /.*/, file, :status => 503) 193 | env = {'PATH_INFO' => '/abc'} 194 | assert_equal 503, rule.apply!(env)[0] 195 | end 196 | end 197 | 198 | end 199 | 200 | context 'Rule#matches' do 201 | context 'Given rule with :not option which matches "from" string' do 202 | setup do 203 | @rule = Rack::Rewrite::Rule.new(:rewrite, /^\/features/, '/facial_features', :not => '/features') 204 | end 205 | should 'not match PATH_INFO of /features' do 206 | assert !@rule.matches?(rack_env_for("/features")) 207 | end 208 | should 'match PATH_INFO of /features.xml' do 209 | assert @rule.matches?(rack_env_for("/features.xml")) 210 | end 211 | end 212 | 213 | context 'Given rule with :host option of testapp.com' do 214 | setup do 215 | @rule = Rack::Rewrite::Rule.new(:rewrite, /^\/features/, '/facial_features', :host => 'testapp.com') 216 | end 217 | 218 | should 'match PATH_INFO of /features and HOST of testapp.com' do 219 | assert @rule.matches?(rack_env_for("/features", 'SERVER_NAME' => 'testapp.com', "SERVER_PORT" => "8080")) 220 | end 221 | 222 | should 'not match PATH_INFO of /features and HOST of nottestapp.com' do 223 | assert ! @rule.matches?(rack_env_for("/features", 'SERVER_NAME' => 'nottestapp.com', "SERVER_PORT" => "8080")) 224 | end 225 | 226 | should 'match PATH_INFO of /features AND HTTP_X_FORWARDED_HOST of testapp.com and SERVER_NAME of 127.0.0.1' do 227 | assert @rule.matches?(rack_env_for("/features", "SERVER_NAME" => "127.0.0.1", "SERVER_PORT" => "8080", "HTTP_X_FORWARDED_HOST" => "testapp.com")) 228 | end 229 | 230 | should 'not match PATH_INFO of /features AND HTTP_X_FORWARDED_HOST of nottestapp.com and SERVER_NAME of 127.0.0.1' do 231 | assert !@rule.matches?(rack_env_for("/features", "SERVER_NAME" => "127.0.0.1", "SERVER_PORT" => "8080", "HTTP_X_FORWARDED_HOST" => "nottestapp.com")) 232 | end 233 | end 234 | 235 | context 'Given rule with :method option of POST' do 236 | setup do 237 | @rule = Rack::Rewrite::Rule.new(:rewrite, '/features', '/facial_features', :method => 'POST') 238 | end 239 | 240 | should 'match PATH_INFO of /features and REQUEST_METHOD of POST' do 241 | assert @rule.matches?(rack_env_for("/features", 'REQUEST_METHOD' => 'POST')) 242 | end 243 | 244 | should 'not match PATH_INFO of /features and REQUEST_METHOD of DELETE' do 245 | assert ! @rule.matches?(rack_env_for("/features", 'REQUEST_METHOD' => 'DELETE')) 246 | end 247 | end 248 | 249 | context 'Given any rule with a "from" string of /features' do 250 | setup do 251 | @rule = Rack::Rewrite::Rule.new(:rewrite, '/features', '/facial_features') 252 | end 253 | 254 | should 'match PATH_INFO of /features' do 255 | assert @rule.matches?(rack_env_for("/features")) 256 | end 257 | 258 | should 'not match PATH_INFO of /features.xml' do 259 | assert !@rule.matches?(rack_env_for("/features.xml")) 260 | end 261 | 262 | should 'not match PATH_INFO of /my_features' do 263 | assert !@rule.matches?(rack_env_for("/my_features")) 264 | end 265 | end 266 | 267 | context 'Given a rule with the ^ operator' do 268 | setup do 269 | @rule = Rack::Rewrite::Rule.new(:rewrite, %r{^/jason}, '/steve') 270 | end 271 | should 'match with the ^ operator if match is at the beginning of the path' do 272 | assert @rule.matches?(rack_env_for('/jason')) 273 | end 274 | 275 | should 'not match with the ^ operator when match is deeply nested' do 276 | assert !@rule.matches?(rack_env_for('/foo/bar/jason')) 277 | end 278 | end 279 | 280 | context 'Given any rule with a "from" regular expression of /features(.*)' do 281 | setup do 282 | @rule = Rack::Rewrite::Rule.new(:rewrite, %r{/features(.*)}, '/facial_features$1') 283 | end 284 | 285 | should 'match PATH_INFO of /features' do 286 | assert @rule.matches?(rack_env_for("/features")) 287 | end 288 | 289 | should 'match PATH_INFO of /features.xml' do 290 | assert @rule.matches?(rack_env_for('/features.xml')) 291 | end 292 | 293 | should 'match PATH_INFO of /features/1' do 294 | assert @rule.matches?(rack_env_for('/features/1')) 295 | end 296 | 297 | should 'match PATH_INFO of /features?filter_by=name' do 298 | assert @rule.matches?(rack_env_for('/features?filter_by_name=name')) 299 | end 300 | 301 | should 'match PATH_INFO of /features/1?hide_bio=1' do 302 | assert @rule.matches?(rack_env_for('/features/1?hide_bio=1')) 303 | end 304 | end 305 | 306 | context 'Given a rule with a guard that checks for the presence of a file' do 307 | setup do 308 | @rule = Rack::Rewrite::Rule.new(:rewrite, %r{(.)*}, '/maintenance.html', lambda { |rack_env| 309 | File.exists?('maintenance.html') 310 | }) 311 | end 312 | 313 | context 'when the file exists' do 314 | setup do 315 | File.stubs(:exists?).returns(true) 316 | end 317 | 318 | should 'match' do 319 | assert @rule.matches?(rack_env_for('/anything/should/match')) 320 | end 321 | end 322 | 323 | context 'when the file does not exist' do 324 | setup do 325 | File.stubs(:exists?).returns(false) 326 | end 327 | 328 | should 'not match' do 329 | assert !@rule.matches?(rack_env_for('/nothing/should/match')) 330 | end 331 | end 332 | end 333 | 334 | context 'Given the capistrano maintenance.html rewrite rule given in our README' do 335 | setup do 336 | @rule = Rack::Rewrite::Rule.new(:rewrite, /.*/, '/system/maintenance.html', lambda { |rack_env| 337 | maintenance_file = File.join('system', 'maintenance.html') 338 | File.exists?(maintenance_file) && rack_env['PATH_INFO'] !~ /\.(css|jpg|png)/ 339 | }) 340 | end 341 | should_pass_maintenance_tests 342 | end 343 | 344 | if negative_lookahead_supported? 345 | context 'Given the negative lookahead regular expression version of the capistrano maintenance.html rewrite rule given in our README' do 346 | setup do 347 | @rule = Rack::Rewrite::Rule.new(:rewrite, negative_lookahead_regexp, '/system/maintenance.html', lambda { |rack_env| 348 | File.exists?(File.join('public', 'system', 'maintenance.html')) 349 | }) 350 | end 351 | should_pass_maintenance_tests 352 | end 353 | end 354 | 355 | context 'Given the CNAME alternative rewrite rule in our README' do 356 | setup do 357 | @rule = Rack::Rewrite::Rule.new(:r301, %r{.*}, 'http://mynewdomain.com$&', lambda {|rack_env| 358 | rack_env['SERVER_NAME'] != 'mynewdomain.com' 359 | }) 360 | end 361 | 362 | should 'match requests for domain myolddomain.com and redirect to mynewdomain.com' do 363 | env = {'PATH_INFO' => '/anything', 'QUERY_STRING' => 'abc=1', 'SERVER_NAME' => 'myolddomain.com'} 364 | assert @rule.matches?(env) 365 | rack_response = @rule.apply!(env) 366 | assert_equal 'http://mynewdomain.com/anything?abc=1', rack_response[1]['Location'] 367 | end 368 | 369 | should 'not match requests for domain mynewdomain.com' do 370 | assert !@rule.matches?({'PATH_INFO' => '/anything', 'SERVER_NAME' => 'mynewdomain.com'}) 371 | end 372 | end 373 | 374 | context 'Given a lambda matcher' do 375 | setup do 376 | @rule = Rack::Rewrite::Rule.new(:r302, ->{ Thread.current[:test_matcher] }, '/today' ) 377 | end 378 | should 'call the lambda and match appropriately' do 379 | Thread.current[:test_matcher] = '/abcd' 380 | assert @rule.matches?(rack_env_for("/abcd")) 381 | assert !@rule.matches?(rack_env_for("/DEFG")) 382 | Thread.current[:test_matcher] = /DEFG$/ 383 | assert !@rule.matches?(rack_env_for("/abcd")) 384 | assert @rule.matches?(rack_env_for("/DEFG")) 385 | end 386 | end 387 | end 388 | 389 | context 'Rule#interpret_to' do 390 | should 'return #to when #from is a string' do 391 | rule = Rack::Rewrite::Rule.new(:rewrite, '/abc', '/def') 392 | assert_equal '/def', rule.send(:interpret_to, rack_env_for('/abc')) 393 | end 394 | 395 | should 'replace $1 on a match' do 396 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)}, '/people/$1') 397 | assert_equal '/people/1', rule.send(:interpret_to, rack_env_for("/person_1")) 398 | end 399 | 400 | should 'be able to catch querystrings with a regexp match' do 401 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)(.*)}, '/people/$1$2') 402 | assert_equal '/people/1?show_bio=1', rule.send(:interpret_to, rack_env_for('/person_1?show_bio=1')) 403 | end 404 | 405 | should 'be able to make 10 replacements' do 406 | # regexp to reverse 10 characters 407 | 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') 408 | assert_equal 'jihgfedcba', rule.send(:interpret_to, rack_env_for("abcdefghij")) 409 | end 410 | 411 | should 'replace $& on a match' do 412 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{.*}, 'http://example.org$&') 413 | assert_equal 'http://example.org/person/1', rule.send(:interpret_to, rack_env_for("/person/1")) 414 | end 415 | 416 | should 'ignore empty captures' do 417 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person(_\d+)?}, '/people/$1') 418 | assert_equal '/people/', rule.send(:interpret_to, rack_env_for("/person")) 419 | end 420 | 421 | should 'call to with from when it is a lambda' do 422 | rule = Rack::Rewrite::Rule.new(:rewrite, 'a', lambda { |from, env| from * 2 }) 423 | assert_equal 'aa', rule.send(:interpret_to, rack_env_for('a')) 424 | end 425 | 426 | should 'call to with from match data' do 427 | rule = Rack::Rewrite::Rule.new(:rewrite, %r{/person_(\d+)(.*)}, lambda {|match, env| "people-#{match[1].to_i * 3}#{match[2]}"}) 428 | assert_equal 'people-3?show_bio=1', rule.send(:interpret_to, rack_env_for('/person_1?show_bio=1')) 429 | end 430 | 431 | should 'call to with from lambda match data' do 432 | rule = Rack::Rewrite::Rule.new(:rewrite, ->{ Thread.current[:test_matcher]}, ->(match, env){ match[1][0] }) 433 | Thread.current[:test_matcher] = /^\/(alpha|beta|gamma)$/ 434 | assert_equal 'b', rule.send(:interpret_to, rack_env_for('/beta')) 435 | Thread.current[:test_matcher] = /^\/(zulu)/ 436 | assert_equal 'z', rule.send(:interpret_to, rack_env_for('/zulu')) 437 | end 438 | end 439 | 440 | context 'Mongel 1.2.0.pre2 edge case: root url with a query string' do 441 | should 'handle a nil PATH_INFO variable without errors' do 442 | rule = Rack::Rewrite::Rule.new(:r301, '/a', '/') 443 | assert_equal '?exists', rule.send(:build_path_from_env, {'QUERY_STRING' => 'exists'}) 444 | end 445 | end 446 | 447 | def rack_env_for(url, options = {}) 448 | components = url.split('?') 449 | {'PATH_INFO' => components[0], 'QUERY_STRING' => components[1] || ''}.merge(options) 450 | end 451 | end 452 | --------------------------------------------------------------------------------