google-apis-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Google
16
+ module Apis
17
+ module Core
18
+ # Adds to_hash to objects
19
+ module Hashable
20
+ # Convert object to hash representation
21
+ #
22
+ # @return [Hash]
23
+ def to_h
24
+ Hash[instance_variables.map { |k| [k[1..-1].to_sym, Hashable.process_value(instance_variable_get(k))] }]
25
+ end
26
+
27
+ # Recursively serialize an object
28
+ #
29
+ # @param [Object] val
30
+ # @return [Hash]
31
+ def self.process_value(val)
32
+ case val
33
+ when Hash
34
+ Hash[val.map {|k, v| [k.to_sym, Hashable.process_value(v)] }]
35
+ when Array
36
+ val.map{ |v| Hashable.process_value(v) }
37
+ else
38
+ val.respond_to?(:to_h) ? val.to_h : val
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,447 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'addressable/uri'
16
+ require 'addressable/template'
17
+ require 'google/apis/options'
18
+ require 'google/apis/errors'
19
+ require 'retriable'
20
+ require 'google/apis/core/logging'
21
+ require 'pp'
22
+
23
+ module Google
24
+ module Apis
25
+ module Core
26
+ # Command for HTTP request/response.
27
+ class HttpCommand
28
+ include Logging
29
+
30
+ RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError]
31
+
32
+ begin
33
+ require 'opencensus'
34
+ OPENCENSUS_AVAILABLE = true
35
+ rescue LoadError
36
+ OPENCENSUS_AVAILABLE = false
37
+ end
38
+
39
+ # Request options
40
+ # @return [Google::Apis::RequestOptions]
41
+ attr_accessor :options
42
+
43
+ # HTTP request URL
44
+ # @return [String, Addressable::URI]
45
+ attr_accessor :url
46
+
47
+ # HTTP headers
48
+ # @return [Hash]
49
+ attr_accessor :header
50
+
51
+ # Request body
52
+ # @return [#read]
53
+ attr_accessor :body
54
+
55
+ # HTTP method
56
+ # @return [symbol]
57
+ attr_accessor :method
58
+
59
+ # HTTP Client
60
+ # @return [HTTPClient]
61
+ attr_accessor :connection
62
+
63
+ # Query params
64
+ # @return [Hash]
65
+ attr_accessor :query
66
+
67
+ # Path params for URL Template
68
+ # @return [Hash]
69
+ attr_accessor :params
70
+
71
+ # @param [symbol] method
72
+ # HTTP method
73
+ # @param [String,Addressable::URI, Addressable::Template] url
74
+ # HTTP URL or template
75
+ # @param [String, #read] body
76
+ # Request body
77
+ def initialize(method, url, body: nil)
78
+ self.options = Google::Apis::RequestOptions.default.dup
79
+ self.url = url
80
+ self.url = Addressable::Template.new(url) if url.is_a?(String)
81
+ self.method = method
82
+ self.header = Hash.new
83
+ self.body = body
84
+ self.query = {}
85
+ self.params = {}
86
+ @opencensus_span = nil
87
+ end
88
+
89
+ # Execute the command, retrying as necessary
90
+ #
91
+ # @param [HTTPClient] client
92
+ # HTTP client
93
+ # @yield [result, err] Result or error if block supplied
94
+ # @return [Object]
95
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
96
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
97
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
98
+ def execute(client)
99
+ prepare!
100
+ opencensus_begin_span
101
+ begin
102
+ Retriable.retriable tries: options.retries + 1,
103
+ base_interval: 1,
104
+ multiplier: 2,
105
+ on: RETRIABLE_ERRORS do |try|
106
+ # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
107
+ # auth to be re-attempted without having to retry all sorts of other failures like
108
+ # NotFound, etc
109
+ auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
110
+ Retriable.retriable tries: auth_tries,
111
+ on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
112
+ on_retry: proc { |*| refresh_authorization } do
113
+ execute_once(client).tap do |result|
114
+ if block_given?
115
+ yield result, nil
116
+ end
117
+ end
118
+ end
119
+ end
120
+ rescue => e
121
+ if block_given?
122
+ yield nil, e
123
+ else
124
+ raise e
125
+ end
126
+ end
127
+ ensure
128
+ opencensus_end_span
129
+ @http_res = nil
130
+ release!
131
+ end
132
+
133
+ # Refresh the authorization authorization after a 401 error
134
+ #
135
+ # @private
136
+ # @return [void]
137
+ def refresh_authorization
138
+ # Handled implicitly by auth lib, here in case need to override
139
+ logger.debug('Retrying after authentication failure')
140
+ end
141
+
142
+ # Check if attached credentials can be automatically refreshed
143
+ # @return [Boolean]
144
+ def authorization_refreshable?
145
+ options.authorization.respond_to?(:apply!)
146
+ end
147
+
148
+ # Prepare the request (e.g. calculate headers, add query params, serialize data, etc) before sending
149
+ #
150
+ # @private
151
+ # @return [void]
152
+ def prepare!
153
+ normalize_unicode = true
154
+ if options
155
+ header.update(options.header) if options.header
156
+ query.update(options.query) if options.query
157
+ normalize_unicode = options.normalize_unicode
158
+ end
159
+ self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
160
+ url.query_values = normalize_query_values(query).merge(url.query_values || {})
161
+
162
+ if allow_form_encoding?
163
+ @form_encoded = true
164
+ self.body = Addressable::URI.form_encode(url.query_values(Array))
165
+ self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
166
+ self.url.query_values = {}
167
+ else
168
+ @form_encoded = false
169
+ end
170
+
171
+ self.body = '' unless self.body
172
+ end
173
+
174
+ # Release any resources used by this command
175
+ # @private
176
+ # @return [void]
177
+ def release!
178
+ end
179
+
180
+ # Check the response and either decode body or raise error
181
+ #
182
+ # @param [Fixnum] status
183
+ # HTTP status code of response
184
+ # @param [Hash] header
185
+ # Response headers
186
+ # @param [String, #read] body
187
+ # Response body
188
+ # @return [Object]
189
+ # Response object
190
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
191
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
192
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
193
+ def process_response(status, header, body)
194
+ check_status(status, header, body)
195
+ decode_response_body(header['Content-Type'].first, body)
196
+ end
197
+
198
+ # Check the response and raise error if needed
199
+ #
200
+ # @param [Fixnum] status
201
+ # HTTP status code of response
202
+ # @param [Hash] header
203
+ # HTTP response headers
204
+ # @param [String] body
205
+ # HTTP response body
206
+ # @param [String] message
207
+ # Error message text
208
+ # @return [void]
209
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
210
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
211
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
212
+ def check_status(status, header = nil, body = nil, message = nil)
213
+ # TODO: 304 Not Modified depends on context...
214
+ case status
215
+ when 200...300
216
+ nil
217
+ when 301, 302, 303, 307
218
+ message ||= sprintf('Redirect to %s', header['Location'])
219
+ raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
220
+ when 401
221
+ message ||= 'Unauthorized'
222
+ raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
223
+ when 429
224
+ message ||= 'Rate limit exceeded'
225
+ raise Google::Apis::RateLimitError.new(message, status_code: status, header: header, body: body)
226
+ when 304, 400, 402...500
227
+ message ||= 'Invalid request'
228
+ raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
229
+ when 500...600
230
+ message ||= 'Server error'
231
+ raise Google::Apis::ServerError.new(message, status_code: status, header: header, body: body)
232
+ else
233
+ logger.warn(sprintf('Encountered unexpected status code %s', status))
234
+ message ||= 'Unknown error'
235
+ raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
236
+ end
237
+ end
238
+
239
+ # Process the actual response body. Intended to be overridden by subclasses
240
+ #
241
+ # @param [String] _content_type
242
+ # Content type of body
243
+ # @param [String, #read] body
244
+ # Response body
245
+ # @return [Object]
246
+ def decode_response_body(_content_type, body)
247
+ body
248
+ end
249
+
250
+ # Process a success response
251
+ # @param [Object] result
252
+ # Result object
253
+ # @return [Object] result if no block given
254
+ # @yield [result, nil] if block given
255
+ def success(result, &block)
256
+ logger.debug { sprintf('Success - %s', safe_object_representation(result)) }
257
+ block.call(result, nil) if block_given?
258
+ result
259
+ end
260
+
261
+ # Process an error response
262
+ # @param [StandardError] err
263
+ # Error object
264
+ # @param [Boolean] rethrow
265
+ # True if error should be raised again after handling
266
+ # @return [void]
267
+ # @yield [nil, err] if block given
268
+ # @raise [StandardError] if no block
269
+ def error(err, rethrow: false, &block)
270
+ logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
271
+ if err.is_a?(HTTPClient::BadResponseError)
272
+ begin
273
+ res = err.res
274
+ raise Google::Apis::TransmissionError.new(err) if res.nil?
275
+ check_status(res.status.to_i, res.header, res.body)
276
+ rescue Google::Apis::Error => e
277
+ err = e
278
+ end
279
+ elsif err.is_a?(HTTPClient::TimeoutError) || err.is_a?(SocketError)
280
+ err = Google::Apis::TransmissionError.new(err)
281
+ end
282
+ block.call(nil, err) if block_given?
283
+ fail err if rethrow || block.nil?
284
+ end
285
+
286
+ # Execute the command once.
287
+ #
288
+ # @private
289
+ # @param [HTTPClient] client
290
+ # HTTP client
291
+ # @return [Object]
292
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
293
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
294
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
295
+ def execute_once(client)
296
+ body.rewind if body.respond_to?(:rewind)
297
+ begin
298
+ logger.debug { sprintf('Sending HTTP %s %s', method, url) }
299
+ request_header = header.dup
300
+ apply_request_options(request_header)
301
+
302
+ @http_res = client.request(method.to_s.upcase,
303
+ url.to_s,
304
+ query: nil,
305
+ body: body,
306
+ header: request_header,
307
+ follow_redirect: true)
308
+ logger.debug { @http_res.status }
309
+ logger.debug { safe_response_representation @http_res }
310
+ response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body)
311
+ success(response)
312
+ rescue => e
313
+ logger.debug { sprintf('Caught error %s', e) }
314
+ error(e, rethrow: true)
315
+ end
316
+ end
317
+
318
+ # Update the request with any specified options.
319
+ # @param [Hash] req_header
320
+ # HTTP headers
321
+ # @return [void]
322
+ def apply_request_options(req_header)
323
+ if options.authorization.respond_to?(:apply!)
324
+ options.authorization.apply!(req_header)
325
+ elsif options.authorization.is_a?(String)
326
+ req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
327
+ end
328
+ req_header.update(header)
329
+ end
330
+
331
+ def allow_form_encoding?
332
+ [:post, :put].include?(method) && body.nil?
333
+ end
334
+
335
+ private
336
+
337
+ UNSAFE_CLASS_NAMES = [
338
+ "Google::Apis::CloudkmsV1::DecryptResponse"
339
+ ]
340
+
341
+ def safe_object_representation obj
342
+ name = obj.class.name
343
+ if UNSAFE_CLASS_NAMES.include? name
344
+ "#<#{name} (fields redacted)>"
345
+ else
346
+ PP.pp(obj, "")
347
+ end
348
+ end
349
+
350
+ def safe_response_representation http_res
351
+ if respond_to?(:response_class) && response_class.is_a?(Class) &&
352
+ UNSAFE_CLASS_NAMES.include?(response_class.name)
353
+ return "#<#{http_res.class.name} (fields redacted)>"
354
+ end
355
+ http_res.inspect
356
+ end
357
+
358
+ def opencensus_begin_span
359
+ return unless OPENCENSUS_AVAILABLE && options.use_opencensus
360
+ return if @opencensus_span
361
+ return unless OpenCensus::Trace.span_context
362
+
363
+ @opencensus_span = OpenCensus::Trace.start_span url.path.to_s
364
+ @opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
365
+ @opencensus_span.put_attribute "http.host", url.host.to_s
366
+ @opencensus_span.put_attribute "http.method", method.to_s.upcase
367
+ @opencensus_span.put_attribute "http.path", url.path.to_s
368
+ if body.respond_to? :bytesize
369
+ @opencensus_span.put_message_event \
370
+ OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize
371
+ end
372
+
373
+ formatter = OpenCensus::Trace.config.http_formatter
374
+ if formatter.respond_to? :header_name
375
+ header[formatter.header_name] = formatter.serialize @opencensus_span.context.trace_context
376
+ end
377
+ rescue StandardError => e
378
+ # Log exceptions and continue, so opencensus failures don't cause
379
+ # the entire request to fail.
380
+ logger.debug { sprintf('Error opening OpenCensus span: %s', e) }
381
+ end
382
+
383
+ def opencensus_end_span
384
+ return unless OPENCENSUS_AVAILABLE
385
+ return unless @opencensus_span
386
+ return unless OpenCensus::Trace.span_context
387
+
388
+ if @http_res
389
+ if @http_res.body.respond_to? :bytesize
390
+ @opencensus_span.put_message_event \
391
+ OpenCensus::Trace::SpanBuilder::RECEIVED, 1, @http_res.body.bytesize
392
+ end
393
+ status = @http_res.status.to_i
394
+ if status > 0
395
+ @opencensus_span.set_status map_http_status status
396
+ @opencensus_span.put_attribute "http.status_code", status
397
+ end
398
+ end
399
+
400
+ OpenCensus::Trace.end_span @opencensus_span
401
+ @opencensus_span = nil
402
+ rescue StandardError => e
403
+ # Log exceptions and continue, so failures don't cause leaks by
404
+ # aborting cleanup.
405
+ logger.debug { sprintf('Error finishing OpenCensus span: %s', e) }
406
+ end
407
+
408
+ def form_encoded?
409
+ @form_encoded
410
+ end
411
+
412
+ def map_http_status http_status
413
+ case http_status
414
+ when 200..399 then 0 # OK
415
+ when 400 then 3 # INVALID_ARGUMENT
416
+ when 401 then 16 # UNAUTHENTICATED
417
+ when 403 then 7 # PERMISSION_DENIED
418
+ when 404 then 5 # NOT_FOUND
419
+ when 429 then 8 # RESOURCE_EXHAUSTED
420
+ when 501 then 12 # UNIMPLEMENTED
421
+ when 503 then 14 # UNAVAILABLE
422
+ when 504 then 4 # DEADLINE_EXCEEDED
423
+ else 2 # UNKNOWN
424
+ end
425
+ end
426
+
427
+ def normalize_query_values(input)
428
+ input.inject({}) do |h, (k, v)|
429
+ h[k] = normalize_query_value(v)
430
+ h
431
+ end
432
+ end
433
+
434
+ def normalize_query_value(v)
435
+ case v
436
+ when Array
437
+ v.map { |v2| normalize_query_value(v2) }
438
+ when nil
439
+ nil
440
+ else
441
+ v.to_s
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end
447
+ end