forked from jaswope/rack-reverse-proxy
/
reverse_proxy.rb
187 lines (156 loc) · 5.36 KB
/
reverse_proxy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
require 'net/http'
require 'net/https'
module Rack
class ReverseProxy
def initialize(app = nil, &b)
@app = app || lambda {|env| [404, [], []] }
@matchers = []
@global_options = {:preserve_host => true, :x_forwarded_host => true, :matching => :all, :verify_ssl => true, :debug => false}
instance_eval &b if block_given?
end
def call(env)
rackreq = Rack::Request.new(env)
matcher = get_matcher rackreq.fullpath
return @app.call(env) if matcher.nil?
uri = matcher.get_uri(rackreq.fullpath,env)
all_opts = @global_options.dup.merge(matcher.options)
headers = Rack::Utils::HeaderHash.new
env.each { |key, value|
if key =~ /HTTP_(.*)/
header = $1.gsub('_', '-')
headers[header] = value
end
}
headers['HOST'] = uri.host if all_opts[:preserve_host]
headers['X-Forwarded-Host'] = rackreq.host if all_opts[:x_forwarded_host]
puts "Proxying #{rackreq.url} => #{uri} (Headers: #{headers.inspect})" if all_opts[:debug]
if headers_opt = all_opts[:headers]
if headers_opt.is_a?(Proc)
headers = headers_opt.call(headers)
elsif headers_opt.is_a?(Hash)
headers.merge!(headers_opt)
else
$stderr.puts "Warning: :headers option provided with unsupported type '#{headers_opt.class}'. Expected a Hash or Proc"
end
end
session = Net::HTTP.new(uri.host, uri.port)
session.read_timeout=all_opts[:timeout] if all_opts[:timeout]
session.use_ssl = (uri.scheme == 'https')
if uri.scheme == 'https' && all_opts[:verify_ssl]
session.verify_mode = OpenSSL::SSL::VERIFY_PEER
else
# DO NOT DO THIS IN PRODUCTION !!!
session.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
session.start { |http|
m = rackreq.request_method
case m
when "GET", "HEAD", "DELETE", "OPTIONS", "TRACE"
req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
req.basic_auth all_opts[:username], all_opts[:password] if all_opts[:username] and all_opts[:password]
when "PUT", "POST"
req = Net::HTTP.const_get(m.capitalize).new(uri.request_uri, headers)
req.basic_auth all_opts[:username], all_opts[:password] if all_opts[:username] and all_opts[:password]
if rackreq.body.respond_to?(:read) && rackreq.body.respond_to?(:rewind)
body = rackreq.body.read
req.content_length = body.size
rackreq.body.rewind
else
req.content_length = rackreq.body.size
end
req.content_type = rackreq.content_type unless rackreq.content_type.nil?
req.body_stream = rackreq.body
else
raise "method not supported: #{m}"
end
body = ''
res = http.request(req) do |res|
res.read_body do |segment|
body << segment
end
end
[res.code, create_response_headers(res), [body]]
}
end
private
def get_matcher path
matches = @matchers.select do |matcher|
matcher.match?(path)
end
if matches.length < 1
nil
elsif matches.length > 1 && @global_options[:matching] != :first
raise AmbiguousProxyMatch.new(path, matches)
else
matches.first
end
end
def create_response_headers http_response
response_headers = Rack::Utils::HeaderHash.new(http_response.to_hash)
# handled by Rack
response_headers.delete('status')
# TODO: figure out how to handle chunked responses
response_headers.delete('transfer-encoding')
# TODO: Verify Content Length, and required Rack headers
response_headers
end
def reverse_proxy_options(options)
@global_options=options
end
def reverse_proxy matcher, url, opts={}
raise GenericProxyURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
@matchers << ReverseProxyMatcher.new(matcher,url,opts)
end
end
class GenericProxyURI < Exception
attr_reader :url
def intialize(url)
@url = url
end
def to_s
%Q(Your URL "#{@url}" is too generic. Did you mean "http://#{@url}"?)
end
end
class AmbiguousProxyMatch < Exception
attr_reader :path, :matches
def initialize(path, matches)
@path = path
@matches = matches
end
def to_s
%Q(Path "#{path}" matched multiple endpoints: #{formatted_matches})
end
private
def formatted_matches
matches.map {|matcher| matcher.to_s}.join(', ')
end
end
class ReverseProxyMatcher
def initialize(matching,url,options)
@matching=matching
@url=url
@options=options
@matching_regexp= matching.kind_of?(Regexp) ? matching : /^#{matching.to_s}/
end
attr_reader :matching,:matching_regexp,:url,:options
def match?(path)
match_path(path) ? true : false
end
def get_uri(path,env)
_url=(url.respond_to?(:call) ? url.call(env) : url.clone)
if _url =~/\$\d/
match_path(path).to_a.each_with_index { |m, i| _url.gsub!("$#{i.to_s}", m) }
URI(_url)
else
URI.join(_url, path)
end
end
def to_s
%Q("#{matching.to_s}" => "#{url}")
end
private
def match_path(path)
path.match(matching_regexp)
end
end
end