401 lines
9.2 KiB
Ruby
401 lines
9.2 KiB
Ruby
require 'pp'
|
|
|
|
module Apache
|
|
# Handle the creation of RewriteRules, RewriteConds, Redirects, and RedirectMatches
|
|
module Rewrites
|
|
# Enable the rewrite engine, optionally setting the logging level
|
|
#
|
|
# enable_rewrite_engine :log_level => 1 #=>
|
|
# RewriteEngine on
|
|
# RewriteLogLevel 1
|
|
def enable_rewrite_engine(options = {})
|
|
self << ''
|
|
rewrite_engine! :on
|
|
options.each do |option, value|
|
|
case option
|
|
when :log_level
|
|
rewrite_log_level! value
|
|
end
|
|
end
|
|
self << ''
|
|
end
|
|
|
|
# Pass the block to RewriteManager.build
|
|
def rewrites(*opt, &block)
|
|
self + indent(RewriteManager.build(*opt, &block))
|
|
self << ''
|
|
end
|
|
|
|
def rewrite(*opt, &block)
|
|
raise "You probably want rewrites #{quoteize(*opt) * " "} do" if block
|
|
end
|
|
|
|
# Create a permanent Redirect
|
|
#
|
|
# r301 '/here', '/there' #=> Redirect permanent "/here" "/there"
|
|
def r301(*opt)
|
|
self << "Redirect permanent #{quoteize(*opt) * " "}"
|
|
end
|
|
end
|
|
|
|
# Handle the creation of Rewritable things
|
|
class RewriteManager
|
|
class << self
|
|
attr_accessor :rewrites
|
|
|
|
# Reset the current list of rewrites
|
|
def reset!
|
|
@rewrites = []
|
|
end
|
|
|
|
# Build rewritable things from the provided block
|
|
def build(*opt, &block)
|
|
reset!
|
|
|
|
@any_tests = false
|
|
@needs_tests = false
|
|
self.instance_eval(&block)
|
|
|
|
name = opt.first || (@rewrites.empty? ? 'unnamed block' : "#{@rewrites.first.from} => #{@rewrites.first.to}")
|
|
|
|
if !@any_tests && !@rewrites.empty?
|
|
puts " [#{"rewrite".foreground(:blue)}] no tests found for #{name}"
|
|
end
|
|
|
|
if @needs_tests
|
|
puts " [#{"rewrite".foreground(:blue)}] #{name} needs more tests"
|
|
end
|
|
|
|
output = @rewrites.collect(&:to_a).flatten
|
|
output.unshift("# #{name}") if opt.first
|
|
|
|
output
|
|
end
|
|
|
|
# Commit the latest rewritable thing to the list of rewrites
|
|
def commit!
|
|
@rewrites << @rewrite
|
|
@rewrite = nil
|
|
end
|
|
|
|
# Ensure that there's a RewriteRule to be worked with
|
|
def ensure_rewrite!
|
|
@rewrite = RewriteRule.new if !@rewrite
|
|
end
|
|
|
|
# Create a RewriteRule with the given options
|
|
#
|
|
# rewrite %r{/here(.*)}, '/there$1', :last => true #=>
|
|
# RewriteRule "/here(.*)" "/there$1" [L]
|
|
def rewrite(*opts)
|
|
ensure_rewrite!
|
|
@rewrite.rule(*opts)
|
|
commit!
|
|
end
|
|
|
|
alias :rule :rewrite
|
|
|
|
# Create a RewriteCond with the given options
|
|
#
|
|
# cond "%{REQUEST_FILENAME}", "^/here" #=>
|
|
# RewriteCond "%{REQUEST_FILENAME}", "^/here"
|
|
def cond(*opts)
|
|
ensure_rewrite!
|
|
@rewrite.cond(*opts)
|
|
end
|
|
|
|
# Create a permanent RedirectMatch
|
|
#
|
|
# r301 %r{/here(.*)}, "/there$1" #=>
|
|
# RedirectMatch permanent "/here(.*)" "/there$1"
|
|
def r301(*opts)
|
|
redirect = RedirectMatchPermanent.new
|
|
redirect.rule(*opts)
|
|
|
|
@rewrites << redirect
|
|
end
|
|
|
|
# Test the rewritable things defined in this block
|
|
def rewrite_test(from, to, opts = {})
|
|
@any_tests = true
|
|
orig_from = from.dup
|
|
@rewrites.each do |r|
|
|
pre_from = from.dup
|
|
if r.match?(from, opts)
|
|
from = r.test(from, opts)
|
|
from = pre_from if (r.to == '-')
|
|
from = :http_forbidden if (r.forbidden?)
|
|
break if r.stop_if_match?
|
|
end
|
|
end
|
|
|
|
if from != to
|
|
puts " [#{"rewrite".foreground(:blue)}] #{orig_from} >> #{to} failed!"
|
|
puts " [#{"rewrite".foreground(:blue)}] Result: #{from}"
|
|
end
|
|
end
|
|
|
|
def needs_tests
|
|
@needs_tests = true
|
|
end
|
|
|
|
def cond_not_a_file(opts = {})
|
|
cond_file_flag '!-f', opts
|
|
end
|
|
|
|
def cond_is_a_file(opts = {})
|
|
cond_file_flag '-f', opts
|
|
end
|
|
|
|
def cond_not_a_directory(opts = {})
|
|
cond_file_flag '!-d', opts
|
|
end
|
|
|
|
def cond_is_a_directory(opts = {})
|
|
cond_file_flag '-d', opts
|
|
end
|
|
|
|
private
|
|
def cond_file_flag(flag, opts)
|
|
cond opts[:filename_only] ? "%{REQUEST_FILENAME}" : "%{DOCUMENT_ROOT}%{REQUEST_FILENAME}", flag
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
# Common methods for testing rewritable things that use regular expressions
|
|
module RegularExpressionMatcher
|
|
# Test this rewritable thing
|
|
def test(from, opts = {})
|
|
from = from.gsub(@from, @to.gsub(/\$([0-9])/) { |m| '\\' + $1 })
|
|
replace_placeholders(from, opts)
|
|
end
|
|
|
|
def match?(from, opts = {})
|
|
replace_placeholders(from, opts)[@from]
|
|
end
|
|
|
|
# Replace the placeholders in this rewritable thing
|
|
def replace_placeholders(s, opts)
|
|
opts.each do |opt, value|
|
|
case value
|
|
when String
|
|
s = s.gsub('%{' + opt.to_s.upcase + '}', value)
|
|
end
|
|
end
|
|
s.gsub(%r{%\{[^\}]+\}}, '')
|
|
end
|
|
end
|
|
|
|
# A matchable thing to be extended
|
|
class MatchableThing
|
|
include Apache::Quoteize
|
|
|
|
attr_reader :from, :to
|
|
|
|
# The Apache directive tag for this thing
|
|
def tag; raise 'Override this method'; end
|
|
|
|
def initialize
|
|
@from = nil
|
|
@to = nil
|
|
end
|
|
|
|
def rule(from, to)
|
|
@from = from
|
|
@to = to
|
|
end
|
|
|
|
def to_s
|
|
"#{tag} #{[quoteize(@from), quoteize(@to)].compact.flatten * " "}"
|
|
end
|
|
|
|
def to_a
|
|
[ to_s ]
|
|
end
|
|
|
|
def stop_if_match?; false; end
|
|
def forbidden?; false; end
|
|
end
|
|
|
|
# A RewriteRule definition
|
|
class RewriteRule < MatchableThing
|
|
include RegularExpressionMatcher
|
|
|
|
def tag; 'RewriteRule'; end
|
|
|
|
def initialize
|
|
super
|
|
@conditions = []
|
|
@options = nil
|
|
@input_options = {}
|
|
end
|
|
|
|
# Define the rule, passing in additional options
|
|
#
|
|
# rule %r{^/here}, '/there', { :last => true, :preserve_query_string => true }
|
|
def rule(from, to, options = {})
|
|
super(from, to)
|
|
|
|
raise "from must be a Regexp" if !from.kind_of?(Regexp)
|
|
|
|
@input_options = options
|
|
|
|
options = options.collect do |key, value|
|
|
case key
|
|
when :last
|
|
'L'
|
|
when :forbidden
|
|
'F'
|
|
when :no_escape
|
|
'NE'
|
|
when :redirect
|
|
(value == true) ? 'R' : "R=#{value}"
|
|
when :pass_through
|
|
'PT'
|
|
when :preserve_query_string, :query_string_append
|
|
'QSA'
|
|
when :env
|
|
"E=#{value}"
|
|
end
|
|
end.compact.sort
|
|
|
|
@options = !options.empty? ? "[#{options * ','}]" : nil
|
|
end
|
|
|
|
# Add a RewriteCondition to this RewriteRule
|
|
def cond(from, to, *opts)
|
|
rewrite_cond = RewriteCondition.new
|
|
rewrite_cond.cond(from, to, *opts)
|
|
|
|
@conditions << rewrite_cond
|
|
end
|
|
|
|
def to_s
|
|
"#{tag} #{[quoteize(@from.source), quoteize(@to), @options].compact.flatten * " "}"
|
|
end
|
|
|
|
def to_a
|
|
[ ('' if !@conditions.empty?), @conditions.collect(&:to_s), super ].flatten
|
|
end
|
|
|
|
# Test this RewriteRule, ensuring the RewriteConds also match
|
|
def test(from, opts = {})
|
|
opts[:request_uri] = from
|
|
result = from
|
|
|
|
result = super(from, opts) if match?(from, opts)
|
|
|
|
replace_placeholders(result, opts)
|
|
end
|
|
|
|
def match?(from, opts = {})
|
|
opts[:request_uri] = from
|
|
ok = true
|
|
|
|
@conditions.each do |c|
|
|
ok = false if !c.test(from, opts)
|
|
end
|
|
|
|
if ok
|
|
super(from, opts)
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def stop_if_match?
|
|
@input_options[:last]
|
|
end
|
|
|
|
def forbidden?
|
|
@input_options[:forbidden]
|
|
end
|
|
end
|
|
|
|
# A permanent RedirectMatch
|
|
class RedirectMatchPermanent < MatchableThing
|
|
include RegularExpressionMatcher
|
|
|
|
def tag; 'RedirectMatch permanent'; end
|
|
|
|
def rule(from, to)
|
|
super(from, to)
|
|
|
|
raise "from must be a Regexp" if !from.kind_of?(Regexp)
|
|
end
|
|
|
|
def to_s
|
|
"#{tag} #{[quoteize(@from.source), quoteize(@to)].compact.flatten * " "}"
|
|
end
|
|
|
|
def stop_if_match; true; end
|
|
end
|
|
|
|
# A RewriteCond
|
|
class RewriteCondition < MatchableThing
|
|
include RegularExpressionMatcher
|
|
|
|
def tag; 'RewriteCond'; end
|
|
|
|
# Define a RewriteCond
|
|
#
|
|
# rule "%{REQUEST_FILENAME}", "^/here", :case_insensitive #=>
|
|
# RewriteCond "%{REQUEST_FILENAME}" "^/here" [NC]
|
|
def rule(from, to, *opts)
|
|
super(from, to)
|
|
|
|
options = opts.collect do |opt|
|
|
case opt
|
|
when :or
|
|
'OR'
|
|
when :case_insensitive
|
|
'NC'
|
|
when :no_vary
|
|
'NV'
|
|
end
|
|
end
|
|
|
|
@options = (!options.empty?) ? "[#{options * ','}]" : nil
|
|
end
|
|
|
|
alias :cond :rule
|
|
|
|
def initialize
|
|
super
|
|
@options = nil
|
|
end
|
|
|
|
def to_s
|
|
"#{tag} #{[quoteize(@from), quoteize(@to), @options].compact.flatten * " "}"
|
|
end
|
|
|
|
# Test this RewriteCond
|
|
def test(from, opts = {})
|
|
super(from, opts)
|
|
source = replace_placeholders(@from, opts)
|
|
|
|
to = @to
|
|
reverse = false
|
|
|
|
if @to[0..0] == '!'
|
|
reverse = true
|
|
to = @to[1..-1]
|
|
end
|
|
|
|
result = false
|
|
case to[0..0]
|
|
when '-'
|
|
case to
|
|
when '-f'
|
|
result = opts[:files].include? source if opts[:files]
|
|
end
|
|
else
|
|
result = source[Regexp.new(to)]
|
|
end
|
|
|
|
reverse ? !result : result
|
|
end
|
|
end
|
|
end
|