rack-reverse-proxy 0.9.1 → 0.10.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,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
@@ -0,0 +1,4 @@
1
+ #:nodoc:
2
+ module RackReverseProxy
3
+ VERSION = "0.10.0".freeze
4
+ end