google-api-client 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
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
+ require 'jwt'
16
+ require 'signet/oauth_2/client'
17
+ require 'delegate'
18
+
19
+ module Google
20
+ class APIClient
21
+ ##
22
+ # Generates access tokens using the JWT assertion profile. Requires a
23
+ # service account & access to the private key.
24
+ #
25
+ # @example
26
+ #
27
+ # client = Google::APIClient.new
28
+ # key = Google::APIClient::PKCS12.load_key('client.p12', 'notasecret')
29
+ # service_account = Google::APIClient::JWTAsserter(
30
+ # '123456-abcdef@developer.gserviceaccount.com',
31
+ # 'https://www.googleapis.com/auth/prediction',
32
+ # key)
33
+ # client.authorization = service_account.authorize
34
+ # client.execute(...)
35
+ #
36
+ # @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount
37
+ class JWTAsserter
38
+ # @return [String] ID/email of the issuing party
39
+ attr_accessor :issuer
40
+ # @return [Fixnum] How long, in seconds, the assertion is valid for
41
+ attr_accessor :expiry
42
+ # @return [Fixnum] Seconds to expand the issued at/expiry window to account for clock skew
43
+ attr_accessor :skew
44
+ # @return [String] Scopes to authorize
45
+ attr_reader :scope
46
+ # @return [OpenSSL::PKey] key for signing assertions
47
+ attr_writer :key
48
+
49
+ ##
50
+ # Initializes the asserter for a service account.
51
+ #
52
+ # @param [String] issuer
53
+ # Name/ID of the client issuing the assertion
54
+ # @param [String, Array] scope
55
+ # Scopes to authorize. May be a space delimited string or array of strings
56
+ # @param [OpenSSL::PKey] key
57
+ # RSA private key for signing assertions
58
+ def initialize(issuer, scope, key)
59
+ self.issuer = issuer
60
+ self.scope = scope
61
+ self.expiry = 60 # 1 min default
62
+ self.skew = 60
63
+ self.key = key
64
+ end
65
+
66
+ ##
67
+ # Set the scopes to authorize
68
+ #
69
+ # @param [String, Array] new_scope
70
+ # Scopes to authorize. May be a space delimited string or array of strings
71
+ def scope=(new_scope)
72
+ case new_scope
73
+ when Array
74
+ @scope = new_scope.join(' ')
75
+ when String
76
+ @scope = new_scope
77
+ when nil
78
+ @scope = ''
79
+ else
80
+ raise TypeError, "Expected Array or String, got #{new_scope.class}"
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Builds & signs the assertion.
86
+ #
87
+ # @param [String] person
88
+ # Email address of a user, if requesting a token to act on their behalf
89
+ # @return [String] Encoded JWT
90
+ def to_jwt(person=nil)
91
+ now = Time.new
92
+ assertion = {
93
+ "iss" => @issuer,
94
+ "scope" => self.scope,
95
+ "aud" => "https://accounts.google.com/o/oauth2/token",
96
+ "exp" => (now + expiry).to_i,
97
+ "iat" => (now - skew).to_i
98
+ }
99
+ assertion['prn'] = person unless person.nil?
100
+ return JWT.encode(assertion, @key, "RS256")
101
+ end
102
+
103
+ ##
104
+ # Request a new access token.
105
+ #
106
+ # @param [String] person
107
+ # Email address of a user, if requesting a token to act on their behalf
108
+ # @param [Hash] options
109
+ # Pass through to Signet::OAuth2::Client.fetch_access_token
110
+ # @return [Signet::OAuth2::Client] Access token
111
+ #
112
+ # @see Signet::OAuth2::Client.fetch_access_token
113
+ def authorize(person = nil, options={})
114
+ assertion = self.to_jwt(person)
115
+ authorization = Signet::OAuth2::Client.new(
116
+ :token_credential_uri => 'https://accounts.google.com/o/oauth2/token'
117
+ )
118
+ authorization.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
119
+ authorization.extension_parameters = { :assertion => assertion }
120
+ authorization.fetch_access_token!(options)
121
+ return JWTAuthorization.new(authorization, self, person)
122
+ end
123
+ end
124
+
125
+ class JWTAuthorization < DelegateClass(Signet::OAuth2::Client)
126
+ def initialize(authorization, asserter, person = nil)
127
+ @asserter = asserter
128
+ @person = person
129
+ super(authorization)
130
+ end
131
+
132
+ def fetch_access_token!(options={})
133
+ new_authorization = @asserter.authorize(@person, options)
134
+ __setobj__(new_authorization)
135
+ self
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,48 @@
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
+ module Google
16
+ class APIClient
17
+ ##
18
+ # Helper for loading keys from the PKCS12 files downloaded when
19
+ # setting up service accounts at the APIs Console.
20
+ #
21
+ module PKCS12
22
+ ##
23
+ # Loads a key from PKCS12 file, assuming a single private key
24
+ # is present.
25
+ #
26
+ # @param [String] keyfile
27
+ # Path of the PKCS12 file to load. If not a path to an actual file,
28
+ # assumes the string is the content of the file itself.
29
+ # @param [String] passphrase
30
+ # Passphrase for unlocking the private key
31
+ #
32
+ # @return [OpenSSL::PKey] The private key for signing assertions.
33
+ def self.load_key(keyfile, passphrase)
34
+ begin
35
+ if File.exists?(keyfile)
36
+ content = File.read(keyfile)
37
+ else
38
+ content = keyfile
39
+ end
40
+ pkcs12 = OpenSSL::PKCS12.new(content, passphrase)
41
+ return pkcs12.key
42
+ rescue OpenSSL::PKCS12::PKCS12Error
43
+ raise ArgumentError.new("Invalid keyfile or passphrase")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -13,57 +13,91 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require 'addressable/uri'
16
+ require 'google/api_client/reference'
16
17
  require 'uuidtools'
