rack-rewrite-matches 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -0
- data/History.rdoc +61 -0
- data/LICENSE +22 -0
- data/README.markdown +332 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/rack-rewrite.rb +1 -0
- data/lib/rack/rewrite.rb +28 -0
- data/lib/rack/rewrite/rule.rb +247 -0
- data/lib/rack/rewrite/version.rb +5 -0
- data/rack-rewrite.gemspec +54 -0
- data/test/geminstaller.yml +9 -0
- data/test/rack-rewrite_test.rb +136 -0
- data/test/rule_test.rb +429 -0
- data/test/test_helper.rb +13 -0
- metadata +132 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.3.3
|
data/lib/rack-rewrite.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/rewrite'
|
data/lib/rack/rewrite.rb
ADDED
@@ -0,0 +1,28 @@
|
|
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, &rule_block)
|
9
|
+
@app = app
|
10
|
+
@rule_set = RuleSet.new
|
11
|
+
@rule_set.instance_eval(&rule_block) if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
if matched_rule = find_first_matching_rule(env)
|
16
|
+
rack_response = matched_rule.apply!(env)
|
17
|
+
# Don't invoke the app if applying the rule returns a rack response
|
18
|
+
return rack_response unless rack_response === true
|
19
|
+
end
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def find_first_matching_rule(env) #:nodoc:
|
25
|
+
@rule_set.rules.detect { |rule| rule.matches?(env) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'rack/mime'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Rewrite
|
5
|
+
class RuleSet
|
6
|
+
attr_reader :rules
|
7
|
+
def initialize #: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
|
+
private
|
94
|
+
def add_rule(method, from, to, options = {}) #:nodoc:
|
95
|
+
@rules << Rule.new(method.to_sym, from, to, options)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
# TODO: Break rules into subclasses
|
101
|
+
class Rule #:nodoc:
|
102
|
+
attr_reader :rule_type, :from, :to, :options
|
103
|
+
def initialize(rule_type, from, to, options={}) #:nodoc:
|
104
|
+
@rule_type, @from, @to, @options = rule_type, from, to, normalize_options(options)
|
105
|
+
end
|
106
|
+
|
107
|
+
def matches?(rack_env) #:nodoc:
|
108
|
+
return false if options[:if].respond_to?(:call) && !options[:if].call(rack_env)
|
109
|
+
path = build_path_from_env(rack_env)
|
110
|
+
|
111
|
+
self.match_options?(rack_env) && string_matches?(path, self.from)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Either (a) return a Rack response (short-circuiting the Rack stack), or
|
115
|
+
# (b) alter env as necessary and return true
|
116
|
+
def apply!(env) #:nodoc:
|
117
|
+
interpreted_to = self.interpret_to(env)
|
118
|
+
additional_headers = {}
|
119
|
+
if @options[:headers]
|
120
|
+
if @options[:headers].respond_to?(:call)
|
121
|
+
additional_headers = @options[:headers].call(*@matches) || {}
|
122
|
+
else
|
123
|
+
additional_headers = @options[:headers] || {}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
status = @options[:status] || 200
|
127
|
+
case self.rule_type
|
128
|
+
when :r301
|
129
|
+
[301, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
|
130
|
+
when :r302
|
131
|
+
[302, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
|
132
|
+
when :r303
|
133
|
+
[303, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
|
134
|
+
when :r307
|
135
|
+
[307, {'Location' => interpreted_to, 'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))}.merge!(additional_headers), [redirect_message(interpreted_to)]]
|
136
|
+
when :rewrite
|
137
|
+
# return [200, {}, {:content => env.inspect}]
|
138
|
+
env['REQUEST_URI'] = interpreted_to
|
139
|
+
if q_index = interpreted_to.index('?')
|
140
|
+
env['PATH_INFO'] = interpreted_to[0..q_index-1]
|
141
|
+
env['QUERY_STRING'] = interpreted_to[q_index+1..interpreted_to.size-1]
|
142
|
+
else
|
143
|
+
env['PATH_INFO'] = interpreted_to
|
144
|
+
env['QUERY_STRING'] = ''
|
145
|
+
end
|
146
|
+
true
|
147
|
+
when :send_file
|
148
|
+
[status, {
|
149
|
+
'Content-Length' => ::File.size(interpreted_to).to_s,
|
150
|
+
'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))
|
151
|
+
}.merge!(additional_headers), [::File.read(interpreted_to)]]
|
152
|
+
when :x_send_file
|
153
|
+
[status, {
|
154
|
+
'X-Sendfile' => interpreted_to,
|
155
|
+
'Content-Length' => ::File.size(interpreted_to).to_s,
|
156
|
+
'Content-Type' => Rack::Mime.mime_type(::File.extname(interpreted_to))
|
157
|
+
}.merge!(additional_headers), []]
|
158
|
+
else
|
159
|
+
raise Exception.new("Unsupported rule: #{self.rule_type}")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
def interpret_to(env) #:nodoc:
|
165
|
+
path = build_path_from_env(env)
|
166
|
+
return interpret_to_proc(path, env) if self.to.is_a?(Proc)
|
167
|
+
return computed_to(path) if compute_to?(path)
|
168
|
+
self.to
|
169
|
+
end
|
170
|
+
|
171
|
+
def is_a_regexp?(obj)
|
172
|
+
obj.is_a?(Regexp) || (Object.const_defined?(:Oniguruma) && obj.is_a?(Oniguruma::ORegexp))
|
173
|
+
end
|
174
|
+
|
175
|
+
def match_options?(env, path = build_path_from_env(env))
|
176
|
+
matches = []
|
177
|
+
request = Rack::Request.new(env)
|
178
|
+
|
179
|
+
# negative matches
|
180
|
+
matches << !string_matches?(path, options[:not]) if options[:not]
|
181
|
+
|
182
|
+
# possitive matches
|
183
|
+
matches << string_matches?(env['REQUEST_METHOD'], options[:method]) if options[:method]
|
184
|
+
matches << string_matches?(request.host, options[:host]) if options[:host]
|
185
|
+
matches << string_matches?(request.scheme, options[:scheme]) if options[:scheme]
|
186
|
+
|
187
|
+
matches.all?
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
def normalize_options(arg)
|
192
|
+
options = arg.respond_to?(:call) ? {:if => arg} : arg
|
193
|
+
options.symbolize_keys! if options.respond_to? :symbolize_keys!
|
194
|
+
options.freeze
|
195
|
+
end
|
196
|
+
|
197
|
+
def interpret_to_proc(path, env)
|
198
|
+
return self.to.call(match(path), env) if self.from.is_a?(Regexp)
|
199
|
+
self.to.call(self.from, env)
|
200
|
+
end
|
201
|
+
|
202
|
+
def compute_to?(path)
|
203
|
+
self.is_a_regexp?(from) && match(path)
|
204
|
+
end
|
205
|
+
|
206
|
+
def match(path)
|
207
|
+
self.from.match(path)
|
208
|
+
end
|
209
|
+
|
210
|
+
def string_matches?(string, matcher)
|
211
|
+
if self.is_a_regexp?(matcher)
|
212
|
+
string =~ matcher
|
213
|
+
elsif matcher.is_a?(String)
|
214
|
+
string == matcher
|
215
|
+
elsif matcher.is_a?(Symbol)
|
216
|
+
string.downcase == matcher.to_s.downcase
|
217
|
+
else
|
218
|
+
false
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def computed_to(path)
|
223
|
+
# is there a better way to do this?
|
224
|
+
computed_to = self.to.dup
|
225
|
+
computed_to.gsub!("$&",match(path).to_s)
|
226
|
+
@matches = []
|
227
|
+
(match(path).size - 1).downto(1) do |num|
|
228
|
+
m = match(path)[num].to_s
|
229
|
+
@matches << m
|
230
|
+
computed_to.gsub!("$#{num}", m)
|
231
|
+
end
|
232
|
+
return computed_to
|
233
|
+
end
|
234
|
+
|
235
|
+
# Construct the URL (without domain) from PATH_INFO and QUERY_STRING
|
236
|
+
def build_path_from_env(env)
|
237
|
+
path = env['PATH_INFO'] || ''
|
238
|
+
path += "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].nil? || env['QUERY_STRING'].empty?
|
239
|
+
path
|
240
|
+
end
|
241
|
+
|
242
|
+
def redirect_message(location)
|
243
|
+
%Q(Redirecting to <a href="#{location}">#{location}</a>)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rack-rewrite-matches'
|
3
|
+
s.version = File.read('VERSION')
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Travis Jeffery", "John Trupiano"]
|
7
|
+
s.date = Date.today.to_s
|
8
|
+
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.}
|
9
|
+
s.email = %q{travisjeffery@gmail.com}
|
10
|
+
s.extra_rdoc_files = [
|
11
|
+
"LICENSE",
|
12
|
+
"History.rdoc",
|
13
|
+
]
|
14
|
+
s.files = [
|
15
|
+
"History.rdoc",
|
16
|
+
"LICENSE",
|
17
|
+
"README.markdown",
|
18
|
+
"Rakefile",
|
19
|
+
"VERSION",
|
20
|
+
"Gemfile",
|
21
|
+
"lib/rack-rewrite.rb",
|
22
|
+
"lib/rack/rewrite.rb",
|
23
|
+
"lib/rack/rewrite/rule.rb",
|
24
|
+
"lib/rack/rewrite/version.rb",
|
25
|
+
"rack-rewrite.gemspec",
|
26
|
+
"test/geminstaller.yml",
|
27
|
+
"test/rack-rewrite_test.rb",
|
28
|
+
"test/rule_test.rb",
|
29
|
+
"test/test_helper.rb"
|
30
|
+
]
|
31
|
+
s.homepage = %q{http://github.com/jtrupiano/rack-rewrite}
|
32
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
s.rubyforge_project = %q{johntrupiano}
|
35
|
+
s.rubygems_version = %q{1.3.7}
|
36
|
+
s.summary = %q{A rack middleware for enforcing rewrite rules}
|
37
|
+
s.test_files = [
|
38
|
+
"test/rack-rewrite_test.rb",
|
39
|
+
"test/geminstaller.yml",
|
40
|
+
"test/rack-rewrite_test.rb",
|
41
|
+
"test/rule_test.rb",
|
42
|
+
"test/test_helper.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
s.add_development_dependency 'bundler'
|
46
|
+
s.add_development_dependency 'shoulda', '~> 2.10.2'
|
47
|
+
s.add_development_dependency 'mocha', '~> 0.9.7'
|
48
|
+
s.add_development_dependency 'rack'
|
49
|
+
|
50
|
+
if s.respond_to? :specification_version then
|
51
|
+
s.specification_version = 3
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,136 @@
|
|
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
|