google-api-client 0.4.7 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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