rack-reverse-proxy 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.document +5 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +20 -0
- data/README.md +30 -4
- data/Rakefile +10 -0
- data/lib/rack/reverse_proxy.rb +3 -128
- data/lib/rack_reverse_proxy.rb +8 -0
- data/lib/rack_reverse_proxy/errors.rb +36 -0
- data/lib/rack_reverse_proxy/middleware.rb +40 -0
- data/lib/rack_reverse_proxy/response_builder.rb +60 -0
- data/lib/rack_reverse_proxy/roundtrip.rb +263 -0
- data/lib/rack_reverse_proxy/rule.rb +182 -0
- data/lib/rack_reverse_proxy/version.rb +4 -0
- data/rack-reverse-proxy.gemspec +42 -0
- data/script/rubocop +5 -0
- data/spec/rack/reverse_proxy_spec.rb +586 -0
- data/spec/rack_reverse_proxy/response_builder_spec.rb +37 -0
- data/spec/spec_helper.rb +106 -0
- data/spec/support/http_streaming_response_patch.rb +32 -0
- metadata +58 -42
- data/lib/rack/exception.rb +0 -31
- data/lib/rack/reverse_proxy/http_streaming_response.rb +0 -7
- data/lib/rack/reverse_proxy_matcher.rb +0 -53
@@ -0,0 +1,263 @@
|
|
1
|
+
require "rack_reverse_proxy/response_builder"
|
2
|
+
|
3
|
+
module RackReverseProxy
|
4
|
+
# FIXME: Enable them and fix issues during refactoring
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
6
|
+
|
7
|
+
# RoundTrip represents one request-response made by rack-reverse-proxy
|
8
|
+
# middleware.
|
9
|
+
class RoundTrip
|
10
|
+
def initialize(app, env, global_options, rules, response_builder_klass = ResponseBuilder)
|
11
|
+
@app = app
|
12
|
+
@env = env
|
13
|
+
@global_options = global_options
|
14
|
+
@rules = rules
|
15
|
+
@response_builder_klass = response_builder_klass
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
return app.call(env) if rule.nil?
|
20
|
+
return proxy_with_newrelic if new_relic?
|
21
|
+
proxy
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :app, :env, :global_options, :rules, :response_builder_klass
|
27
|
+
|
28
|
+
def new_relic?
|
29
|
+
global_options[:newrelic_instrumentation]
|
30
|
+
end
|
31
|
+
|
32
|
+
def proxy_with_newrelic
|
33
|
+
perform_action_with_newrelic_trace(:name => action_name, :request => source_request) do
|
34
|
+
proxy
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def action_name
|
39
|
+
"#{action_path}/#{source_request.request_method}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def action_path
|
43
|
+
# Rack::ReverseProxy/foo/bar#GET
|
44
|
+
source_request.path.gsub(%r{/\d+}, "/:id").gsub(%r{^/}, "")
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri
|
48
|
+
return @_uri if defined?(@_uri)
|
49
|
+
@_uri = rule.get_uri(path, env, headers, source_request)
|
50
|
+
end
|
51
|
+
|
52
|
+
def options
|
53
|
+
@_options ||= global_options.dup.merge(rule.options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def https_redirect
|
57
|
+
rewrite_uri(uri, source_request)
|
58
|
+
uri.scheme = "https"
|
59
|
+
[301, { "Location" => uri.to_s }, ""]
|
60
|
+
end
|
61
|
+
|
62
|
+
def need_https_redirect?
|
63
|
+
options[:force_ssl] &&
|
64
|
+
options[:replace_response_host] &&
|
65
|
+
source_request.scheme == "http"
|
66
|
+
end
|
67
|
+
|
68
|
+
def target_request
|
69
|
+
@_target_request ||= build_target_request
|
70
|
+
end
|
71
|
+
|
72
|
+
def target_request_headers
|
73
|
+
@_target_request_headers ||= headers
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_target_request
|
77
|
+
Net::HTTP.const_get(
|
78
|
+
source_request.request_method.capitalize
|
79
|
+
).new(uri.request_uri)
|
80
|
+
end
|
81
|
+
|
82
|
+
def preserve_host
|
83
|
+
return unless options[:preserve_host]
|
84
|
+
target_request_headers["HOST"] = host_header
|
85
|
+
end
|
86
|
+
|
87
|
+
def host_header
|
88
|
+
return uri.host if uri.port == uri.default_port
|
89
|
+
"#{uri.host}:#{uri.port}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_forwarded_host
|
93
|
+
return unless options[:x_forwarded_host]
|
94
|
+
target_request_headers["X-Forwarded-Host"] = source_request.host
|
95
|
+
target_request_headers["X-Forwarded-Port"] = source_request.port.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize_http_header
|
99
|
+
target_request.initialize_http_header(target_request_headers)
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_basic_auth
|
103
|
+
return unless need_basic_auth?
|
104
|
+
target_request.basic_auth(options[:username], options[:password])
|
105
|
+
end
|
106
|
+
|
107
|
+
def need_basic_auth?
|
108
|
+
options[:username] && options[:password]
|
109
|
+
end
|
110
|
+
|
111
|
+
def setup_body
|
112
|
+
return unless can_have_body? && body?
|
113
|
+
source_request.body.rewind
|
114
|
+
target_request.body_stream = source_request.body
|
115
|
+
end
|
116
|
+
|
117
|
+
def can_have_body?
|
118
|
+
target_request.request_body_permitted?
|
119
|
+
end
|
120
|
+
|
121
|
+
def body?
|
122
|
+
source_request.body
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_content_length
|
126
|
+
target_request.content_length = source_request.content_length || 0
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_content_type
|
130
|
+
return unless content_type?
|
131
|
+
target_request.content_type = source_request.content_type
|
132
|
+
end
|
133
|
+
|
134
|
+
def content_type?
|
135
|
+
source_request.content_type
|
136
|
+
end
|
137
|
+
|
138
|
+
def target_response
|
139
|
+
@_target_response ||= response_builder_klass.new(
|
140
|
+
target_request,
|
141
|
+
uri,
|
142
|
+
options
|
143
|
+
).fetch
|
144
|
+
end
|
145
|
+
|
146
|
+
def response_headers
|
147
|
+
@_response_headers ||= build_response_headers
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_response_headers
|
151
|
+
["Transfer-Encoding", "Status"].inject(rack_response_headers) do |acc, header|
|
152
|
+
acc.delete(header)
|
153
|
+
acc
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def rack_response_headers
|
158
|
+
Rack::Utils::HeaderHash.new(
|
159
|
+
Rack::Proxy.normalize_headers(
|
160
|
+
format_headers(target_response.headers)
|
161
|
+
)
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def replace_location_header
|
166
|
+
return unless need_replace_location?
|
167
|
+
rewrite_uri(response_location, source_request)
|
168
|
+
response_headers["Location"] = response_location.to_s
|
169
|
+
end
|
170
|
+
|
171
|
+
def response_location
|
172
|
+
@_response_location ||= URI(response_headers["Location"])
|
173
|
+
end
|
174
|
+
|
175
|
+
def need_replace_location?
|
176
|
+
response_headers["Location"] && options[:replace_response_host]
|
177
|
+
end
|
178
|
+
|
179
|
+
def setup_request
|
180
|
+
preserve_host
|
181
|
+
set_forwarded_host
|
182
|
+
initialize_http_header
|
183
|
+
set_basic_auth
|
184
|
+
setup_body
|
185
|
+
set_content_length
|
186
|
+
set_content_type
|
187
|
+
end
|
188
|
+
|
189
|
+
def setup_response_headers
|
190
|
+
replace_location_header
|
191
|
+
end
|
192
|
+
|
193
|
+
def rack_response
|
194
|
+
[target_response.status, response_headers, target_response.body]
|
195
|
+
end
|
196
|
+
|
197
|
+
def proxy
|
198
|
+
return app.call(env) if uri.nil?
|
199
|
+
return https_redirect if need_https_redirect?
|
200
|
+
|
201
|
+
setup_request
|
202
|
+
setup_response_headers
|
203
|
+
|
204
|
+
rack_response
|
205
|
+
end
|
206
|
+
|
207
|
+
def format_headers(headers)
|
208
|
+
headers.inject({}) do |acc, (key, val)|
|
209
|
+
formated_key = key.split("-").map(&:capitalize).join("-")
|
210
|
+
acc[formated_key] = Array(val)
|
211
|
+
acc
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def request_default_port?(req)
|
216
|
+
[["http", 80], ["https", 443]].include?([req.scheme, req.port])
|
217
|
+
end
|
218
|
+
|
219
|
+
def rewrite_uri(uri, original_req)
|
220
|
+
uri.scheme = original_req.scheme
|
221
|
+
uri.host = original_req.host
|
222
|
+
uri.port = original_req.port unless request_default_port?(original_req)
|
223
|
+
end
|
224
|
+
|
225
|
+
def source_request
|
226
|
+
@_source_request ||= Rack::Request.new(env)
|
227
|
+
end
|
228
|
+
|
229
|
+
def rule
|
230
|
+
return @_rule if defined?(@_rule)
|
231
|
+
@_rule = find_rule
|
232
|
+
end
|
233
|
+
|
234
|
+
def find_rule
|
235
|
+
return if matches.length < 1
|
236
|
+
non_ambiguous_match
|
237
|
+
matches.first
|
238
|
+
end
|
239
|
+
|
240
|
+
def path
|
241
|
+
@_path ||= source_request.fullpath
|
242
|
+
end
|
243
|
+
|
244
|
+
def headers
|
245
|
+
Rack::Proxy.extract_http_request_headers(source_request.env)
|
246
|
+
end
|
247
|
+
|
248
|
+
def matches
|
249
|
+
@_matches ||= rules.select do |rule|
|
250
|
+
rule.proxy?(path, headers, source_request)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def non_ambiguous_match
|
255
|
+
return unless ambiguous_match?
|
256
|
+
raise Errors::AmbiguousMatch.new(path, matches)
|
257
|
+
end
|
258
|
+
|
259
|
+
def ambiguous_match?
|
260
|
+
matches.length > 1 && global_options[:matching] != :first
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module RackReverseProxy
|
2
|
+
# Rule understands which urls need to be proxied
|
3
|
+
class Rule
|
4
|
+
# FIXME: It needs to be hidden
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def initialize(spec, url = nil, options = {})
|
8
|
+
@has_custom_url = url.nil?
|
9
|
+
@url = url
|
10
|
+
@options = options
|
11
|
+
@spec = build_matcher(spec)
|
12
|
+
end
|
13
|
+
|
14
|
+
def proxy?(path, *args)
|
15
|
+
matches(path, *args).any?
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_uri(path, env, *args)
|
19
|
+
Candidate.new(
|
20
|
+
self,
|
21
|
+
has_custom_url,
|
22
|
+
path,
|
23
|
+
env,
|
24
|
+
matches(path, *args)
|
25
|
+
).build_uri
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
%("#{spec}" => "#{url}")
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :spec, :url, :has_custom_url
|
35
|
+
|
36
|
+
def matches(path, *args)
|
37
|
+
Matches.new(
|
38
|
+
spec,
|
39
|
+
url,
|
40
|
+
path,
|
41
|
+
options[:accept_headers],
|
42
|
+
has_custom_url,
|
43
|
+
*args
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_matcher(spec)
|
48
|
+
return /^#{spec}/ if spec.is_a?(String)
|
49
|
+
return spec if spec.respond_to?(:match)
|
50
|
+
return spec if spec.respond_to?(:call)
|
51
|
+
raise ArgumentError, "Invalid Rule for reverse_proxy"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Candidate represents a request being matched
|
55
|
+
class Candidate
|
56
|
+
def initialize(rule, has_custom_url, path, env, matches)
|
57
|
+
@rule = rule
|
58
|
+
@env = env
|
59
|
+
@path = path
|
60
|
+
@has_custom_url = has_custom_url
|
61
|
+
@matches = matches
|
62
|
+
|
63
|
+
@url = evaluate(matches.custom_url)
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_uri
|
67
|
+
return nil unless url
|
68
|
+
raw_uri
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
attr_reader :rule, :url, :has_custom_url, :path, :env, :matches
|
74
|
+
|
75
|
+
def raw_uri
|
76
|
+
return substitute_matches if with_substitutions?
|
77
|
+
return just_uri if has_custom_url
|
78
|
+
uri_with_path
|
79
|
+
end
|
80
|
+
|
81
|
+
def just_uri
|
82
|
+
URI.parse(url)
|
83
|
+
end
|
84
|
+
|
85
|
+
def uri_with_path
|
86
|
+
URI.join(url, path)
|
87
|
+
end
|
88
|
+
|
89
|
+
def evaluate(url)
|
90
|
+
return unless url
|
91
|
+
return url.call(env) if lazy?(url)
|
92
|
+
url.clone
|
93
|
+
end
|
94
|
+
|
95
|
+
def lazy?(url)
|
96
|
+
url.respond_to?(:call)
|
97
|
+
end
|
98
|
+
|
99
|
+
def with_substitutions?
|
100
|
+
url =~ /\$\d/
|
101
|
+
end
|
102
|
+
|
103
|
+
def substitute_matches
|
104
|
+
URI(matches.substitute(url))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Matches represents collection of matched objects for Rule
|
109
|
+
class Matches
|
110
|
+
# rubocop:disable Metrics/ParameterLists
|
111
|
+
|
112
|
+
# FIXME: eliminate :url, :accept_headers, :has_custom_url
|
113
|
+
def initialize(spec, url, path, accept_headers, has_custom_url, headers, rackreq, *_)
|
114
|
+
@spec = spec
|
115
|
+
@url = url
|
116
|
+
@path = path
|
117
|
+
@has_custom_url = has_custom_url
|
118
|
+
@rackreq = rackreq
|
119
|
+
|
120
|
+
@headers = headers if accept_headers
|
121
|
+
@spec_arity = spec.method(spec_match_method_name).arity
|
122
|
+
end
|
123
|
+
|
124
|
+
def any?
|
125
|
+
found.any?
|
126
|
+
end
|
127
|
+
|
128
|
+
def custom_url
|
129
|
+
return url unless has_custom_url
|
130
|
+
found.map do |match|
|
131
|
+
match.url(path)
|
132
|
+
end.first
|
133
|
+
end
|
134
|
+
|
135
|
+
def substitute(url)
|
136
|
+
found.each_with_index.inject(url) do |acc, (match, i)|
|
137
|
+
acc.gsub("$#{i}", match)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
attr_reader :spec, :url, :path, :headers, :rackreq, :spec_arity, :has_custom_url
|
144
|
+
|
145
|
+
def found
|
146
|
+
@_found ||= find_matches
|
147
|
+
end
|
148
|
+
|
149
|
+
def find_matches
|
150
|
+
Array(
|
151
|
+
spec.send(spec_match_method_name, *spec_params)
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
def spec_params
|
156
|
+
@_spec_params ||= _spec_params
|
157
|
+
end
|
158
|
+
|
159
|
+
def _spec_params
|
160
|
+
[
|
161
|
+
path,
|
162
|
+
headers,
|
163
|
+
rackreq
|
164
|
+
][0...spec_param_count]
|
165
|
+
end
|
166
|
+
|
167
|
+
def spec_param_count
|
168
|
+
@_spec_param_count ||= _spec_param_count
|
169
|
+
end
|
170
|
+
|
171
|
+
def _spec_param_count
|
172
|
+
return 1 if spec_arity == -1
|
173
|
+
spec_arity
|
174
|
+
end
|
175
|
+
|
176
|
+
def spec_match_method_name
|
177
|
+
return :match if spec.respond_to?(:match)
|
178
|
+
:call
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|