rack-reverse-proxy-pact 1.0.0
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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.github/workflows/test.yml +22 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +41 -0
- data/Gemfile +20 -0
- data/LICENSE +20 -0
- data/README.md +129 -0
- data/Rakefile +10 -0
- data/lib/rack/reverse_proxy.rb +6 -0
- data/lib/rack_reverse_proxy/errors.rb +36 -0
- data/lib/rack_reverse_proxy/middleware.rb +47 -0
- data/lib/rack_reverse_proxy/response_builder.rb +60 -0
- data/lib/rack_reverse_proxy/roundtrip.rb +286 -0
- data/lib/rack_reverse_proxy/rule.rb +206 -0
- data/lib/rack_reverse_proxy/version.rb +4 -0
- data/lib/rack_reverse_proxy.rb +8 -0
- data/rack-reverse-proxy.gemspec +49 -0
- data/script/rubocop +5 -0
- data/spec/rack/reverse_proxy_spec.rb +768 -0
- data/spec/rack_reverse_proxy/response_builder_spec.rb +37 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/http_streaming_response_patch.rb +32 -0
- metadata +168 -0
@@ -0,0 +1,286 @@
|
|
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 strip_headers
|
88
|
+
return unless options[:stripped_headers]
|
89
|
+
options[:stripped_headers].each do |header|
|
90
|
+
target_request_headers.delete(header)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def host_header
|
95
|
+
return uri.host if uri.port == uri.default_port
|
96
|
+
"#{uri.host}:#{uri.port}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_forwarded_headers
|
100
|
+
return unless options[:x_forwarded_headers]
|
101
|
+
target_request_headers["X-Forwarded-Host"] = source_request.host
|
102
|
+
target_request_headers["X-Forwarded-Port"] = source_request.port.to_s
|
103
|
+
target_request_headers["X-Forwarded-Proto"] = source_request.scheme
|
104
|
+
end
|
105
|
+
|
106
|
+
def initialize_http_header
|
107
|
+
target_request.initialize_http_header(target_request_headers)
|
108
|
+
end
|
109
|
+
|
110
|
+
def set_basic_auth
|
111
|
+
return unless need_basic_auth?
|
112
|
+
target_request.basic_auth(options[:username], options[:password])
|
113
|
+
end
|
114
|
+
|
115
|
+
def need_basic_auth?
|
116
|
+
options[:username] && options[:password]
|
117
|
+
end
|
118
|
+
|
119
|
+
def setup_body
|
120
|
+
return unless can_have_body? && body?
|
121
|
+
source_request.body.rewind
|
122
|
+
target_request.body_stream = source_request.body
|
123
|
+
end
|
124
|
+
|
125
|
+
def can_have_body?
|
126
|
+
target_request.request_body_permitted?
|
127
|
+
end
|
128
|
+
|
129
|
+
def body?
|
130
|
+
source_request.body
|
131
|
+
end
|
132
|
+
|
133
|
+
def set_content_length
|
134
|
+
target_request.content_length = source_request.content_length || 0
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_content_type
|
138
|
+
return unless content_type?
|
139
|
+
target_request.content_type = source_request.content_type
|
140
|
+
end
|
141
|
+
|
142
|
+
def content_type?
|
143
|
+
source_request.content_type
|
144
|
+
end
|
145
|
+
|
146
|
+
def target_response
|
147
|
+
@_target_response ||= response_builder_klass.new(
|
148
|
+
target_request,
|
149
|
+
uri,
|
150
|
+
options
|
151
|
+
).fetch
|
152
|
+
end
|
153
|
+
|
154
|
+
def response_headers
|
155
|
+
@_response_headers ||= begin
|
156
|
+
headers = build_response_headers
|
157
|
+
headers = headers.transform_keys(&:downcase) unless rack_version_less_than_three
|
158
|
+
headers
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_response_headers
|
163
|
+
["Transfer-Encoding", "Status"].inject(rack_response_headers) do |acc, header|
|
164
|
+
acc.delete(header)
|
165
|
+
acc
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def rack_response_headers
|
170
|
+
headers = Rack::Proxy.normalize_headers(format_headers(target_response.headers))
|
171
|
+
rack_version_less_than_three ? Rack::Utils::HeaderHash.new(headers) : Rack::Headers.new.merge(headers)
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
def replace_location_header
|
176
|
+
return unless need_replace_location?
|
177
|
+
rewrite_uri(response_location, source_request)
|
178
|
+
response_headers[set_rack_version_specific_location_header] = response_location.to_s
|
179
|
+
end
|
180
|
+
|
181
|
+
def response_location
|
182
|
+
@_response_location ||= URI(response_headers[set_rack_version_specific_location_header] || uri)
|
183
|
+
end
|
184
|
+
|
185
|
+
def need_replace_location?
|
186
|
+
response_headers[set_rack_version_specific_location_header] && options[:replace_response_host] && response_location.host
|
187
|
+
end
|
188
|
+
|
189
|
+
def setup_request
|
190
|
+
preserve_host
|
191
|
+
strip_headers
|
192
|
+
set_forwarded_headers
|
193
|
+
initialize_http_header
|
194
|
+
set_basic_auth
|
195
|
+
setup_body
|
196
|
+
set_content_length
|
197
|
+
set_content_type
|
198
|
+
end
|
199
|
+
|
200
|
+
def setup_response_headers
|
201
|
+
replace_location_header
|
202
|
+
end
|
203
|
+
|
204
|
+
def rack_response
|
205
|
+
[target_response.status, response_headers, target_response.body]
|
206
|
+
end
|
207
|
+
|
208
|
+
def proxy
|
209
|
+
return app.call(env) if uri.nil?
|
210
|
+
return https_redirect if need_https_redirect?
|
211
|
+
|
212
|
+
setup_request
|
213
|
+
setup_response_headers
|
214
|
+
|
215
|
+
transform_response(rack_response)
|
216
|
+
end
|
217
|
+
|
218
|
+
def transform_response(response)
|
219
|
+
rule.transform(path, env, response, uri, headers, source_request)
|
220
|
+
end
|
221
|
+
|
222
|
+
def format_headers(headers)
|
223
|
+
headers.inject({}) do |acc, (key, val)|
|
224
|
+
formated_key = key.split("-").map(&:capitalize).join("-")
|
225
|
+
acc[formated_key] = Array(val)
|
226
|
+
acc
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def request_default_port?(req)
|
231
|
+
[["http", 80], ["https", 443]].include?([req.scheme, req.port])
|
232
|
+
end
|
233
|
+
|
234
|
+
def rewrite_uri(uri, original_req)
|
235
|
+
uri.scheme = original_req.scheme
|
236
|
+
uri.host = original_req.host
|
237
|
+
uri.port = original_req.port unless request_default_port?(original_req)
|
238
|
+
end
|
239
|
+
|
240
|
+
def source_request
|
241
|
+
@_source_request ||= Rack::Request.new(env)
|
242
|
+
end
|
243
|
+
|
244
|
+
def rule
|
245
|
+
return @_rule if defined?(@_rule)
|
246
|
+
@_rule = find_rule
|
247
|
+
end
|
248
|
+
|
249
|
+
def find_rule
|
250
|
+
return if matches.empty?
|
251
|
+
non_ambiguous_match
|
252
|
+
matches.first
|
253
|
+
end
|
254
|
+
|
255
|
+
def path
|
256
|
+
@_path ||= source_request.fullpath
|
257
|
+
end
|
258
|
+
|
259
|
+
def headers
|
260
|
+
Rack::Proxy.extract_http_request_headers(source_request.env)
|
261
|
+
end
|
262
|
+
|
263
|
+
def matches
|
264
|
+
@_matches ||= rules.select do |rule|
|
265
|
+
rule.proxy?(path, headers, source_request)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def non_ambiguous_match
|
270
|
+
return unless ambiguous_match?
|
271
|
+
raise Errors::AmbiguousMatch.new(path, matches)
|
272
|
+
end
|
273
|
+
|
274
|
+
def ambiguous_match?
|
275
|
+
matches.length > 1 && global_options[:matching] != :first
|
276
|
+
end
|
277
|
+
|
278
|
+
def rack_version_less_than_three
|
279
|
+
Rack.release.split('.').first.to_i < 3
|
280
|
+
end
|
281
|
+
|
282
|
+
def set_rack_version_specific_location_header
|
283
|
+
rack_version_less_than_three ? 'Location' : 'location'
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,206 @@
|
|
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 transform(path, env, response, request_uri, *args)
|
29
|
+
Candidate.new(
|
30
|
+
self,
|
31
|
+
has_custom_url,
|
32
|
+
path,
|
33
|
+
env,
|
34
|
+
matches(path, *args)
|
35
|
+
).transform(response, request_uri)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
%("#{spec}" => "#{url}")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :spec, :url, :has_custom_url
|
45
|
+
|
46
|
+
def matches(path, *args)
|
47
|
+
Matches.new(
|
48
|
+
spec,
|
49
|
+
url,
|
50
|
+
path,
|
51
|
+
options[:accept_headers],
|
52
|
+
has_custom_url,
|
53
|
+
*args
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_matcher(spec)
|
58
|
+
return /^#{spec}/ if spec.is_a?(String)
|
59
|
+
return spec if spec.respond_to?(:match)
|
60
|
+
return spec if spec.respond_to?(:call)
|
61
|
+
raise ArgumentError, "Invalid Rule for reverse_proxy"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Candidate represents a request being matched
|
65
|
+
class Candidate
|
66
|
+
def initialize(rule, has_custom_url, path, env, matches)
|
67
|
+
@rule = rule
|
68
|
+
@env = env
|
69
|
+
@path = path
|
70
|
+
@has_custom_url = has_custom_url
|
71
|
+
@matches = matches
|
72
|
+
|
73
|
+
@url = evaluate(matches.custom_url)
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_uri
|
77
|
+
return nil unless url
|
78
|
+
raw_uri
|
79
|
+
end
|
80
|
+
|
81
|
+
def transform(response, request_uri)
|
82
|
+
matches.transform(response, request_uri)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
attr_reader :rule, :url, :has_custom_url, :path, :env, :matches
|
88
|
+
|
89
|
+
def raw_uri
|
90
|
+
return substitute_matches if with_substitutions?
|
91
|
+
return just_uri if has_custom_url
|
92
|
+
uri_with_path
|
93
|
+
end
|
94
|
+
|
95
|
+
def just_uri
|
96
|
+
URI.parse(url)
|
97
|
+
end
|
98
|
+
|
99
|
+
def uri_with_path
|
100
|
+
URI.join(url, path)
|
101
|
+
end
|
102
|
+
|
103
|
+
def evaluate(url)
|
104
|
+
return unless url
|
105
|
+
return url.call(env) if lazy?(url)
|
106
|
+
url.clone
|
107
|
+
end
|
108
|
+
|
109
|
+
def lazy?(url)
|
110
|
+
url.respond_to?(:call)
|
111
|
+
end
|
112
|
+
|
113
|
+
def with_substitutions?
|
114
|
+
url =~ /\$\d/
|
115
|
+
end
|
116
|
+
|
117
|
+
def substitute_matches
|
118
|
+
URI(matches.substitute(url))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Matches represents collection of matched objects for Rule
|
123
|
+
class Matches
|
124
|
+
# rubocop:disable Metrics/ParameterLists
|
125
|
+
|
126
|
+
# FIXME: eliminate :url, :accept_headers, :has_custom_url
|
127
|
+
def initialize(spec, url, path, accept_headers, has_custom_url, headers, rackreq, *_)
|
128
|
+
@spec = spec
|
129
|
+
@url = url
|
130
|
+
@path = path
|
131
|
+
@has_custom_url = has_custom_url
|
132
|
+
@rackreq = rackreq
|
133
|
+
|
134
|
+
@headers = headers if accept_headers
|
135
|
+
@spec_arity = spec.method(spec_match_method_name).arity
|
136
|
+
end
|
137
|
+
|
138
|
+
def any?
|
139
|
+
found.any?
|
140
|
+
end
|
141
|
+
|
142
|
+
def custom_url
|
143
|
+
return url unless has_custom_url
|
144
|
+
found.map do |match|
|
145
|
+
match.url(path)
|
146
|
+
end.first
|
147
|
+
end
|
148
|
+
|
149
|
+
def substitute(url)
|
150
|
+
found.each_with_index.inject(url) do |acc, (match, i)|
|
151
|
+
acc.gsub("$#{i}", match)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def transform(response, request_uri)
|
156
|
+
found.inject(response) do |accumulator, match|
|
157
|
+
if match.respond_to?(:transform)
|
158
|
+
match.transform(accumulator, request_uri)
|
159
|
+
else
|
160
|
+
accumulator
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
attr_reader :spec, :url, :path, :headers, :rackreq, :spec_arity, :has_custom_url
|
168
|
+
|
169
|
+
def found
|
170
|
+
@_found ||= find_matches
|
171
|
+
end
|
172
|
+
|
173
|
+
def find_matches
|
174
|
+
Array(
|
175
|
+
spec.send(spec_match_method_name, *spec_params)
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
def spec_params
|
180
|
+
@_spec_params ||= _spec_params
|
181
|
+
end
|
182
|
+
|
183
|
+
def _spec_params
|
184
|
+
[
|
185
|
+
path,
|
186
|
+
headers,
|
187
|
+
rackreq
|
188
|
+
][0...spec_param_count]
|
189
|
+
end
|
190
|
+
|
191
|
+
def spec_param_count
|
192
|
+
@_spec_param_count ||= _spec_param_count
|
193
|
+
end
|
194
|
+
|
195
|
+
def _spec_param_count
|
196
|
+
return 1 if spec_arity == -1
|
197
|
+
spec_arity
|
198
|
+
end
|
199
|
+
|
200
|
+
def spec_match_method_name
|
201
|
+
return :match if spec.respond_to?(:match)
|
202
|
+
:call
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "rack_reverse_proxy/version"
|
4
|
+
|
5
|
+
# rubocop:disable
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rack-reverse-proxy-pact"
|
8
|
+
spec.version = RackReverseProxy::VERSION
|
9
|
+
|
10
|
+
spec.authors = [
|
11
|
+
"Jon Swope",
|
12
|
+
"Ian Ehlert",
|
13
|
+
"Roman Ernst",
|
14
|
+
"Oleksii Fedorov"
|
15
|
+
]
|
16
|
+
|
17
|
+
spec.email = [
|
18
|
+
"jaswope@gmail.com",
|
19
|
+
"ehlertij@gmail.com",
|
20
|
+
"rernst@farbenmeer.net",
|
21
|
+
"waterlink000@gmail.com"
|
22
|
+
]
|
23
|
+
|
24
|
+
spec.summary = "A Simple Reverse Proxy for Rack"
|
25
|
+
spec.description = <<eos
|
26
|
+
A Rack based reverse proxy for basic needs.
|
27
|
+
Useful for testing or in cases where webserver configuration is unavailable.
|
28
|
+
eos
|
29
|
+
|
30
|
+
spec.homepage = "https://github.com/waterlink/rack-reverse-proxy"
|
31
|
+
spec.license = "MIT"
|
32
|
+
|
33
|
+
spec.files = `git ls-files -z`.split("\x0")
|
34
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
35
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
36
|
+
spec.require_paths = ["lib"]
|
37
|
+
|
38
|
+
if ENV['RACK_VERSION'] == '2'
|
39
|
+
spec.add_dependency 'rack', ">= 1.0.0", '< 3.0'
|
40
|
+
else
|
41
|
+
spec.add_dependency 'rack', '>= 3.0', '< 4.0'
|
42
|
+
spec.add_dependency 'rackup', '~> 2.0'
|
43
|
+
end
|
44
|
+
spec.add_dependency "rack-proxy", "~> 0.6", ">= 0.6.1"
|
45
|
+
|
46
|
+
spec.add_development_dependency "bundler", ">= 1.7", "< 3.0"
|
47
|
+
spec.add_development_dependency "rake", ">= 10.3"
|
48
|
+
end
|
49
|
+
# rubocop:enable
|