Skip to content

Instantly share code, notes, and snippets.

@jtrupiano
Created October 13, 2009 02:33
Show Gist options
  • Save jtrupiano/208916 to your computer and use it in GitHub Desktop.
Save jtrupiano/208916 to your computer and use it in GitHub Desktop.
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.
# 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
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment