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.
@@ -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