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.
@@ -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,4 @@
1
+ #:nodoc:
2
+ module RackReverseProxy
3
+ VERSION = "1.0.0".freeze
4
+ end
@@ -0,0 +1,8 @@
1
+ require "rack_reverse_proxy/version"
2
+ require "rack_reverse_proxy/errors"
3
+ require "rack_reverse_proxy/rule"
4
+ require "rack_reverse_proxy/middleware"
5
+
6
+ # A Reverse Proxy for Rack
7
+ module RackReverseProxy
8
+ 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
data/script/rubocop ADDED
@@ -0,0 +1,5 @@
1
+ #/usr/bin/env bash
2
+
3
+ if ruby -e 'exit(1) unless RUBY_VERSION.to_f >= 2.0'; then
4
+ bundle exec rubocop
5
+ fi