blob: e11beff04b04702194037b28cc22e933deac6f84 [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 """
170 req = self._CreateRequest(
171 url="https://www.google.com/accounts/ClientLogin",
172 data=urllib.urlencode({
173 "Email": email,
174 "Passwd": password,
175 "service": "ah",
176 "source": "gerrit-codereview-client",
177 "accountType": "HOSTED_OR_GOOGLE",
178 })
179 )
180 try:
181 response = self.opener.open(req)
182 response_body = response.read()
183 response_dict = dict(x.split("=")
184 for x in response_body.split("\n") if x)
185 return response_dict["Auth"]
186 except urllib2.HTTPError, e:
187 if e.code == 403:
188 body = e.read()
189 response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
190 raise ClientLoginError(req.get_full_url(), e.code, e.msg,
191 e.headers, response_dict)
192 else:
193 raise
194
195 def _GetAuthCookie(self, auth_token):
196 """Fetches authentication cookies for an authentication token.
197
198 Args:
199 auth_token: The authentication token returned by ClientLogin.
200
201 Raises:
202 HTTPError: If there was an error fetching the authentication cookies.
203 """
204 # This is a dummy value to allow us to identify when we're successful.
205 continue_location = "http://localhost/"
206 args = {"continue": continue_location, "auth": auth_token}
207 req = self._CreateRequest("http://%s/_ah/login?%s" %
208 (self.host, urllib.urlencode(args)))
209 try:
210 response = self.opener.open(req)
211 except urllib2.HTTPError, e:
212 response = e
213 if (response.code != 302 or
214 response.info()["location"] != continue_location):
215 raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
216 response.headers, response.fp)
217 self.authenticated = True
218
219 def _GetXsrfToken(self):
220 """Fetches /proto/_token for use in X-XSRF-Token HTTP header.
221
222 Raises:
223 HTTPError: If there was an error fetching a new token.
224 """
225 tries = 0
226 while True:
227 url = "http://%s/proto/_token" % self.host
228 req = self._CreateRequest(url)
229 try:
230 response = self.opener.open(req)
231 self.xsrf_token = response.read()
232 return
233 except urllib2.HTTPError, e:
234 if tries > 3:
235 raise
236 elif e.code == 401:
237 self._Authenticate()
238 else:
239 raise
240
241 def _Authenticate(self):
242 """Authenticates the user.
243
244 The authentication process works as follows:
245 1) We get a username and password from the user
246 2) We use ClientLogin to obtain an AUTH token for the user
247 (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
248 3) We pass the auth token to /_ah/login on the server to obtain an
249 authentication cookie. If login was successful, it tries to redirect
250 us to the URL we provided.
251
252 If we attempt to access the upload API without first obtaining an
253 authentication cookie, it returns a 401 response and directs us to
254 authenticate ourselves with ClientLogin.
255 """
256 for i in range(3):
257 credentials = self.auth_function()
258 auth_token = self._GetAuthToken(credentials[0], credentials[1])
259 self._GetAuthCookie(auth_token)
260 if self.cookie_file is not None:
261 self.cookie_jar.save()
262 return
263
264 def _Send(self, request_path, payload, content_type, content_md5):
265 """Sends an RPC and returns the response.
266
267 Args:
268 request_path: The path to send the request to, eg /api/appversion/create.
269 payload: The body of the request, or None to send an empty request.
270 content_type: The Content-Type header to use.
271 content_md5: The Content-MD5 header to use.
272
273 Returns:
274 The content type, as a string.
275 The response body, as a string.
276 """
277 if not self.authenticated:
278 self._Authenticate()
279 if not self.xsrf_token:
280 self._GetXsrfToken()
281
282 old_timeout = socket.getdefaulttimeout()
283 socket.setdefaulttimeout(None)
284 try:
285 tries = 0
286 while True:
287 tries += 1
288 url = "http://%s%s" % (self.host, request_path)
289 req = self._CreateRequest(url=url, data=payload)
290 req.add_header("Content-Type", content_type)
291 req.add_header("Content-MD5", content_md5)
292 req.add_header("X-XSRF-Token", self.xsrf_token)
293 try:
294 f = self.opener.open(req)
295 hdr = f.info()
296 type = hdr.getheader('Content-Type',
297 'application/octet-stream')
298 response = f.read()
299 f.close()
300 return type, response
301 except urllib2.HTTPError, e:
302 if tries > 3:
303 raise
304 elif e.code == 401:
305 self._Authenticate()
306 elif e.code == 403:
307 if not hasattr(e, 'read'):
308 e.read = lambda self: ''
309 raise RuntimeError, '403\nxsrf: %s\n%s' \
310 % (self.xsrf_token, e.read())
311 else:
312 raise
313 finally:
314 socket.setdefaulttimeout(old_timeout)
315
316 def _GetOpener(self):
317 """Returns an OpenerDirector that supports cookies and ignores redirects.
318
319 Returns:
320 A urllib2.OpenerDirector object.
321 """
322 opener = urllib2.OpenerDirector()
323 opener.add_handler(urllib2.ProxyHandler())
324 opener.add_handler(urllib2.UnknownHandler())
325 opener.add_handler(urllib2.HTTPHandler())
326 opener.add_handler(urllib2.HTTPDefaultErrorHandler())
327 opener.add_handler(urllib2.HTTPSHandler())
328 opener.add_handler(urllib2.HTTPErrorProcessor())
329 if self.cookie_file is not None:
330 self.cookie_jar = cookielib.MozillaCookieJar(self.cookie_file)
331 if os.path.exists(self.cookie_file):
332 try:
333 self.cookie_jar.load()
334 self.authenticated = True
335 except (cookielib.LoadError, IOError):
336 # Failed to load cookies - just ignore them.
337 pass
338 else:
339 # Create an empty cookie file with mode 600
340 fd = os.open(self.cookie_file, os.O_CREAT, 0600)
341 os.close(fd)
342 # Always chmod the cookie file
343 os.chmod(self.cookie_file, 0600)
344 else:
345 # Don't save cookies across runs of update.py.
346 self.cookie_jar = cookielib.CookieJar()
347 opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
348 return opener
349