17
18
 
18
19
  module Google
19
20
  class APIClient
20
21
 
22
+ ##
21
23
  # Helper class to contain a response to an individual batched call.
24
+ #
25
+ # @api private
22
26
  class BatchedCallResponse
27
+ # @return [String] UUID of the call
23
28
  attr_reader :call_id
24
- attr_accessor :status, :headers, :body
29
+ # @return [Fixnum] HTTP status code
30
+ attr_accessor :status
31
+ # @return [Hash] HTTP response headers
32
+ attr_accessor :headers
33
+ # @return [String] HTTP response body
34
+ attr_accessor :body
25
35
 
36
+ ##
37
+ # Initialize the call response
38
+ #
39
+ # @param [String] call_id
40
+ # UUID of the original call
41
+ # @param [Fixnum] status
42
+ # HTTP status
43
+ # @param [Hash] headers
44
+ # HTTP response headers
45
+ # @param [#read, #to_str] body
46
+ # Response body
26
47
  def initialize(call_id, status = nil, headers = nil, body = nil)
27
48
  @call_id, @status, @headers, @body = call_id, status, headers, body
28
49
  end
29
50
  end
30
-
31
- ##
51
+
32
52
  # Wraps multiple API calls into a single over-the-wire HTTP request.
33
- class BatchRequest
34
-
53
+ #
54
+ # @example
55
+ #
56
+ # client = Google::APIClient.new
57
+ # urlshortener = client.discovered_api('urlshortener')
58
+ # batch = Google::APIClient::BatchRequest.new do |result|
59
+ # puts result.data
60
+ # end
61
+ #
62
+ # batch.add(:api_method=>urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/foo' })
63
+ # batch.add(:api_method=>urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/bar' })
64
+ #
65
+ # client.execute(batch)
66
+ #
67
+
68
+ class BatchRequest < Request
35
69
  BATCH_BOUNDARY = "-----------RubyApiBatchRequest".freeze
36
70
 
37
- attr_accessor :options
38
- attr_reader :calls, :callbacks
71
+ # @api private
72
+ # @return [Array<(String,Google::APIClient::Request,Proc)] List of API calls in the batch
73
+ attr_reader :calls
39
74
 
40
75
  ##
41
76
  # Creates a new batch request.
42
77
  #
43
78
  # @param [Hash] options
44
- # Set of options for this request, the only important one being
45
- # :connection, which specifies an HTTP connection to use.
79
+ # Set of options for this request.
46
80
  # @param [Proc] block
47
81
  # Callback for every call's response. Won't be called if a call defined
48
82
  # a callback of its own.
49
83
  #
