The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 1 | # 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 | |
| 15 | import base64 |
| 16 | import cookielib |
| 17 | import getpass |
| 18 | import logging |
| 19 | import md5 |
| 20 | import os |
| 21 | import random |
| 22 | import socket |
Shawn O. Pearce | d3c3883 | 2008-11-04 10:40:43 -0800 | [diff] [blame^] | 23 | import sys |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 24 | import time |
| 25 | import urllib |
| 26 | import urllib2 |
| 27 | import urlparse |
| 28 | |
| 29 | from froofle.protobuf.service import RpcChannel |
| 30 | from froofle.protobuf.service import RpcController |
| 31 | from need_retry_pb2 import RetryRequestLaterResponse; |
| 32 | |
Shawn O. Pearce | d3c3883 | 2008-11-04 10:40:43 -0800 | [diff] [blame^] | 33 | _cookie_jars = {} |
| 34 | |
| 35 | def _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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 65 | class 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 | |
| 74 | class 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 | |
| 115 | class 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. Pearce | bb0ee80 | 2008-10-22 13:02:56 -0700 | [diff] [blame] | 203 | account_type = 'GOOGLE' |
| 204 | if self.host.endswith('.google.com'): |
| 205 | account_type = 'HOSTED' |
| 206 | |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 207 | 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. Pearce | bb0ee80 | 2008-10-22 13:02:56 -0700 | [diff] [blame] | 214 | "accountType": account_type, |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 215 | }) |
| 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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 254 | |
| 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. Pearce | bb0ee80 | 2008-10-22 13:02:56 -0700 | [diff] [blame] | 292 | 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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 302 | self._GetAuthCookie(auth_token) |
Shawn O. Pearce | bb0ee80 | 2008-10-22 13:02:56 -0700 | [diff] [blame] | 303 | self.authenticated = True |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 304 | if self.cookie_file is not None: |
Shawn O. Pearce | d3c3883 | 2008-11-04 10:40:43 -0800 | [diff] [blame^] | 305 | print >>sys.stderr, \ |
| 306 | 'Saving authentication cookies to %s' \ |
| 307 | % self.cookie_file |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 308 | 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. Pearce | d3c3883 | 2008-11-04 10:40:43 -0800 | [diff] [blame^] | 376 | |
| 377 | self.cookie_jar, \ |
| 378 | self.authenticated = _open_jar(self.cookie_file) |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 379 | opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) |
| 380 | return opener |