Merge pull request #4 from vapir/baseURI_and_fixups
[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.key?("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     def relative_uri(path, expand={})
179       Addressable::Template.new(baseURI+path).expand(expand)
180     end
181
182     ##
183     # Returns the URI for the directory document.
184     #
185     # @return [Addressable::URI] The URI of the directory document.
186     def directory_uri
187       relative_uri('/apis')
188     end
189
190     ##
191     # Manually registers a URI as a discovery document for a specific version
192     # of an API.
193     #
194     # @param [String, Symbol] api The API name.
195     # @param [String] version The desired version of the API.
196     # @param [Addressable::URI] uri The URI of the discovery document.
197     def register_discovery_uri(api, version, uri)
198       api = api.to_s
199       version = version || 'v1'
200       @discovery_uris["#{api}:#{version}"] = uri
201     end
202
203     ##
204     # Returns the URI for the discovery document.
205     #
206     # @param [String, Symbol] api The API name.
207     # @param [String] version The desired version of the API.
208     # @return [Addressable::URI] The URI of the discovery document.
209     def discovery_uri(api, version=nil)
210       api = api.to_s
211       version = version || 'v1'
212       return @discovery_uris["#{api}:#{version}"] ||= (begin
213         relative_uri("/apis/{api}/{version}/rest", 'api' => api, 'version' => version)
214       end)
215     end
216
217     ##
218     # Manually registers a pre-loaded discovery document for a specific version
219     # of an API.
220     #
221     # @param [String, Symbol] api The API name.
222     # @param [String] version The desired version of the API.
223     # @param [String, StringIO] discovery_document
224     #   The contents of the discovery document.
225     def register_discovery_document(api, version, discovery_document)
226       api = api.to_s
227       version = version || 'v1'
228       if discovery_document.kind_of?(StringIO)
229         discovery_document.rewind
230         discovery_document = discovery_document.string
231       elsif discovery_document.respond_to?(:to_str)
232         discovery_document = discovery_document.to_str
233       else
234         raise TypeError,
235           "Expected String or StringIO, got #{discovery_document.class}."
236       end
237       @discovery_documents["#{api}:#{version}"] =
238         MultiJson.decode(discovery_document)
239     end
240
241     ##
242     # Returns the parsed directory document.
243     #
244     # @return [Hash] The parsed JSON from the directory document.
245     def directory_document
246       return @directory_document ||= (begin
247         request = self.generate_request(
248           :http_method => :get,
249           :uri => self.directory_uri,
250           :authenticated => false
251         )
252         response = self.transmit(:request => request)
253         if response.status >= 200 && response.status < 300
254           MultiJson.decode(response.body)
255         elsif response.status >= 400
256           case response.status
257           when 400...500
258             exception_type = ClientError
259           when 500...600
260             exception_type = ServerError
261           else
262             exception_type = TransmissionError
263           end
264           url = request.to_env(Faraday.default_connection)[:url]
265           raise exception_type,
266             "Could not retrieve directory document at: #{url}"
267         end
268       end)
269     end
270
271     ##
272     # Returns the parsed discovery document.
273     #
274     # @param [String, Symbol] api The API name.
275     # @param [String] version The desired version of the API.
276     # @return [Hash] The parsed JSON from the discovery document.
277     def discovery_document(api, version=nil)
278       api = api.to_s
279       version = version || 'v1'
280       return @discovery_documents["#{api}:#{version}"] ||= (begin
281         request = self.generate_request(
282           :http_method => :get,
283           :uri => self.discovery_uri(api, version),
284           :authenticated => false
285         )
286         response = self.transmit(:request => request)
287         if response.status >= 200 && response.status < 300
288           MultiJson.decode(response.body)
289         elsif response.status >= 400
290           case response.status
291           when 400...500
292             exception_type = ClientError
293           when 500...600
294             exception_type = ServerError
295           else
296             exception_type = TransmissionError
297           end
298           url = request.to_env(Faraday.default_connection)[:url]
299           raise exception_type,
300             "Could not retrieve discovery document at: #{url}"
301         end
302       end)
303     end
304
305     ##
306     # Returns all APIs published in the directory document.
307     #
308     # @return [Array] The list of available APIs.
309     def discovered_apis
310       @directory_apis ||= (begin
311         document_base = self.directory_uri
312         if self.directory_document && self.directory_document['items']
313           self.directory_document['items'].map do |discovery_document|
314             Google::APIClient::API.new(
315               document_base,
316               discovery_document
317             )
318           end
319         else
320           []
321         end
322       end)
323     end
324
325     ##
326     # Returns the service object for a given service name and service version.
327     #
328     # @param [String, Symbol] api The API name.
329     # @param [String] version The desired version of the API.
330     #
331     # @return [Google::APIClient::API] The service object.
332     def discovered_api(api, version=nil)
333       if !api.kind_of?(String) && !api.kind_of?(Symbol)
334         raise TypeError,
335           "Expected String or Symbol, got #{api.class}."
336       end
337       api = api.to_s
338       version = version || 'v1'
339       return @discovered_apis["#{api}:#{version}"] ||= begin
340         document_base = self.discovery_uri(api, version)
341         discovery_document = self.discovery_document(api, version)
342         if document_base && discovery_document
343           Google::APIClient::API.new(
344             document_base,
345             discovery_document
346           )
347         else
348           nil
349         end
350       end
351     end
352
353     ##
354     # Returns the method object for a given RPC name and service version.
355     #
356     # @param [String, Symbol] rpc_name The RPC name of the desired method.
357     # @param [String, Symbol] rpc_name The API the method is within.
358     # @param [String] version The desired version of the API.
359     #
360     # @return [Google::APIClient::Method] The method object.
361     def discovered_method(rpc_name, api, version=nil)
362       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
363         raise TypeError,
364           "Expected String or Symbol, got #{rpc_name.class}."
365       end
366       rpc_name = rpc_name.to_s
367       api = api.to_s
368       version = version || 'v1'
369       service = self.discovered_api(api, version)
370       if service.to_h[rpc_name]
371         return service.to_h[rpc_name]
372       else
373         return nil
374       end
375     end
376
377     ##
378     # Returns the service object with the highest version number.
379     #
380     # @note <em>Warning</em>: This method should be used with great care.
381     # As APIs are updated, minor differences between versions may cause
382     # incompatibilities. Requesting a specific version will avoid this issue.
383     #
384     # @param [String, Symbol] api The name of the service.
385     #
386     # @return [Google::APIClient::API] The service object.
387     def preferred_version(api)
388       if !api.kind_of?(String) && !api.kind_of?(Symbol)
389         raise TypeError,
390           "Expected String or Symbol, got #{api.class}."
391       end
392       api = api.to_s
393       return self.discovered_apis.detect do |a|
394         a.name == api && a.preferred == true
395       end
396     end
397
398     ##
399     # Verifies an ID token against a server certificate. Used to ensure that
400     # an ID token supplied by an untrusted client-side mechanism is valid.
401     # Raises an error if the token is invalid or missing.
402     def verify_id_token!
403       gem 'jwt', '~> 0.1.4'
404       require 'jwt'
405       require 'openssl'
406       @certificates ||= {}
407       if !self.authorization.respond_to?(:id_token)
408         raise ArgumentError, (
409           "Current authorization mechanism does not support ID tokens: " +
410           "#{self.authorization.class.to_s}"
411         )
412       elsif !self.authorization.id_token
413         raise ArgumentError, (
414           "Could not verify ID token, ID token missing. " +
415           "Scopes were: #{self.authorization.scope.inspect}"
416         )
417       else
418         check_cached_certs = lambda do
419           valid = false
420           for key, cert in @certificates
421             begin
422               self.authorization.decoded_id_token(cert.public_key)
423               valid = true
424             rescue JWT::DecodeError, Signet::UnsafeOperationError
425               # Expected exception. Ignore, ID token has not been validated.
426             end
427           end
428           valid
429         end
430         if check_cached_certs.call()
431           return true
432         end
433         request = self.generate_request(
434           :http_method => :get,
435           :uri => 'https://www.googleapis.com/oauth2/v1/certs',
436           :authenticated => false
437         )
438         response = self.transmit(:request => request)
439         if response.status >= 200 && response.status < 300
440           @certificates.merge!(
441             Hash[MultiJson.decode(response.body).map do |key, cert|
442               [key, OpenSSL::X509::Certificate.new(cert)]
443             end]
444           )
445         elsif response.status >= 400
446           case response.status
447           when 400...500
448             exception_type = ClientError
449           when 500...600
450             exception_type = ServerError
451           else
452             exception_type = TransmissionError
453           end
454           url = request.to_env(Faraday.default_connection)[:url]
455           raise exception_type,
456             "Could not retrieve certificates from: #{url}"
457         end
458         if check_cached_certs.call()
459           return true
460         else
461           raise InvalidIDTokenError,
462             "Could not verify ID token against any available certificate."
463         end
464       end
465       return nil
466     end
467
468     ##
469     # Generates a request.
470     #
471     # @option options [Google::APIClient::Method, String] :api_method
472     #   The method object or the RPC name of the method being executed.
473     # @option options [Hash, Array] :parameters
474     #   The parameters to send to the method.
475     # @option options [Hash, Array] :headers The HTTP headers for the request.
476     # @option options [String] :body The body of the request.
477     # @option options [String] :version ("v1")
478     #   The service version. Only used if `api_method` is a `String`.
479     # @option options [#generate_authenticated_request] :authorization
480     #   The authorization mechanism for the response. Used only if
481     #   `:authenticated` is `true`.
482     # @option options [TrueClass, FalseClass] :authenticated (true)
483     #   `true` if the request must be signed or somehow
484     #   authenticated, `false` otherwise.
485     #
486     # @return [Faraday::Request] The generated request.
487     #
488     # @example
489     #   request = client.generate_request(
490     #     :api_method => 'plus.activities.list',
491     #     :parameters =>
492     #       {'collection' => 'public', 'userId' => 'me'}
493     #   )
494     def generate_request(options={})
495       # Note: The merge method on a Hash object will coerce an API Reference
496       # object into a Hash and merge with the default options.
497       options={
498         :version => 'v1',
499         :authorization => self.authorization,
500         :key => self.key,
501         :user_ip => self.user_ip,
502         :connection => Faraday.default_connection
503       }.merge(options)
504       # The Reference object is going to need this to do method ID lookups.
505       options[:client] = self
506       # The default value for the :authenticated option depends on whether an
507       # authorization mechanism has been set.
508       if options[:authorization]
509         options = {:authenticated => true}.merge(options)
510       else
511         options = {:authenticated => false}.merge(options)
512       end
513       reference = Google::APIClient::Reference.new(options)
514       request = reference.to_request
515       if options[:authenticated]
516         request = self.generate_authenticated_request(
517           :request => request,
518           :connection => options[:connection]
519         )
520       end
521       return request
522     end
523
524     ##
525     # Signs a request using the current authorization mechanism.
526     #
527     # @param [Hash] options a customizable set of options
528     #
529     # @return [Faraday::Request] The signed or otherwise authenticated request.
530     def generate_authenticated_request(options={})
531       return authorization.generate_authenticated_request(options)
532     end
533
534     ##
535     # Transmits the request using the current HTTP adapter.
536     #
537     # @option options [Array, Faraday::Request] :request
538     #   The HTTP request to transmit.
539     # @option options [String, Symbol] :method
540     #   The method for the HTTP request.
541     # @option options [String, Addressable::URI] :uri
542     #   The URI for the HTTP request.
543     # @option options [Array, Hash] :headers
544     #   The headers for the HTTP request.
545     # @option options [String] :body
546     #   The body for the HTTP request.
547     # @option options [Faraday::Connection] :connection
548     #   The HTTP connection to use.
549     #
550     # @return [Faraday::Response] The response from the server.
551     def transmit(options={})
552       options[:connection] ||= Faraday.default_connection
553       if options[:request]
554         if options[:request].kind_of?(Array)
555           method, uri, headers, body = options[:request]
556         elsif options[:request].kind_of?(Faraday::Request)
557           unless options[:connection]
558             raise ArgumentError,
559               "Faraday::Request used, requires a connection to be provided."
560           end
561           method = options[:request].method.to_s.downcase.to_sym
562           uri = options[:connection].build_url(
563             options[:request].path, options[:request].params
564           )
565           headers = options[:request].headers || {}
566           body = options[:request].body || ''
567         end
568       else
569         method = options[:method] || :get
570         uri = options[:uri]
571         headers = options[:headers] || []
572         body = options[:body] || ''
573       end
574       headers = headers.to_a if headers.kind_of?(Hash)
575       request_components = {
576         :method => method,
577         :uri => uri,
578         :headers => headers,
579         :body => body
580       }
581       # Verify that we have all pieces required to transmit an HTTP request
582       request_components.each do |(key, value)|
583         unless value
584           raise ArgumentError, "Missing :#{key} parameter."
585         end
586       end
587
588       if self.user_agent != nil
589         # If there's no User-Agent header, set one.
590         unless headers.kind_of?(Enumerable)
591           # We need to use some Enumerable methods, relying on the presence of
592           # the #each method.
593           class << headers
594             include Enumerable
595           end
596         end
597         if self.user_agent.kind_of?(String)
598           unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
599             headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
600           end
601         elsif self.user_agent != nil
602           raise TypeError,
603             "Expected User-Agent to be String, got #{self.user_agent.class}"
604         end
605       end
606
607       request = Faraday::Request.create(method.to_s.downcase.to_sym) do |req|
608         req.url(Addressable::URI.parse(uri))
609         req.headers = Faraday::Utils::Headers.new(headers)
610         req.body = body
611       end
612       request_env = request.to_env(options[:connection])
613       response = options[:connection].app.call(request_env)
614       return response
615     end
616
617     ##
618     # Executes a request, wrapping it in a Result object.
619     #
620     # @param [Google::APIClient::Method, String] api_method
621     #   The method object or the RPC name of the method being executed.
622     # @param [Hash, Array] parameters
623     #   The parameters to send to the method.
624     # @param [String] body The body of the request.
625     # @param [Hash, Array] headers The HTTP headers for the request.
626     # @option options [String] :version ("v1")
627     #   The service version. Only used if `api_method` is a `String`.
628     # @option options [#generate_authenticated_request] :authorization
629     #   The authorization mechanism for the response. Used only if
630     #   `:authenticated` is `true`.
631     # @option options [TrueClass, FalseClass] :authenticated (true)
632     #   `true` if the request must be signed or somehow
633     #   authenticated, `false` otherwise.
634     #
635     # @return [Google::APIClient::Result] The result from the API.
636     #
637     # @example
638     #   result = client.execute(
639     #     :api_method => 'plus.activities.list',
640     #     :parameters => {'collection' => 'public', 'userId' => 'me'}
641     #   )
642     #
643     # @see Google::APIClient#generate_request
644     def execute(*params)
645       # This block of code allows us to accept multiple parameter passing
646       # styles, and maintaining some backwards compatibility.
647       #
648       # Note: I'm extremely tempted to deprecate this style of execute call.
649       if params.last.respond_to?(:to_hash) && params.size == 1
650         options = params.pop
651       else
652         options = {}
653       end
654       options[:api_method] = params.shift if params.size > 0
655       options[:parameters] = params.shift if params.size > 0
656       options[:body] = params.shift if params.size > 0
657       options[:headers] = params.shift if params.size > 0
658       options[:client] = self
659
660       reference = Google::APIClient::Reference.new(options)
661       request = self.generate_request(reference)
662       response = self.transmit(
663         :request => request,
664         :connection => options[:connection]
665       )
666       return Google::APIClient::Result.new(reference, request, response)
667     end
668
669     ##
670     # Same as Google::APIClient#execute, but raises an exception if there was
671     # an error.
672     #
673     # @see Google::APIClient#execute
674     def execute!(*params)
675       result = self.execute(*params)
676       if result.data.respond_to?(:error) &&
677           result.data.error.respond_to?(:message)
678         # You're going to get a terrible error message if the response isn't
679         # parsed successfully as an error.
680         error_message = result.data.error.message
681       elsif result.data['error'] && result.data['error']['message']
682         error_message = result.data['error']['message']
683       end
684       if result.response.status >= 400
685         case result.response.status
686         when 400...500
687           exception_type = ClientError
688           error_message ||= "A client error has occurred."
689         when 500...600
690           exception_type = ServerError
691           error_message ||= "A server error has occurred."
692         else
693           exception_type = TransmissionError
694           error_message ||= "A transmission error has occurred."
695         end
696         raise exception_type, error_message
697       end
698       return result
699     end
700   end
701 end
702
703 require 'google/api_client/version'