Skip to content

Commit 8cd6be4

Browse files
committed
Merge pull request #4 from experteer/master
refactoring + some new features
2 parents 0c1d08f + f955093 commit 8cd6be4

File tree

3 files changed

+88
-33
lines changed

3 files changed

+88
-33
lines changed

README.rdoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@ Below is an example for configuring the middleware:
1717

1818
use Rack::ReverseProxy do
1919
# Forward the path /test* to http://example.com/test*
20+
reverse_proxy_options :preserve_host => true
21+
2022
reverse_proxy '/test', 'http://example.com/'
2123

2224
# Forward the path /foo/* to http://example.com/bar/*
23-
reverse_proxy /^\/foo(\/.*)$/, 'http://example.com/bar$1'
25+
reverse_proxy /^\/foo(\/.*)$/, 'http://example.com/bar$1', :username => 'name', :password => 'basic_auth_secret'
2426
end
2527

2628
app = proc do |env|
2729
[ 200, {'Content-Type' => 'text/plain'}, "b" ]
2830
end
2931
run app
3032

33+
revers_proxy_options set global options for all reverse proxies. Available options are:
34+
* :preserve_host
35+
* :username username for basic auth
36+
* :password password for basic auth
37+
* :matching is a global only option, if set to :first the first matched url will be requested (no ambigous error). Default: :all.
38+
* :timeout seconds to timout the requests
3139
== Note on Patches/Pull Requests
3240

3341
* Fork the project.

lib/rack/reverse_proxy.rb

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,40 @@ module Rack
55
class ReverseProxy
66
def initialize(app = nil, &b)
77
@app = app || lambda { [404, [], []] }
8-
@paths = {}
9-
@opts = {:preserve_host => false}
8+
@matchers = []
9+
@global_options = {:preserve_host => false,:matching => :all}
1010
instance_eval &b if block_given?
1111
end
1212

1313
def call(env)
1414
rackreq = Rack::Request.new(env)
15-
matcher, url = get_matcher_and_url rackreq.fullpath
15+
matcher = get_matcher rackreq.fullpath
1616
return @app.call(env) if matcher.nil?
1717

18-
uri = get_uri(url, matcher, rackreq.fullpath)
18+
uri = matcher.get_uri(rackreq.fullpath,env)
19+
all_opts = @global_options.dup.merge(matcher.options)
1920
headers = Rack::Utils::HeaderHash.new
2021
env.each { |key, value|
2122
if key =~ /HTTP_(.*)/
2223
headers[$1] = value
2324
end
2425
}
25-
headers['HOST'] = uri.host if @opts[:preserve_host]
26+
headers['HOST'] = uri.host if all_opts[:preserve_host]
2627