50
- # @return [Google::APIClient::BatchRequest] The constructed object.
84
+ # @return [Google::APIClient::BatchRequest]
85
+ # The constructed object.
86
+ #
87
+ # @yield [Google::APIClient::Result]
88
+ # block to be called when result ready
51
89
  def initialize(options = {}, &block)
52
- # Request options, ignoring method and parameters.
53
- @options = options
54
- # Batched calls to be made, indexed by call ID.
55
- @calls = {}
56
- # Callbacks per batched call, indexed by call ID.
57
- @callbacks = {}
58
- # Order for the call IDs, since Ruby 1.8 hashes are unordered.
59
- @order = []
60
- # Global callback to be used for every call. If a specific callback
61
- # has been defined for a request, this won't be called.
90
+ @calls = []
62
91
  @global_callback = block if block_given?
63
- # The last auto generated ID.
64
92
  @last_auto_id = 0
65
- # Base ID for the batch request.
66
- @base_id = nil
93
+
94
+ # TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
95
+ @base_id = UUIDTools::UUID.random_create.to_s
96
+
97
+ options[:uri] ||= 'https://www.googleapis.com/batch'
98
+ options[:http_method] ||= 'POST'
99
+
100
+ super options
67
101
  end
68
102
 
69
103
  ##
@@ -72,69 +106,84 @@ module Google
72
106
  # automatically be generated, avoiding collisions. If duplicate call IDs
73
107
  # are provided, an error will be thrown.
74
108
  #
75
- # @param [Hash, Google::APIClient::Reference] call: the call to be added.
76
- # @param [String] call_id: the ID to be used for this call. Must be unique
77
- # @param [Proc] block: callback for this call's response.
109
+ # @param [Hash, Google::APIClient::Request] call
110
+ # the call to be added.
111
+ # @param [String] call_id
112
+ # the ID to be used for this call. Must be unique
113
+ # @param [Proc] block
114
+ # callback for this call's response.
115
+ #
116
+ # @return [Google::APIClient::BatchRequest]
117
+ # the BatchRequest, for chaining
78
118
  #
79
- # @return [Google::APIClient::BatchRequest] The BatchRequest, for chaining
119
+ # @yield [Google::APIClient::Result]
120
+ # block to be called when result ready
80
121
  def add(call, call_id = nil, &block)
81
122
  unless call.kind_of?(Google::APIClient::Reference)
82
123
  call = Google::APIClient::Reference.new(call)
83
124
  end
84
- if call_id.nil?
85
- call_id = new_id
86
- end
87
- if @calls.include?(call_id)
125
+ call_id ||= new_id
126
+ if @calls.assoc(call_id)
88
127
  raise BatchError,
89
128
  'A call with this ID already exists: %s' % call_id
90
129
  end
91
- @calls[call_id] = call
92
- @order << call_id
93
- if block_given?
94
- @callbacks[call_id] = block
95
- elsif @global_callback
96
- @callbacks[call_id] = @global_callback
97
- end
130
+ callback = block_given? ? block : @global_callback
131
+ @calls << [call_id, call, callback]
98
132
  return self
99
133
  end
100
134
 
101
- ##
102
- # Convert this batch request into an HTTP request.
103
- #
104
- # @return [Array<String, String, Hash, String>]
105
- # An array consisting of, in order: HTTP method, request path, request
106
- # headers and request body.
107
- def to_http_request
108
- return ['POST', request_uri, request_headers, request_body]
109
- end
110
-
111
135
  ##
112
136
  # Processes the HTTP response to the batch request, issuing callbacks.
113
137
  #
114
- # @param [Faraday::Response] response: the HTTP response.
115
- def process_response(response)
138
+ # @api private
139
+ #
140
+ # @param [Faraday::Response] response
141
+ # the HTTP response.
142
+ def process_http_response(response)
116
143
  content_type = find_header('Content-Type', response.headers)
117
144
  boundary = /.*boundary=(.+)/.match(content_type)[1]
