| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- # Copyright 2007, 2008 Google Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- import base64
- import cookielib
- import getpass
- import logging
- import md5
- import os
- import random
- import socket
- import sys
- import time
- import urllib
- import urllib2
- import urlparse
- from froofle.protobuf.service import RpcChannel
- from froofle.protobuf.service import RpcController
- from need_retry_pb2 import RetryRequestLaterResponse;
- _cookie_jars = {}
- def _open_jar(path):
- auth = False
- if path is None:
- c = cookielib.CookieJar()
- else:
- c = _cookie_jars.get(path)
- if c is None:
- c = cookielib.MozillaCookieJar(path)
- if os.path.exists(path):
- try:
- c.load()
- auth = True
- except (cookielib.LoadError, IOError):
- pass
- if auth:
- print >>sys.stderr, \
- 'Loaded authentication cookies from %s' \
- % path
- else:
- os.close(os.open(path, os.O_CREAT, 0600))
- os.chmod(path, 0600)
- _cookie_jars[path] = c
- else:
- auth = True
- return c, auth
- class ClientLoginError(urllib2.HTTPError):
- """Raised to indicate an error authenticating with ClientLogin."""
- def __init__(self, url, code, msg, headers, args):
- urllib2.HTTPError.__init__(self, url, code, msg, headers, None)
- self.args = args
- self.reason = args["Error"]
- class Proxy(object):
- class _ResultHolder(object):
- def __call__(self, result):
- self._result = result
- class _RemoteController(RpcController):
- def Reset(self):
- pass
-
- def Failed(self):
- pass
-
- def ErrorText(self):
- pass
-
- def StartCancel(self):
- pass
-
- def SetFailed(self, reason):
- raise RuntimeError, reason
-
- def IsCancelled(self):
- pass
-
- def NotifyOnCancel(self, callback):
- pass
-
- def __init__(self, stub):
- self._stub = stub
- def __getattr__(self, key):
- method = getattr(self._stub, key)
- def call(request):
- done = self._ResultHolder()
- method(self._RemoteController(), request, done)
- return done._result
- return call
- class HttpRpc(RpcChannel):
- """Simple protobuf over HTTP POST implementation."""
- def __init__(self, host, auth_function,
- host_override=None,
- extra_headers={},
- cookie_file=None):
- """Creates a new HttpRpc.
- Args:
- host: The host to send requests to.
- auth_function: A function that takes no arguments and returns an
- (email, password) tuple when called. Will be called if authentication
- is required.
- host_override: The host header to send to the server (defaults to host).
- extra_headers: A dict of extra headers to append to every request.
- cookie_file: If not None, name of the file in ~/ to save the
- cookie jar into. Applications are encouraged to set this to
- '.$appname_cookies' or some otherwise unique name.
- """
- self.host = host.lower()
- self.host_override = host_override
- self.auth_function = auth_function
- self.authenticated = False
- self.extra_headers = extra_headers
- self.xsrf_token = None
- if cookie_file is None:
- self.cookie_file = None
- else:
- self.cookie_file = os.path.expanduser("~/%s" % cookie_file)
- self.opener = self._GetOpener()
- if self.host_override:
- logging.info("Server: %s; Host: %s", self.host, self.host_override)
- else:
- logging.info("Server: %s", self.host)
- def CallMethod(self, method, controller, request, response_type, done):
- pat = "application/x-google-protobuf; name=%s"
- url = "/proto/%s/%s" % (method.containing_service.name, method.name)
- reqbin = request.SerializeToString()
- reqtyp = pat % request.DESCRIPTOR.full_name
- reqmd5 = base64.b64encode(md5.new(reqbin).digest())
- start = time.time()
- while True:
- t, b = self._Send(url, reqbin, reqtyp, reqmd5)
- if t == (pat % RetryRequestLaterResponse.DESCRIPTOR.full_name):
- if time.time() >= (start + 1800):
- controller.SetFailed("timeout")
- return
- s = random.uniform(0.250, 2.000)
- print "Busy, retrying in %.3f seconds ..." % s
- time.sleep(s)
- continue
- if t == (pat % response_type.DESCRIPTOR.full_name):
- response = response_type()
- response.ParseFromString(b)
- done(response)
- else:
- controller.SetFailed("Unexpected %s response" % t)
- break
- def _CreateRequest(self, url, data=None):
- """Creates a new urllib request."""
- logging.debug("Creating request for: '%s' with payload:\n%s", url, data)
- req = urllib2.Request(url, data=data)
- if self.host_override:
- req.add_header("Host", self.host_override)
- for key, value in self.extra_headers.iteritems():
- req.add_header(key, value)
- return req
- def _GetAuthToken(self, email, password):
- """Uses ClientLogin to authenticate the user, returning an auth token.
- Args:
- email: The user's email address
- password: The user's password
- Raises:
- ClientLoginError: If there was an error authenticating with ClientLogin.
- HTTPError: If there was some other form of HTTP error.
- Returns:
- The authentication token returned by ClientLogin.
- """
- account_type = 'GOOGLE'
- if self.host.endswith('.google.com'):
- account_type = 'HOSTED'
- req = self._CreateRequest(
- url="https://www.google.com/accounts/ClientLogin",
- data=urllib.urlencode({
- "Email": email,
- "Passwd": password,
- "service": "ah",
- "source": "gerrit-codereview-client",
- "accountType": account_type,
- })
- )
- try:
- response = self.opener.open(req)
- response_body = response.read()
- response_dict = dict(x.split("=")
- for x in response_body.split("\n") if x)
- return response_dict["Auth"]
- except urllib2.HTTPError, e:
- if e.code == 403:
- body = e.read()
- response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
- raise ClientLoginError(req.get_full_url(), e.code, e.msg,
- e.headers, response_dict)
- else:
- raise
- def _GetAuthCookie(self, auth_token):
- """Fetches authentication cookies for an authentication token.
- Args:
- auth_token: The authentication token returned by ClientLogin.
- Raises:
- HTTPError: If there was an error fetching the authentication cookies.
- """
- # This is a dummy value to allow us to identify when we're successful.
- continue_location = "http://localhost/"
- args = {"continue": continue_location, "auth": auth_token}
- req = self._CreateRequest("http://%s/_ah/login?%s" %
- (self.host, urllib.urlencode(args)))
- try:
- response = self.opener.open(req)
- except urllib2.HTTPError, e:
- response = e
- if (response.code != 302 or
- response.info()["location"] != continue_location):
- raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
- response.headers, response.fp)
- def _GetXsrfToken(self):
- """Fetches /proto/_token for use in X-XSRF-Token HTTP header.
- Raises:
- HTTPError: If there was an error fetching a new token.
- """
- tries = 0
- while True:
- url = "http://%s/proto/_token" % self.host
- req = self._CreateRequest(url)
- try:
- response = self.opener.open(req)
- self.xsrf_token = response.read()
- return
- except urllib2.HTTPError, e:
- if tries > 3:
- raise
- elif e.code == 401:
- self._Authenticate()
- else:
- raise
- def _Authenticate(self):
- """Authenticates the user.
- The authentication process works as follows:
- 1) We get a username and password from the user
- 2) We use ClientLogin to obtain an AUTH token for the user
- (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
- 3) We pass the auth token to /_ah/login on the server to obtain an
- authentication cookie. If login was successful, it tries to redirect
- us to the URL we provided.
- If we attempt to access the upload API without first obtaining an
- authentication cookie, it returns a 401 response and directs us to
- authenticate ourselves with ClientLogin.
- """
- attempts = 0
- while True:
- attempts += 1
- try:
- cred = self.auth_function()
- auth_token = self._GetAuthToken(cred[0], cred[1])
- except ClientLoginError:
- if attempts < 3:
- continue
- raise
- self._GetAuthCookie(auth_token)
- self.authenticated = True
- if self.cookie_file is not None:
- print >>sys.stderr, \
- 'Saving authentication cookies to %s' \
- % self.cookie_file
- self.cookie_jar.save()
- return
- def _Send(self, request_path, payload, content_type, content_md5):
- """Sends an RPC and returns the response.
- Args:
- request_path: The path to send the request to, eg /api/appversion/create.
- payload: The body of the request, or None to send an empty request.
- content_type: The Content-Type header to use.
- content_md5: The Content-MD5 header to use.
- Returns:
- The content type, as a string.
- The response body, as a string.
- """
- if not self.authenticated:
- self._Authenticate()
- if not self.xsrf_token:
- self._GetXsrfToken()
- old_timeout = socket.getdefaulttimeout()
- socket.setdefaulttimeout(None)
- try:
- tries = 0
- while True:
- tries += 1
- url = "http://%s%s" % (self.host, request_path)
- req = self._CreateRequest(url=url, data=payload)
- req.add_header("Content-Type", content_type)
- req.add_header("Content-MD5", content_md5)
- req.add_header("X-XSRF-Token", self.xsrf_token)
- try:
- f = self.opener.open(req)
- hdr = f.info()
- type = hdr.getheader('Content-Type',
- 'application/octet-stream')
- response = f.read()
- f.close()
- return type, response
- except urllib2.HTTPError, e:
- if tries > 3:
- raise
- elif e.code == 401:
- self._Authenticate()
- elif e.code == 403:
- if not hasattr(e, 'read'):
- e.read = lambda self: ''
- raise RuntimeError, '403\nxsrf: %s\n%s' \
- % (self.xsrf_token, e.read())
- else:
- raise
- finally:
- socket.setdefaulttimeout(old_timeout)
- def _GetOpener(self):
- """Returns an OpenerDirector that supports cookies and ignores redirects.
- Returns:
- A urllib2.OpenerDirector object.
- """
- opener = urllib2.OpenerDirector()
- opener.add_handler(urllib2.ProxyHandler())
- opener.add_handler(urllib2.UnknownHandler())
- opener.add_handler(urllib2.HTTPHandler())
- opener.add_handler(urllib2.HTTPDefaultErrorHandler())
- opener.add_handler(urllib2.HTTPSHandler())
- opener.add_handler(urllib2.HTTPErrorProcessor())
- self.cookie_jar, \
- self.authenticated = _open_jar(self.cookie_file)
- opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
- return opener
|