blob: b58cf6a867cab908f6070f7b7084da6f43b8c2a6 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright 2007, 2008 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
15import base64
16import cookielib
17import getpass
18import logging
19import md5
20import os
21import random
22import socket
Shawn O. Pearced3c38832008-11-04 10:40:43 -080023import sys
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070024import time
25import urllib
26import urllib2
27import urlparse
28
29from froofle.protobuf.service import RpcChannel
30from froofle.protobuf.service import RpcController
31from need_retry_pb2 import RetryRequestLaterResponse;
32
Shawn O. Pearced3c38832008-11-04 10:40:43 -080033_cookie_jars = {}
34
35def _open_jar(path):
36 auth = False
37
38 if path is None:
39 c = cookielib.CookieJar()
40 else:
41 c = _cookie_jars.get(path)
42 if c is None:
43 c = cookielib.MozillaCookieJar(path)
44
45 if os.path.exists(path):
46 try:
47 c.load()
48 auth = True
49 except (cookielib.LoadError, IOError):
50 pass
51
52 if auth:
53 print >>sys.stderr, \
54 'Loaded authentication cookies from %s' \
55 % path
56 else:
57 os.close(os.open(path, os.O_CREAT, 0600))
58 os.chmod(path, 0600)
59 _cookie_jars[path] = c
60 else:
61 auth = True
62 return c, auth
63
64
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065class ClientLoginError(urllib2.HTTPError):
66 """Raised to indicate an error authenticating with ClientLogin."""
67
68 def __init__(self, url, code, msg, headers, args):
69 urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
70 self.args = args
71 self.reason = args["Error"]
72
73
74class Proxy(object):
75 class _ResultHolder(object):
76 def __call__(self, result):
77 self._result = result
78
79 class _RemoteController(RpcController):
80 def Reset(self):
81 pass
82
83 def Failed(self):
84 pass
85
86 def ErrorText(self):
87 pass
88
89 def StartCancel(self):
90 pass
91
92 def SetFailed(self, reason):
93 raise RuntimeError, reason
94
95 def IsCancelled(self):
96 pass
97
98 def NotifyOnCancel(self, callback):
99 pass
100
101 def __init__(self, stub):
102 self._stub = stub
103
104 def __getattr__(self, key):
105 method = getattr(self._stub, key)
106
107 def call(request):
108 done = self._ResultHolder()
109 method(self._RemoteController(), request, done)
110 return done._result
111
112 return call
113
114
115class HttpRpc(RpcChannel):
116 """Simple protobuf over HTTP POST implementation."""
117
118 def __init__(self, host, auth_function,
119 host_override=None,
120 extra_headers={},
121 cookie_file=None):
122 """Creates a new HttpRpc.
123
124 Args:
125 host: The host to send requests to.
126 auth_function: A function that takes no arguments and returns an
127 (email, password) tuple when called. Will be called if authentication
128 is required.
129 host_override: The host header to send to the server (defaults to host).
130 extra_headers: A dict of extra headers to append to every request.
131 cookie_file: If not None, name of the file in ~/ to save the
132 cookie jar into. Applications are encouraged to set this to
133 '.$appname_cookies' or some otherwise unique name.
134 """
135 self.host = host.lower()
136 self.host_override = host_override
137 self.auth_function = auth_function
138 self.authenticated = False
139 self.extra_headers = extra_headers
140 self.xsrf_token = None
141 if cookie_file is None:
142 self.cookie_file = None
143 else:
144 self.cookie_file = os.path.expanduser("~/%s" % cookie_file)
145 self.opener = self._GetOpener()
146 if self.host_override:
147 logging.info("Server: %s; Host: %s", self.host, self.host_override)
148 else:
149 logging.info("Server: %s", self.host)
150
151 def CallMethod(self, method, controller, request, response_type, done):
152 pat = "application/x-google-protobuf; name=%s"
153
154 url = "/proto/%s/%s" % (method.containing_service.name, method.name)
155 reqbin = request.SerializeToString()
156 reqtyp = pat % request.DESCRIPTOR.full_name
157 reqmd5 = base64.b64encode(md5.new(reqbin).digest())
158
159 start = time.time()
160 while True:
161 t, b = self._Send(url, reqbin, reqtyp, reqmd5)
162 if t == (pat % RetryRequestLaterResponse.DESCRIPTOR.full_name):
163 if time.time() >= (start + 1800):
164 controller.SetFailed("timeout")
165 return
166 s = random.uniform(0.250, 2.000)
167 print "Busy, retrying in %.3f seconds ..." % s
168 time.sleep(s)
169 continue
170
171 if t == (pat % response_type.DESCRIPTOR.full_name):
172 response = response_type()
173 response.ParseFromString(b)
174 done(response)
175 else:
176 controller.SetFailed("Unexpected %s response" % t)
177 break
178
179 def _CreateRequest(self, url, data=None):
180 """Creates a new urllib request."""
181 logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
182 req = urllib2.Request(url, data=data)
183 if self.host_override:
184 req.add_header("Host", self.host_override)
185 for key, value in self.extra_headers.iteritems():
186 req.add_header(key, value)
187 return req
188
189 def _GetAuthToken(self, email, password):
190 """Uses ClientLogin to authenticate the user, returning an auth token.
191
192 Args:
193 email: The user's email address
194 password: The user's password
195
196 Raises:
197 ClientLoginError: If there was an error authenticating with ClientLogin.
198 HTTPError: If there was some other form of HTTP error.
199
200 Returns:
201 The authentication token returned by ClientLogin.
202 """
Shawn O. Pearcebb0ee802008-10-22 13:02:56 -0700203 account_type = 'GOOGLE'
204 if self.host.endswith('.google.com'):
205 account_type = 'HOSTED'
206
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700207 req = self._CreateRequest(
208 url="https://www.google.com/accounts/ClientLogin",
209 data=urllib.urlencode({
210 "Email": email,
211 "Passwd": password,
212 "service": "ah",
213 "source": "gerrit-codereview-client",
Shawn O. Pearcebb0ee802008-10-22 13:02:56 -0700214 "accountType": account_type,
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700215 })
216 )
217 try:
218 response = self.opener.open(req)
219 response_body = response.read()
220 response_dict = dict(x.split("=")
221 for x in response_body.split("\n") if x)
222 return response_dict["Auth"]
223 except urllib2.HTTPError, e:
224 if e.code == 403:
225 body = e.read()
226 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
227 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
228 e.headers, response_dict)
229 else:
230 raise
231
232 def _GetAuthCookie(self, auth_token):
233 """Fetches authentication cookies for an authentication token.
234
235 Args:
236 auth_token: The authentication token returned by ClientLogin.
237
238 Raises:
239 HTTPError: If there was an error fetching the authentication cookies.
240 """
241 # This is a dummy value to allow us to identify when we're successful.
242 continue_location = "http://localhost/"
243 args = {"continue": continue_location, "auth": auth_token}
244 req = self._CreateRequest("http://%s/_ah/login?%s" %
245 (self.host, urllib.urlencode(args)))
246 try:
247 response = self.opener.open(req)
248 except urllib2.HTTPError, e:
249 response = e
250 if (response.code != 302 or
251 response.info()["location"] != continue_location):
252 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
253 response.headers, response.fp)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700254
255 def _GetXsrfToken(self):
256 """Fetches /proto/_token for use in X-XSRF-Token HTTP header.
257
258 Raises:
259 HTTPError: If there was an error fetching a new token.
260 """
261 tries = 0
262 while True:
263 url = "http://%s/proto/_token" % self.host
264 req = self._CreateRequest(url)
265 try:
266 response = self.opener.open(req)
267 self.xsrf_token = response.read()
268 return
269 except urllib2.HTTPError, e:
270 if tries > 3:
271 raise
272 elif e.code == 401:
273 self._Authenticate()
274 else:
275 raise
276
277 def _Authenticate(self):
278 """Authenticates the user.
279
280 The authentication process works as follows:
281 1) We get a username and password from the user
282 2) We use ClientLogin to obtain an AUTH token for the user
283 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
284 3) We pass the auth token to /_ah/login on the server to obtain an
285 authentication cookie. If login was successful, it tries to redirect
286 us to the URL we provided.
287
288 If we attempt to access the upload API without first obtaining an
289 authentication cookie, it returns a 401 response and directs us to
290 authenticate ourselves with ClientLogin.
291 """
Shawn O. Pearcebb0ee802008-10-22 13:02:56 -0700292 attempts = 0
293 while True:
294 attempts += 1
295 try:
296 cred = self.auth_function()
297 auth_token = self._GetAuthToken(cred[0], cred[1])
298 except ClientLoginError:
299 if attempts < 3:
300 continue
301 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700302 self._GetAuthCookie(auth_token)
Shawn O. Pearcebb0ee802008-10-22 13:02:56 -0700303 self.authenticated = True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700304 if self.cookie_file is not None:
Shawn O. Pearced3c38832008-11-04 10:40:43 -0800305 print >>sys.stderr, \
306 'Saving authentication cookies to %s' \
307 % self.cookie_file
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700308 self.cookie_jar.save()
309 return
310
311 def _Send(self, request_path, payload, content_type, content_md5):
312 """Sends an RPC and returns the response.
313
314 Args:
315 request_path: The path to send the request to, eg /api/appversion/create.
316 payload: The body of the request, or None to send an empty request.
317 content_type: The Content-Type header to use.
318 content_md5: The Content-MD5 header to use.
319
320 Returns:
321 The content type, as a string.
322 The response body, as a string.
323 """
324 if not self.authenticated:
325 self._Authenticate()
326 if not self.xsrf_token:
327 self._GetXsrfToken()
328
329 old_timeout = socket.getdefaulttimeout()
330 socket.setdefaulttimeout(None)
331 try:
332 tries = 0
333 while True:
334 tries += 1
335 url = "http://%s%s" % (self.host, request_path)
336 req = self._CreateRequest(url=url, data=payload)
337 req.add_header("Content-Type", content_type)
338 req.add_header("Content-MD5", content_md5)
339 req.add_header("X-XSRF-Token", self.xsrf_token)
340 try:
341 f = self.opener.open(req)
342 hdr = f.info()
343 type = hdr.getheader('Content-Type',
344 'application/octet-stream')
345 response = f.read()
346 f.close()
347 return type, response
348 except urllib2.HTTPError, e:
349 if tries > 3:
350 raise
351 elif e.code == 401:
352 self._Authenticate()
353 elif e.code == 403:
354 if not hasattr(e, 'read'):
355 e.read = lambda self: ''
356 raise RuntimeError, '403\nxsrf: %s\n%s' \
357 % (self.xsrf_token, e.read())
358 else:
359 raise
360 finally:
361 socket.setdefaulttimeout(old_timeout)
362
363 def _GetOpener(self):
364 """Returns an OpenerDirector that supports cookies and ignores redirects.
365
366 Returns:
367 A urllib2.OpenerDirector object.
368 """
369 opener = urllib2.OpenerDirector()
370 opener.add_handler(urllib2.ProxyHandler())
371 opener.add_handler(urllib2.UnknownHandler())
372 opener.add_handler(urllib2.HTTPHandler())
373 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
374 opener.add_handler(urllib2.HTTPSHandler())
375 opener.add_handler(urllib2.HTTPErrorProcessor())
Shawn O. Pearced3c38832008-11-04 10:40:43 -0800376
377 self.cookie_jar, \
378 self.authenticated = _open_jar(self.cookie_file)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700379 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
380 return opener