rack-rewrite-matches 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|