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