Resolve merge conflict on gemspec
[arvados.git] / lib / google / api_client.rb
1 # Copyright 2010 Google Inc.
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
16 require 'faraday'
17 require 'multi_json'
18 require 'compat/multi_json'
19 require 'stringio'
20 require 'retriable'
21
22 require 'google/api_client/version'
23 require 'google/api_client/logging'
24 require 'google/api_client/errors'
25 require 'google/api_client/environment'
26 require 'google/api_client/discovery'
27 require 'google/api_client/request'
28 require 'google/api_client/reference'
29 require 'google/api_client/result'
30 require 'google/api_client/media'
31 require 'google/api_client/service_account'
32 require 'google/api_client/batch'
33 require 'google/api_client/gzip'
34 require 'google/api_client/client_secrets'
35 require 'google/api_client/railtie' if defined?(Rails::Railtie)
36
37 module Google
38
39   ##
40   # This class manages APIs communication.
41   class APIClient
42     include Google::APIClient::Logging
43     
44     ##
45     # Creates a new Google API client.
46     #
47     # @param [Hash] options The configuration parameters for the client.
48     # @option options [Symbol, #generate_authenticated_request] :authorization
49     #   (:oauth_1)
50     #   The authorization mechanism used by the client.  The following
51     #   mechanisms are supported out-of-the-box:
52     #   <ul>
53     #     <li><code>:two_legged_oauth_1</code></li>
54     #     <li><code>:oauth_1</code></li>
55     #     <li><code>:oauth_2</code></li>
56     #   </ul>
57     # @option options [Boolean] :auto_refresh_token (true)
58     #   The setting that controls whether or not the api client attempts to
59     #   refresh authorization when a 401 is hit in #execute. If the token does 
60     #   not support it, this option is ignored.
61     # @option options [String] :application_name
62     #   The name of the application using the client.
63     # @option options [String] :application_version
64     #   The version number of the application using the client.
65     # @option options [String] :user_agent
66     #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
67     #   The user agent used by the client.  Most developers will want to
68     #   leave this value alone and use the `:application_name` option instead.
69     # @option options [String] :host ("www.googleapis.com")
70     #   The API hostname used by the client. This rarely needs to be changed.
71     # @option options [String] :port (443)
72     #   The port number used by the client. This rarely needs to be changed.
73     # @option options [String] :discovery_path ("/discovery/v1")
74     #   The discovery base path. This rarely needs to be changed.
75     # @option options [String] :ca_file
76     #   Optional set of root certificates to use when validating SSL connections.
77     #   By default, a bundled set of trusted roots will be used.
78     def initialize(options={})
79       logger.debug { "#{self.class} - Initializing client with options #{options}" }
80       
81       # Normalize key to String to allow indifferent access.
82       options = options.inject({}) do |accu, (key, value)|
83         accu[key.to_sym] = value
84         accu
85       end
86       # Almost all API usage will have a host of 'www.googleapis.com'.
87       self.host = options[:host] || 'www.googleapis.com'
88       self.port = options[:port] || 443
89       self.discovery_path = options[:discovery_path] || '/discovery/v1'
90
91       # Most developers will want to leave this value alone and use the
92       # application_name option.
93       if options[:application_name]
94         app_name = options[:application_name]
95         app_version = options[:application_version]
96         application_string = "#{app_name}/#{app_version || '0.0.0'}"
97       else
98         logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
99       end
100       self.user_agent = options[:user_agent] || (
101         "#{application_string} " +
102         "google-api-ruby-client/#{Google::APIClient::VERSION::STRING} #{ENV::OS_VERSION} (gzip)"
103       ).strip
104       # The writer method understands a few Symbols and will generate useful
105       # default authentication mechanisms.
106       self.authorization =
107         options.key?(:authorization) ? options[:authorization] : :oauth_2
108       self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
109       self.key = options[:key]
110       self.user_ip = options[:user_ip]
111       self.retries = options.fetch(:retries) { 0 }
112       @discovery_uris = {}
113       @discovery_documents = {}
114       @discovered_apis = {}
115       ca_file = options[:ca_file] || File.expand_path('../../cacerts.pem', __FILE__)
116       self.connection = Faraday.new do |faraday|
117         faraday.response :gzip
118         faraday.options.params_encoder = Faraday::FlatParamsEncoder
119         faraday.ssl.ca_file = ca_file
120         faraday.ssl.verify = true
121         faraday.adapter Faraday.default_adapter
122       end      
123       return self
124     end
125
126     ##
127     # Returns the authorization mechanism used by the client.
128     #
129     # @return [#generate_authenticated_request] The authorization mechanism.
130     attr_reader :authorization
131
132     ##
133     # Sets the authorization mechanism used by the client.
134     #
135     # @param [#generate_authenticated_request] new_authorization
136     #   The new authorization mechanism.
137     def authorization=(new_authorization)
138       case new_authorization
139       when :oauth_1, :oauth
140         require 'signet/oauth_1/client'
141         # NOTE: Do not rely on this default value, as it may change
142         new_authorization = Signet::OAuth1::Client.new(
143           :temporary_credential_uri =>
144             'https://www.google.com/accounts/OAuthGetRequestToken',
145           :authorization_uri =>
146             'https://www.google.com/accounts/OAuthAuthorizeToken',
147           :token_credential_uri =>
148             'https://www.google.com/accounts/OAuthGetAccessToken',
149           :client_credential_key => 'anonymous',
150           :client_credential_secret => 'anonymous'
151         )
152       when :two_legged_oauth_1, :two_legged_oauth
153         require 'signet/oauth_1/client'
154         # NOTE: Do not rely on this default value, as it may change
155         new_authorization = Signet::OAuth1::Client.new(
156           :client_credential_key => nil,
157           :client_credential_secret => nil,
158           :two_legged => true
159         )
160       when :oauth_2
161         require 'signet/oauth_2/client'
162         # NOTE: Do not rely on this default value, as it may change
163         new_authorization = Signet::OAuth2::Client.new(
164           :authorization_uri =>
165             'https://accounts.google.com/o/oauth2/auth',
166           :token_credential_uri =>
167             'https://accounts.google.com/o/oauth2/token'
168         )
169       when nil
170         # No authorization mechanism
171       else
172         if !new_authorization.respond_to?(:generate_authenticated_request)
173           raise TypeError,
174             'Expected authorization mechanism to respond to ' +
175             '#generate_authenticated_request.'
176         end
177       end
178       @authorization = new_authorization
179       return @authorization
180     end
181
182     ##
183     # Default Faraday/HTTP connection.
184     #
185     # @return [Faraday::Connection]
186     attr_accessor :connection
187
188     ##
189     # The setting that controls whether or not the api client attempts to
190     # refresh authorization when a 401 is hit in #execute. 
191     #
192     # @return [Boolean]
193     attr_accessor :auto_refresh_token
194
195     ##
196     # The application's API key issued by the API console.
197     #
198     # @return [String] The API key.
199     attr_accessor :key
200
201     ##
202     # The IP address of the user this request is being performed on behalf of.
203     #
204     # @return [String] The user's IP address.
205     attr_accessor :user_ip
206
207     ##
208     # The user agent used by the client.
209     #
210     # @return [String]
211     #   The user agent string used in the User-Agent header.
212     attr_accessor :user_agent
213
214     ##
215     # The API hostname used by the client.
216     #
217     # @return [String]
218     #   The API hostname. Should almost always be 'www.googleapis.com'.
219     attr_accessor :host
220
221     ##
222     # The port number used by the client.
223     #
224     # @return [String]
225     #   The port number. Should almost always be 443.
226     attr_accessor :port
227
228     ##
229     # The base path used by the client for discovery.
230     #
231     # @return [String]
232     #   The base path. Should almost always be '/discovery/v1'.
233     attr_accessor :discovery_path
234
235     ##
236     # Number of times to retry on recoverable errors
237     # 
238     # @return [FixNum]
239     #  Number of retries
240     attr_accessor :retries
241
242     ##
243     # Returns the URI for the directory document.
244     #
245     # @return [Addressable::URI] The URI of the directory document.
246     def directory_uri
247       return resolve_uri(self.discovery_path + '/apis')
248     end
249
250     ##
251     # Manually registers a URI as a discovery document for a specific version
252     # of an API.
253     #
254     # @param [String, Symbol] api The API name.
255     # @param [String] version The desired version of the API.
256     # @param [Addressable::URI] uri The URI of the discovery document.
257     def register_discovery_uri(api, version, uri)
258       api = api.to_s
259       version = version || 'v1'
260       @discovery_uris["#{api}:#{version}"] = uri
261     end
262
263     ##
264     # Returns the URI for the discovery document.
265     #
266     # @param [String, Symbol] api The API name.
267     # @param [String] version The desired version of the API.
268     # @return [Addressable::URI] The URI of the discovery document.
269     def discovery_uri(api, version=nil)
270       api = api.to_s
271       version = version || 'v1'
272       return @discovery_uris["#{api}:#{version}"] ||= (
273         resolve_uri(
274           self.discovery_path + '/apis/{api}/{version}/rest',
275           'api' => api,
276           'version' => version
277         )
278       )
279     end
280
281     ##
282     # Manually registers a pre-loaded discovery document for a specific version
283     # of an API.
284     #
285     # @param [String, Symbol] api The API name.
286     # @param [String] version The desired version of the API.
287     # @param [String, StringIO] discovery_document
288     #   The contents of the discovery document.
289     def register_discovery_document(api, version, discovery_document)
290       api = api.to_s
291       version = version || 'v1'
292       if discovery_document.kind_of?(StringIO)
293         discovery_document.rewind
294         discovery_document = discovery_document.string
295       elsif discovery_document.respond_to?(:to_str)
296         discovery_document = discovery_document.to_str
297       else
298         raise TypeError,
299           "Expected String or StringIO, got #{discovery_document.class}."
300       end
301       @discovery_documents["#{api}:#{version}"] =
302         MultiJson.load(discovery_document)
303     end
304
305     ##
306     # Returns the parsed directory document.
307     #
308     # @return [Hash] The parsed JSON from the directory document.
309     def directory_document
310       return @directory_document ||= (begin
311         response = self.execute!(
312           :http_method => :get,
313           :uri => self.directory_uri,
314           :authenticated => false
315         )
316         response.data
317       end)
318     end
319
320     ##
321     # Returns the parsed discovery document.
322     #
323     # @param [String, Symbol] api The API name.
324     # @param [String] version The desired version of the API.
325     # @return [Hash] The parsed JSON from the discovery document.
326     def discovery_document(api, version=nil)
327       api = api.to_s
328       version = version || 'v1'
329       return @discovery_documents["#{api}:#{version}"] ||= (begin
330         response = self.execute!(
331           :http_method => :get,
332           :uri => self.discovery_uri(api, version),
333           :authenticated => false
334         )
335         response.data
336       end)
337     end
338
339     ##
340     # Returns all APIs published in the directory document.
341     #
342     # @return [Array] The list of available APIs.
343     def discovered_apis
344       @directory_apis ||= (begin
345         document_base = self.directory_uri
346         if self.directory_document && self.directory_document['items']
347           self.directory_document['items'].map do |discovery_document|
348             Google::APIClient::API.new(
349               document_base,
350               discovery_document
351             )
352           end
353         else
354           []
355         end
356       end)
357     end
358
359     ##
360     # Returns the service object for a given service name and service version.
361     #
362     # @param [String, Symbol] api The API name.
363     # @param [String] version The desired version of the API.
364     #
365     # @return [Google::APIClient::API] The service object.
366     def discovered_api(api, version=nil)
367       if !api.kind_of?(String) && !api.kind_of?(Symbol)
368         raise TypeError,
369           "Expected String or Symbol, got #{api.class}."
370       end
371       api = api.to_s
372       version = version || 'v1'
373       return @discovered_apis["#{api}:#{version}"] ||= begin
374         document_base = self.discovery_uri(api, version)
375         discovery_document = self.discovery_document(api, version)
376         if document_base && discovery_document
377           Google::APIClient::API.new(
378             document_base,
379             discovery_document
380           )
381         else
382           nil
383         end
384       end
385     end
386
387     ##
388     # Returns the method object for a given RPC name and service version.
389     #
390     # @param [String, Symbol] rpc_name The RPC name of the desired method.
391     # @param [String, Symbol] api The API the method is within.
392     # @param [String] version The desired version of the API.
393     #
394     # @return [Google::APIClient::Method] The method object.
395     def discovered_method(rpc_name, api, version=nil)
396       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
397         raise TypeError,
398           "Expected String or Symbol, got #{rpc_name.class}."
399       end
400       rpc_name = rpc_name.to_s
401       api = api.to_s
402       version = version || 'v1'
403       service = self.discovered_api(api, version)
404       if service.to_h[rpc_name]
405         return service.to_h[rpc_name]
406       else
407         return nil
408       end
409     end
410
411     ##
412     # Returns the service object with the highest version number.
413     #
414     # @note <em>Warning</em>: This method should be used with great care.
415     # As APIs are updated, minor differences between versions may cause
416     # incompatibilities. Requesting a specific version will avoid this issue.
417     #
418     # @param [String, Symbol] api The name of the service.
419     #
420     # @return [Google::APIClient::API] The service object.
421     def preferred_version(api)
422       if !api.kind_of?(String) && !api.kind_of?(Symbol)
423         raise TypeError,
424           "Expected String or Symbol, got #{api.class}."
425       end
426       api = api.to_s
427       return self.discovered_apis.detect do |a|
428         a.name == api && a.preferred == true
429       end
430     end
431
432     ##
433     # Verifies an ID token against a server certificate. Used to ensure that
434     # an ID token supplied by an untrusted client-side mechanism is valid.
435     # Raises an error if the token is invalid or missing.
436     # 
437     # @deprecated Use the google-id-token gem for verifying JWTs
438     def verify_id_token!
439       require 'jwt'
440       require 'openssl'
441       @certificates ||= {}
442       if !self.authorization.respond_to?(:id_token)
443         raise ArgumentError, (
444           "Current authorization mechanism does not support ID tokens: " +
445           "#{self.authorization.class.to_s}"
446         )
447       elsif !self.authorization.id_token
448         raise ArgumentError, (
449           "Could not verify ID token, ID token missing. " +
450           "Scopes were: #{self.authorization.scope.inspect}"
451         )
452       else
453         check_cached_certs = lambda do
454           valid = false
455           for key, cert in @certificates
456             begin
457               self.authorization.decoded_id_token(cert.public_key)
458               valid = true
459             rescue JWT::DecodeError, Signet::UnsafeOperationError
460               # Expected exception. Ignore, ID token has not been validated.
461             end
462           end
463           valid
464         end
465         if check_cached_certs.call()
466           return true
467         end
468         response = self.execute!(
469           :http_method => :get,
470           :uri => 'https://www.googleapis.com/oauth2/v1/certs',
471           :authenticated => false
472         )
473         @certificates.merge!(
474           Hash[MultiJson.load(response.body).map do |key, cert|
475             [key, OpenSSL::X509::Certificate.new(cert)]
476           end]
477         )
478         if check_cached_certs.call()
479           return true
480         else
481           raise InvalidIDTokenError,
482             "Could not verify ID token against any available certificate."
483         end
484       end
485       return nil
486     end
487
488     ##
489     # Generates a request.
490     #
491     # @option options [Google::APIClient::Method] :api_method
492     #   The method object or the RPC name of the method being executed.
493     # @option options [Hash, Array] :parameters
494     #   The parameters to send to the method.
495     # @option options [Hash, Array] :headers The HTTP headers for the request.
496     # @option options [String] :body The body of the request.
497     # @option options [String] :version ("v1")
498     #   The service version. Only used if `api_method` is a `String`.
499     # @option options [#generate_authenticated_request] :authorization
500     #   The authorization mechanism for the response. Used only if
501     #   `:authenticated` is `true`.
502     # @option options [TrueClass, FalseClass] :authenticated (true)
503     #   `true` if the request must be signed or somehow
504     #   authenticated, `false` otherwise.
505     #
506     # @return [Google::APIClient::Reference] The generated request.
507     #
508     # @example
509     #   request = client.generate_request(
510     #     :api_method => 'plus.activities.list',
511     #     :parameters =>
512     #       {'collection' => 'public', 'userId' => 'me'}
513     #   )
514     def generate_request(options={})
515       options = {
516         :api_client => self
517       }.merge(options)
518       return Google::APIClient::Request.new(options)
519     end
520
521     ##
522     # Executes a request, wrapping it in a Result object.
523     #
524     # @param [Google::APIClient::Request, Hash, Array] params
525     #   Either a Google::APIClient::Request, a Hash, or an Array.
526     #
527     #   If a Google::APIClient::Request, no other parameters are expected.
528     #
529     #   If a Hash, the below parameters are handled. If an Array, the
530     #   parameters are assumed to be in the below order:
531     #
532     #   - (Google::APIClient::Method) api_method:
533     #     The method object or the RPC name of the method being executed.
534     #   - (Hash, Array) parameters:
535     #     The parameters to send to the method.
536     #   - (String) body: The body of the request.
537     #   - (Hash, Array) headers: The HTTP headers for the request.
538     #   - (Hash) options: A set of options for the request, of which:
539     #     - (#generate_authenticated_request) :authorization (default: true) -
540     #       The authorization mechanism for the response. Used only if
541     #       `:authenticated` is `true`.
542     #     - (TrueClass, FalseClass) :authenticated (default: true) -
543     #       `true` if the request must be signed or somehow
544     #       authenticated, `false` otherwise.
545     #     - (TrueClass, FalseClass) :gzip (default: true) - 
546     #       `true` if gzip enabled, `false` otherwise.
547     #     - (FixNum) :retries -
548     #       # of times to retry on recoverable errors
549     #
550     # @return [Google::APIClient::Result] The result from the API, nil if batch.
551     #
552     # @example
553     #   result = client.execute(batch_request)
554     #
555     # @example
556     #   plus = client.discovered_api('plus')
557     #   result = client.execute(
558     #     :api_method => plus.activities.list,
559     #     :parameters => {'collection' => 'public', 'userId' => 'me'}
560     #   )
561     #
562     # @see Google::APIClient#generate_request
563     def execute!(*params)
564       if params.first.kind_of?(Google::APIClient::Request)
565         request = params.shift
566         options = params.shift || {}
567       else
568         # This block of code allows us to accept multiple parameter passing
569         # styles, and maintaining some backwards compatibility.
570         #
571         # Note: I'm extremely tempted to deprecate this style of execute call.
572         if params.last.respond_to?(:to_hash) && params.size == 1
573           options = params.pop
574         else
575           options = {}
576         end
577
578         options[:api_method] = params.shift if params.size > 0
579         options[:parameters] = params.shift if params.size > 0
580         options[:body] = params.shift if params.size > 0
581         options[:headers] = params.shift if params.size > 0
582         options.update(params.shift) if params.size > 0
583         request = self.generate_request(options)
584       end
585       
586       request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
587       request.headers['Accept-Encoding'] ||= 'gzip' unless options[:gzip] == false
588       request.headers['Content-Type'] ||= ''
589       request.parameters['key'] ||= self.key unless self.key.nil?
590       request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
591
592       connection = options[:connection] || self.connection
593       request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
594       tries = 1 + (options[:retries] || self.retries)
595       Retriable.retriable :tries => tries, 
596                           :on => [TransmissionError], 
597                           :interval => lambda {|attempts| (2 ** attempts) + rand} do
598         result = request.send(connection, true)
599
600         case result.status
601           when 200...300
602             result
603           when 301, 302, 303, 307
604             request = generate_request(request.to_hash.merge({
605               :uri => result.headers['location'],
606               :api_method => nil
607             }))
608             raise RedirectError.new(result.headers['location'], result)
609           when 400...500
610             if result.status == 401 && request.authorization.respond_to?(:refresh_token) && auto_refresh_token
611               begin
612                 logger.debug("Attempting refresh of access token & retry of request")
613                 request.authorization.fetch_access_token!
614               rescue Signet::AuthorizationError
615                  # Ignore since we want the original error
616               end
617             end
618             raise ClientError.new(result.error_message || "A client error has occurred", result)
619           when 500...600
620             raise ServerError.new(result.error_message || "A server error has occurred", result)
621           else
622             raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
623         end
624       end
625     end
626
627     ##
628     # Same as Google::APIClient#execute!, but does not raise an exception for
629     # normal API errros.
630     #
631     # @see Google::APIClient#execute
632     def execute(*params)
633       begin
634         return self.execute!(*params)
635       rescue TransmissionError => e
636         return e.result
637       end
638     end
639
640     protected
641
642     ##
643     # Resolves a URI template against the client's configured base.
644     #
645     # @api private
646     # @param [String, Addressable::URI, Addressable::Template] template
647     #   The template to resolve.
648     # @param [Hash] mapping The mapping that corresponds to the template.
649     # @return [Addressable::URI] The expanded URI.
650     def resolve_uri(template, mapping={})
651       @base_uri ||= Addressable::URI.new(
652         :scheme => 'https',
653         :host => self.host,
654         :port => self.port
655       ).normalize
656       template = if template.kind_of?(Addressable::Template)
657         template.pattern
658       elsif template.respond_to?(:to_str)
659         template.to_str
660       else
661         raise TypeError,
662           "Expected String, Addressable::URI, or Addressable::Template, " +
663           "got #{template.class}."
664       end
665       return Addressable::Template.new(@base_uri + template).expand(mapping)
666     end
667     
668   end
669
670 end
671
672 require 'google/api_client/version'