2728
session = Net::HTTP.new(uri.host, uri.port)
2829
session.use_ssl = (uri.scheme == 'https')
2930
session.verify_mode = OpenSSL::SSL::VERIFY_NONE
31+
session.read_timeout=all_opts[:timeout] if all_opts[:timeout]
3032
session.start { |http|
3133
m = rackreq.request_method
3234
case m
3335
when "GET", "HEAD", "DELETE", "OPTIONS", "TRACE"
3436
req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
35-
req.basic_auth @opts[:username], @opts[:password] if @opts[:username] and @opts[:password]
37+
req.basic_auth all_opts[:username], all_opts[:password] if all_opts[:username] and all_opts[:password]
3638
when "PUT", "POST"
3739
req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
38-
req.basic_auth @opts[:username], @opts[:password] if @opts[:username] and @opts[:password]
39-
req.content_length = rackreq.body.length
40+
req.basic_auth all_opts[:username], all_opts[:password] if all_opts[:username] and all_opts[:password]
41+
req.content_length = rackreq.body.size
4042
req.body_stream = rackreq.body
4143
else
4244
raise "method not supported: #{m}"
@@ -55,17 +57,17 @@ def call(env)
5557

5658
private
5759

58-
def get_matcher_and_url path
59-
matches = @paths.select do |matcher, url|
60-
match_path(path, matcher)
60+
def get_matcher path
61+
matches = @matchers.select do |matcher|
62+
matcher.match?(path)
6163
end
6264

6365
if matches.length < 1
6466
nil
65-
elsif matches.length > 1
67+
elsif matches.length > 1 && @global_options[:matching] != :first
6668
raise AmbiguousProxyMatch.new(path, matches)
6769
else
68-
matches.first.map{|a| a.dup}
70+
matches.first
6971
end
7072
end
7173

@@ -79,27 +81,14 @@ def create_response_headers http_response
7981
response_headers
8082
end
8183

82-
def match_path(path, matcher)
83-
if matcher.is_a?(Regexp)
84-
path.match(matcher)
85-
else
86-
path.match(/^#{matcher.to_s}/)
87-
end
88-
end
8984

90-
def get_uri(url, matcher, path)
91-
if url =~/\$\d/
92-
match_path(path, matcher).to_a.each_with_index { |m, i| url.gsub!("$#{i.to_s}", m) }
93-
URI(url)
94-
else
95-
URI.join(url, path)
96-
end
85+
def reverse_proxy_options(options)
86+
@global_options=options
9787
end
9888

9989
def reverse_proxy matcher, url, opts={}
100-
raise GenericProxyURI.new(url) if matcher.is_a?(String) && URI(url).class == URI::Generic
101-
@paths.merge!(matcher => url)
102-
@opts.merge!(opts)
90+
raise GenericProxyURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
91+
@matchers << ReverseProxyMatcher.new(matcher,url,opts)
10392
end
10493
end
10594

@@ -129,8 +118,41 @@ def to_s
129118
private
130119

131120
def formatted_matches
132-
matches.map {|m| %Q("#{m[0].to_s}" => "#{m[1]}")}.join(', ')
121+
matches.map {|matcher| matcher.to_s}.join(', ')
133122
end
134123
end
135124

125+
class ReverseProxyMatcher
126+
def initialize(matching,url,options)
127+
@matching=matching
128+
@url=url
129+
@options=options
130+
@matching_regexp= matching.kind_of?(Regexp) ? matching : /^#{matching.to_s}/
131+
end
132+
133+
attr_reader :matching,:matching_regexp,:url,:options
134+
135+
def match?(path)
136+
match_path(path) ? true : false
137+
end
138+
139+
def get_uri(path,env)
140+
_url=(url.respond_to?(:call) ? url.call(env) : url)
141+
if _url =~/\$\d/
142+
match_path(path).to_a.each_with_index { |m, i| _url.gsub!("$#{i.to_s}", m) }
143+
URI(_url)
144+
else
145+
URI.join(_url, path)
146+
end
147+
end
148+
def to_s
149+
%Q("#{matching.to_s}" => "#{url}")
150+
end
151+
private
152+
def match_path(path)
153+
path.match(matching_regexp)
154+
end
155+
156+
157+
end
136158
end

spec/rack/reverse_proxy_spec.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def dummy_app
1616
def app
1717
Rack::ReverseProxy.new(dummy_app) do
1818
reverse_proxy '/test', 'http://example.com/', {:preserve_host => true}
19+
reverse_proxy '/2test', lambda{'http://example.com/'}
20+
1921
end
2022
end
2123

@@ -31,6 +33,12 @@ def app
3133
last_response.body.should == "Proxied App"
3234
end
3335

36+
it "should proxy requests to a lambda url when a pattern is matched" do
37+
stub_request(:get, 'http://example.com/2test').to_return({:body => "Proxied App2"})
38+
get '/2test'
39+
last_response.body.should == "Proxied App2"
40+
end
41+
3442
it "the response header should never contain Status" do
3543
stub_request(:any, 'example.com/test/stuff').to_return(:headers => {'Status' => '200 OK'})
3644
get '/test/stuff'
@@ -77,9 +85,10 @@ def app
7785
end
7886
end
7987

80-
describe "with ambiguous routes" do
88+
describe "with ambiguous routes and all matching" do
8189
def app
8290
Rack::ReverseProxy.new(dummy_app) do
91+
reverse_proxy_options :matching => :all
8392
reverse_proxy '/test', 'http://example.com/'
8493
reverse_proxy /^\/test/, 'http://example.com/'
8594
end
@@ -90,6 +99,22 @@ def app
9099
end
91100
end
92101

102+
describe "with ambiguous routes and first matching" do
103+
def app
104+
Rack::ReverseProxy.new(dummy_app) do
105+
reverse_proxy_options :matching => :first
106+
reverse_proxy '/test', 'http://example1.com/'
107+
reverse_proxy /^\/test/, 'http://example2.com/'
108+
end
109+
end
110+
111+
it "should throw an exception" do
112+
stub_request(:get, 'http://example1.com/test').to_return({:body => "Proxied App"})
113+
get '/test'
114+
last_response.body.should == "Proxied App"
115+
end
116+
end
117+
93118
describe "with a route as a regular expression" do
94119
def app
95120
Rack::ReverseProxy.new(dummy_app) do

0 commit comments

Comments
 (0)