rack-rewrite-matches 1.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.3
@@ -0,0 +1 @@
1
+ require 'rack/rewrite'
@@ -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,5 @@
1
+ module Rack
2
+ class Rewrite
3
+ VERSION = File.read File.join(File.expand_path("..", __FILE__), "..", "..", "..", "VERSION")
4
+ end
5
+ 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,9 @@
1
+ gems:
2
+ - name: shoulda
3
+ version: '= 2.10.3'
4
+ - name: mocha
5
+ version: '= 0.9.8'
6
+ - name: rack
7
+ version: '= 1.1.0'
8
+ # - name: oniguruma
9
+ # version: '= 1.1.0'
@@ -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