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