Rack::Rewrite
About
rack-rewrite
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.
Rack::Rewrite is available as a gem: gem install rack-rewrite
Full source code is at github: http://github.com/jtrupiano/rack-rewrite
Use Cases
Rebuild of existing site in a new technology
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 old URL’s, we can transfer that google ranking to the new site. An example rule might look like:
r301 '/contact-us.php', '/contact-us' r301 '/wiki/John_Trupiano', '/john'
Retiring old routes
As a web application evolves you will sometimes have to make changes to routes. The danger here is that any URL’s previously generated (in a transactional email for instance) will have the URL hard-coded. In order for your rails app to continue to serve this URL, you’ll need to add an extra entry to your routes file. Alternatively, you could use Rack::Rewrite to redirect or pass through requests to these routes and keep your routes.rb clean.
rewrite %r{/features(.*)}, '/facial_features$1'
Site maintenance
This common capistrano + apache ruleset
RewriteCond %{REQUEST_URI} !\.(css|jpg|png)$
RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
RewriteCond %{SCRIPT_FILENAME} !maintenance.html
RewriteRule ^.*$ /system/maintenance.html [L]
can be replaced by Rack::Rewrite with the following rule
maintenance_file = File.join(RAILS_ROOT, 'public', 'system', 'maintenance.html')
# Ruby 1.9.x (w/ regexp negative lookahead)
send_file /(.*)$(?<!css|png|jpg)/, maintenance_file, :if => Proc.new { |rack_env|
File.exists?(maintenance_file)
}
# Vanilla Ruby 1.8.x
send_file /.*/, maintenance_file, :if => Proc.new { |rack_env|
File.exists?(maintenance_file) && rack_env['REQUEST_URI'] !~ /\.(css|jpg|png)/
}
Usage
Sample usage in a rackup file
gem 'rack-rewrite', '~> 0.2.0'
require 'rack-rewrite
use Rack::Rewrite do
rewrite '/wiki/John_Trupiano', '/john'
r301 '/wiki/Yair_Flicker', '/yair'
r302 '/wiki/Greg_Jastrab', '/greg'
r301 %r{/wiki/(\w+)_\w+}, '/$1'
end
Sample usage in a rails app
config.gem 'rack-rewrite', '~> 0.2.0'
require 'rack-rewrite
config.middleware.insert_before(Rack::Lock, Rack::Rewrite) do
rewrite '/wiki/John_Trupiano', '/john'
r301 '/wiki/Yair_Flicker', '/yair'
r302 '/wiki/Greg_Jastrab', '/greg'
r301 %r{/wiki/(\w+)_\w+}, '/$1'
end
Code
# This is actually available as a gem: gem install rack-rewrite# Full source code including tests is on github: http://github.com/jtrupiano/rack-rewrite module Rack # 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. class Rewrite def initialize(app, &rule_block) @app = app @rule_set = RuleSet.new @rule_set.instance_eval(&rule_block) if block_given? end def call(env) if matched_rule = find_first_matching_rule(env) rack_response = matched_rule.apply!(env) # Don't invoke the app if applying the rule returns a rack response return rack_response unless rack_response === true end @app.call(env) end private def find_first_matching_rule(env) #:nodoc: @rule_set.rules.detect { |rule| rule.matches?(env) } end class RuleSet attr_reader :rules def initialize #:nodoc: @rules = [] end protected # We're explicitly defining private functions for our DSL rather than # using method_missing # Creates a rewrite rule that will simply rewrite the REQUEST_URI, # PATH_INFO, and QUERYSTRING headers of the Rack environment. The # user's browser will continue to show the initially requested URL. # # rewrite '/wiki/John_Trupiano', '/john' # rewrite %r{/wiki/(\w+)_\w+}, '/$1' # rewrite %r{(.*)}, '/maintenance.html', :if => lambda { File.exists?('maintenance.html') } def rewrite(from, to, *args) options = args.last.is_a?(Hash) ? args.last : {} @rules << Rule.new(:rewrite, from, to, options[:if]) end # Creates a redirect rule that will send a 301 when matching. # # r301 '/wiki/John_Trupiano', '/john' # r301 '/contact-us.php', '/contact-us' def r301(from, to, *args) options = args.last.is_a?(Hash) ? args.last : {} @rules << Rule.new(:r301, from, to, options[:if]) end # Creates a redirect rule that will send a 302 when matching. # # r302 '/wiki/John_Trupiano', '/john' # r302 '/wiki/(.*)', 'http://www.google.com/?q=$1' def r302(from, to, *args) options = args.last.is_a?(Hash) ? args.last : {} @rules << Rule.new(:r302, from, to, options[:if]) end # Creates a rule that will render a file if matched. # # send_file /*/, 'public/system/maintenance.html', # :if => Proc.new { File.exists?('public/system/maintenance.html') } def send_file(from, to, *args) options = args.last.is_a?(Hash) ? args.last : {} @rules << Rule.new(:send_file, from, to, options[:if]) end # Creates a rule that will render a file using x-send-file # if matched. # # x_send_file /*/, 'public/system/maintenance.html', # :if => Proc.new { File.exists?('public/system/maintenance.html') } def x_send_file(from, to, *args) options = args.last.is_a?(Hash) ? args.last : {} @rules << Rule.new(:x_send_file, from, to, options[:if]) end end # TODO: Break rules into subclasses class Rule #:nodoc: attr_reader :rule_type, :from, :to, :guard def initialize(rule_type, from, to, guard=nil) #:nodoc: @rule_type, @from, @to, @guard = rule_type, from, to, guard end def matches?(rack_env) #:nodoc: return false if !guard.nil? && !guard.call(rack_env) path = rack_env['REQUEST_URI'] if self.from.is_a?(Regexp) || (Object.const_defined?(:Oniguruma) && self.from.is_a?(Oniguruma::ORegexp)) path =~ self.from elsif self.from.is_a?(String) path == self.from else false end 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: interpreted_to = self.send(:interpret_to, env['REQUEST_URI'], env) case self.rule_type when :r301 [301, {'Location' => interpreted_to, 'Content-Type' => 'text/html'}, ['Redirecting...']] when :r302 [302, {'Location' => interpreted_to, 'Content-Type' => 'text/html'}, ['Redirecting...']] when :rewrite # return [200, {}, {:content => env.inspect}] env['REQUEST_URI'] = interpreted_to if q_index = interpreted_to.index('?') env['PATH_INFO'] = interpreted_to[0..q_index-1] env['QUERYSTRING'] = interpreted_to[q_index+1..interpreted_to.size-1] else env['PATH_INFO'] = interpreted_to env['QUERYSTRING'] = '' end true when :send_file [200, { 'Content-Length' => ::File.size(interpreted_to).to_s, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to)) }, ::File.read(interpreted_to)] when :x_send_file [200, { 'X-Sendfile' => interpreted_to, 'Content-Length' => ::File.size(interpreted_to).to_s, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to)) }, []] else raise Exception.new("Unsupported rule: #{self.rule_type}") end end private def interpret_to(path, env={}) #:nodoc: return interpret_to_proc(path, env) if self.to.is_a?(Proc) return computed_to(path) if compute_to?(path) self.to end def interpret_to_proc(path, env) return self.to.call(match(path), env) if self.from.is_a?(Regexp) self.to.call(self.from, env) end def compute_to?(path) self.from.is_a?(Regexp) && match(path) end def match(path) self.from.match(path) end def computed_to(path) # is there a better way to do this? computed_to = self.to.dup (match(path).size - 1).downto(1) do |num| computed_to.gsub!("$#{num}", match(path)[num]) end return computed_to end end endend