118
145
  parts = response.body.split(/--#{Regexp.escape(boundary)}/)
119
146
  parts = parts[1...-1]
120
147
  parts.each do |part|
121
148
  call_response = deserialize_call_response(part)
122
- callback = @callbacks[call_response.call_id]
123
- call = @calls[call_response.call_id]
124
- result = Google::APIClient::Result.new(call, nil, call_response)
149
+ _, call, callback = @calls.assoc(call_response.call_id)
150
+ result = Google::APIClient::Result.new(call, call_response)
125
151
  callback.call(result) if callback
126
152
  end
153
+ Google::APIClient::Result.new(self, response)
127
154
  end
128
155
 
129
- private
156
+ ##
157
+ # Return the request body for the BatchRequest's HTTP request.
158
+ #
159
+ # @api private
160
+ #
161
+ # @return [String]
162
+ # the request body.
163
+ def to_http_request
164
+ if @calls.nil? || @calls.empty?
165
+ raise BatchError, 'Cannot make an empty batch request'
166
+ end
167
+ parts = @calls.map {|(call_id, call, callback)| serialize_call(call_id, call)}
168
+ build_multipart(parts, 'multipart/mixed', BATCH_BOUNDARY)
169
+ super
170
+ end
171
+
172
+
173
+ protected
130
174
 
131
175
  ##
132
176
  # Helper method to find a header from its name, regardless of case.
133
177
  #
134
- # @param [String] name: The name of the header to find.
135
- # @param [Hash] headers: The hash of headers and their values.
178
+ # @api private
179
+ #
180
+ # @param [String] name
181
+ # the name of the header to find.
182
+ # @param [Hash] headers
183
+ # the hash of headers and their values.
136
184
  #
137
- # @return [String] The value of the desired header.
185
+ # @return [String]
186
+ # the value of the desired header.
138
187
  def find_header(name, headers)
139
188
  _, header = headers.detect do |h, v|
140
189
  h.downcase == name.downcase
@@ -145,40 +194,29 @@ module Google
145
194
  ##
146
195
  # Create a new call ID. Uses an auto-incrementing, conflict-avoiding ID.
147
196
  #
148
- # @return [String] the new, unique ID.
197
+ # @api private
198
+ #
199
+ # @return [String]
200
+ # the new, unique ID.
149
201
  def new_id
150
202
  @last_auto_id += 1
151
- while @calls.include?(@last_auto_id)
203
+ while @calls.assoc(@last_auto_id)
152
204
  @last_auto_id += 1
153
205
  end
154
206
  return @last_auto_id.to_s
155
207
  end
156
208
 
157
- ##
158
- # Convert an id to a Content-ID header value.
159
- #
160
- # @param [String] call_id: identifier of individual call.
161
- #
162
- # @return [String]
163
- # A Content-ID header with the call_id encoded into it. A UUID is
164
- # prepended to the value because Content-ID headers are supposed to be
165
- # universally unique.
166
- def id_to_header(call_id)
167
- if @base_id.nil?
168
- # TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
169
- @base_id = UUIDTools::UUID.random_create.to_s
170
- end
171
-
172
- return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
173
- end
174
-
175
209
  ##
176
210
  # Convert a Content-ID header value to an id. Presumes the Content-ID
177
211
  # header conforms to the format that id_to_header() returns.
178
212
  #
179
- # @param [String] header: Content-ID header value.
213
+ # @api private
214
+ #
215
+ # @param [String] header
216
+ # Content-ID header value.
180
217
  #
181
- # @return [String] The extracted ID value.
218
+ # @return [String]
219
+ # The extracted ID value.
182
220
  def header_to_id(header)
183
221
  if !header.start_with?('<') || !header.end_with?('>') ||
184
222
  !header.include?('+')
@@ -189,36 +227,16 @@ module Google
189
227
  return Addressable::URI.unencode(call_id)
190
228
  end
191
229
 
192
- ##
193
- # Convert a single batched call into a string.
194
- #
195
- # @param [Google::APIClient::Reference] call: the call to serialize.
196
- #
197
- # @return [String] The request as a string in application/http format.
198
- def serialize_call(call)
199
- http_request = call.to_request
200
- method = http_request.method.to_s.upcase
201
- path = http_request.path.to_s
202
- status_line = method + " " + path + " HTTP/1.1"
203
- serialized_call = status_line
204
- if http_request.headers
205
- http_request.headers.each do |header, value|
206
- serialized_call << "\r\n%s: %s" % [header, value]
207
- end
208
- end
209
- if http_request.body
210
- serialized_call << "\r\n\r\n"
211
- serialized_call << http_request.body
212
- end
213
- return serialized_call
214
- end
215
-
216
230
  ##
217
231
  # Auxiliary method to split the headers from the body in an HTTP response.
218
232
  #
219
- # @param [String] response: the response to parse.
233
+ # @api private
220
234
  #
221
- # @return [Array<Hash>, String] The headers and the body, separately.
235
+ # @param [String] response
236
+ # the response to parse.
237
+ #
238
+ # @return [Array<Hash>, String]
239
+ # the headers and the body, separately.
222
240
  def split_headers_and_body(response)
223
241
  headers = {}
224
242
  payload = response.lstrip
@@ -239,10 +257,13 @@ module Google
239
257
  ##
240
258
  # Convert a single batched response into a BatchedCallResponse object.
241
259
  #
242
- # @param [Google::APIClient::Reference] response:
260
+ # @api private
261
+ #
262
+ # @param [String] call_response
243
263
  # the request to deserialize.
244
264
  #
245
- # @return [BatchedCallResponse] The parsed and converted response.
265
+ # @return [Google::APIClient::BatchedCallResponse]
266
+ # the parsed and converted response.
246
267
  def deserialize_call_response(call_response)
247
268
  outer_headers, outer_body = split_headers_and_body(call_response)
248
269
  status_line, payload = outer_body.split("\n", 2)
@@ -255,42 +276,49 @@ module Google
255
276
  end
256
277
 
257
278
  ##
258
- # Return the request headers for the BatchRequest's HTTP request.
279
+ # Serialize a single batched call for assembling the multipart message
259
280
  #
260
- # @return [Hash] The HTTP headers.
261
- def request_headers
262
- return {
263
- 'Content-Type' => 'multipart/mixed; boundary=%s' % BATCH_BOUNDARY
264
- }
265
- end
266
-
267
- ##
268
- # Return the request path for the BatchRequest's HTTP request.
281
+ # @api private
269
282
  #
270
- # @return [String] The request path.
271
- def request_uri
272
- if @calls.nil? || @calls.empty?
273
- raise BatchError, 'Cannot make an empty batch request'
283
+ # @param [Google::APIClient::Request] call
284
+ # the call to serialize.
285
+ #
286
+ # @return [Faraday::UploadIO]
287
+ # the serialized request
288
+ def serialize_call(call_id, call)
289
+ method, uri, headers, body = call.to_http_request
290
+ request = "#{method.to_s.upcase} #{Addressable::URI.parse(uri).path} HTTP/1.1"
291
+ headers.each do |header, value|
292
+ request << "\r\n%s: %s" % [header, value]
293
+ end
294
+ if body
295
+ # TODO - CompositeIO if body is a stream
296
+ request << "\r\n\r\n"
297
+ if body.respond_to?(:read)
298
+ request << body.read
299
+ else
300
+ request << body.to_s
301
+ end
274
302
  end
275
- # All APIs have the same batch path, so just get the first one.
276
- return @calls.first[1].api_method.api.batch_path
303
+ Faraday::UploadIO.new(StringIO.new(request), 'application/http', 'ruby-api-request', 'Content-ID' => id_to_header(call_id))
277
304
  end
278
-
305
+
279
306
  ##
280
- # Return the request body for the BatchRequest's HTTP request.
307
+ # Convert an id to a Content-ID header value.
281
308
  #
282
- # @return [String] The request body.
283
- def request_body
284
- body = ""
285
- @order.each do |call_id|
286
- body << "--" + BATCH_BOUNDARY + "\r\n"
287
- body << "Content-Type: application/http\r\n"
288
- body << "Content-ID: %s\r\n\r\n" % id_to_header(call_id)
289
- body << serialize_call(@calls[call_id]) + "\r\n\r\n"
290
- end
291
- body << "--" + BATCH_BOUNDARY + "--"
292
- return body
309
+ # @api private
310
+ #
311
+ # @param [String] call_id
312
+ # identifier of individual call.
313
+ #
314
+ # @return [String]
315
+ # A Content-ID header with the call_id encoded into it. A UUID is
316
+ # prepended to the value because Content-ID headers are supposed to be
317
+ # universally unique.
318
+ def id_to_header(call_id)
319
+ return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
293
320
  end
321
+
294
322
  end
295
323
  end
296
324
  end