thirdparty/google_appengine/google/appengine/tools/appengine_rpc.py
changeset 828 f5fd65cc3bf3
child 2273 e4cb9c53db3e
equal deleted inserted replaced
827:88c186556a80 828:f5fd65cc3bf3
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 """Tool for performing authenticated RPCs against App Engine."""
       
    19 
       
    20 
       
    21 import cookielib
       
    22 import logging
       
    23 import os
       
    24 import re
       
    25 import socket
       
    26 import sys
       
    27 import urllib
       
    28 import urllib2
       
    29 
       
    30 
       
    31 https_handler = urllib2.HTTPSHandler
       
    32 uses_cert_verification = False
       
    33 certpath = os.path.join(os.path.dirname(__file__), "cacerts.txt")
       
    34 cert_file_available = os.path.exists(certpath)
       
    35 try:
       
    36   import https_wrapper
       
    37   if cert_file_available:
       
    38     https_handler = lambda: https_wrapper.CertValidatingHTTPSHandler(
       
    39         ca_certs=certpath)
       
    40     uses_cert_verification = True
       
    41 except ImportError:
       
    42   pass
       
    43 
       
    44 
       
    45 def GetPlatformToken(os_module=os, sys_module=sys, platform=sys.platform):
       
    46   """Returns a 'User-agent' token for the host system platform.
       
    47 
       
    48   Args:
       
    49     os_module, sys_module, platform: Used for testing.
       
    50 
       
    51   Returns:
       
    52     String containing the platform token for the host system.
       
    53   """
       
    54   if hasattr(sys_module, "getwindowsversion"):
       
    55     windows_version = sys_module.getwindowsversion()
       
    56     version_info = ".".join(str(i) for i in windows_version[:4])
       
    57     return platform + "/" + version_info
       
    58   elif hasattr(os_module, "uname"):
       
    59     uname = os_module.uname()
       
    60     return "%s/%s" % (uname[0], uname[2])
       
    61   else:
       
    62     return "unknown"
       
    63 
       
    64 
       
    65 class ClientLoginError(urllib2.HTTPError):
       
    66   """Raised to indicate there was 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 AbstractRpcServer(object):
       
    75   """Provides a common interface for a simple RPC server."""
       
    76 
       
    77   def __init__(self, host, auth_function, user_agent, source,
       
    78                host_override=None, extra_headers=None, save_cookies=False,
       
    79                auth_tries=3, account_type=None):
       
    80     """Creates a new HttpRpcServer.
       
    81 
       
    82     Args:
       
    83       host: The host to send requests to.
       
    84       auth_function: A function that takes no arguments and returns an
       
    85         (email, password) tuple when called. Will be called if authentication
       
    86         is required.
       
    87       user_agent: The user-agent string to send to the server. Specify None to
       
    88         omit the user-agent header.
       
    89       source: The source to specify in authentication requests.
       
    90       host_override: The host header to send to the server (defaults to host).
       
    91       extra_headers: A dict of extra headers to append to every request. Values
       
    92         supplied here will override other default headers that are supplied.
       
    93       save_cookies: If True, save the authentication cookies to local disk.
       
    94         If False, use an in-memory cookiejar instead.  Subclasses must
       
    95         implement this functionality.  Defaults to False.
       
    96       auth_tries: The number of times to attempt auth_function before failing.
       
    97       account_type: One of GOOGLE, HOSTED_OR_GOOGLE, or None for automatic.
       
    98     """
       
    99     self.host = host
       
   100     self.host_override = host_override
       
   101     self.auth_function = auth_function
       
   102     self.source = source
       
   103     self.authenticated = False
       
   104     self.auth_tries = auth_tries
       
   105 
       
   106     self.account_type = account_type
       
   107 
       
   108     self.extra_headers = {}
       
   109     if user_agent:
       
   110       self.extra_headers["User-Agent"] = user_agent
       
   111     if extra_headers:
       
   112       self.extra_headers.update(extra_headers)
       
   113 
       
   114     self.save_cookies = save_cookies
       
   115     self.cookie_jar = cookielib.MozillaCookieJar()
       
   116     self.opener = self._GetOpener()
       
   117     if self.host_override:
       
   118       logging.info("Server: %s; Host: %s", self.host, self.host_override)
       
   119     else:
       
   120       logging.info("Server: %s", self.host)
       
   121 
       
   122     if ((self.host_override and self.host_override == "localhost") or
       
   123         self.host == "localhost" or self.host.startswith("localhost:")):
       
   124       self._DevAppServerAuthenticate()
       
   125 
       
   126   def _GetOpener(self):
       
   127     """Returns an OpenerDirector for making HTTP requests.
       
   128 
       
   129     Returns:
       
   130       A urllib2.OpenerDirector object.
       
   131     """
       
   132     raise NotImplemented()
       
   133 
       
   134   def _CreateRequest(self, url, data=None):
       
   135     """Creates a new urllib request."""
       
   136     req = urllib2.Request(url, data=data)
       
   137     if self.host_override:
       
   138       req.add_header("Host", self.host_override)
       
   139     for key, value in self.extra_headers.iteritems():
       
   140       req.add_header(key, value)
       
   141     return req
       
   142 
       
   143   def _GetAuthToken(self, email, password):
       
   144     """Uses ClientLogin to authenticate the user, returning an auth token.
       
   145 
       
   146     Args:
       
   147       email:    The user's email address
       
   148       password: The user's password
       
   149 
       
   150     Raises:
       
   151       ClientLoginError: If there was an error authenticating with ClientLogin.
       
   152       HTTPError: If there was some other form of HTTP error.
       
   153 
       
   154     Returns:
       
   155       The authentication token returned by ClientLogin.
       
   156     """
       
   157     account_type = self.account_type
       
   158     if not account_type:
       
   159       if (self.host.split(':')[0].endswith(".google.com")
       
   160           or (self.host_override
       
   161               and self.host_override.split(':')[0].endswith(".google.com"))):
       
   162         account_type = "HOSTED_OR_GOOGLE"
       
   163       else:
       
   164         account_type = "GOOGLE"
       
   165     data = {
       
   166         "Email": email,
       
   167         "Passwd": password,
       
   168         "service": "ah",
       
   169         "source": self.source,
       
   170         "accountType": account_type
       
   171     }
       
   172 
       
   173     req = self._CreateRequest(
       
   174         url="https://www.google.com/accounts/ClientLogin",
       
   175         data=urllib.urlencode(data))
       
   176     try:
       
   177       response = self.opener.open(req)
       
   178       response_body = response.read()
       
   179       response_dict = dict(x.split("=")
       
   180                            for x in response_body.split("\n") if x)
       
   181       return response_dict["Auth"]
       
   182     except urllib2.HTTPError, e:
       
   183       if e.code == 403:
       
   184         body = e.read()
       
   185         response_dict = dict(x.split("=", 1) for x in body.split("\n") if x)
       
   186         raise ClientLoginError(req.get_full_url(), e.code, e.msg,
       
   187                                e.headers, response_dict)
       
   188       else:
       
   189         raise
       
   190 
       
   191   def _GetAuthCookie(self, auth_token):
       
   192     """Fetches authentication cookies for an authentication token.
       
   193 
       
   194     Args:
       
   195       auth_token: The authentication token returned by ClientLogin.
       
   196 
       
   197     Raises:
       
   198       HTTPError: If there was an error fetching the authentication cookies.
       
   199     """
       
   200     continue_location = "http://localhost/"
       
   201     args = {"continue": continue_location, "auth": auth_token}
       
   202     login_path = os.environ.get("APPCFG_LOGIN_PATH", "/_ah")
       
   203     req = self._CreateRequest("http://%s%s/login?%s" %
       
   204                               (self.host, login_path, urllib.urlencode(args)))
       
   205     try:
       
   206       response = self.opener.open(req)
       
   207     except urllib2.HTTPError, e:
       
   208       response = e
       
   209     if (response.code != 302 or
       
   210         response.info()["location"] != continue_location):
       
   211       raise urllib2.HTTPError(req.get_full_url(), response.code, response.msg,
       
   212                               response.headers, response.fp)
       
   213     self.authenticated = True
       
   214 
       
   215   def _Authenticate(self):
       
   216     """Authenticates the user.
       
   217 
       
   218     The authentication process works as follows:
       
   219      1) We get a username and password from the user
       
   220      2) We use ClientLogin to obtain an AUTH token for the user
       
   221         (see http://code.google.com/apis/accounts/AuthForInstalledApps.html).
       
   222      3) We pass the auth token to /_ah/login on the server to obtain an
       
   223         authentication cookie. If login was successful, it tries to redirect
       
   224         us to the URL we provided.
       
   225 
       
   226     If we attempt to access the upload API without first obtaining an
       
   227     authentication cookie, it returns a 401 response and directs us to
       
   228     authenticate ourselves with ClientLogin.
       
   229     """
       
   230     for unused_i in range(self.auth_tries):
       
   231       credentials = self.auth_function()
       
   232       try:
       
   233         auth_token = self._GetAuthToken(credentials[0], credentials[1])
       
   234       except ClientLoginError, e:
       
   235         if e.reason == "BadAuthentication":
       
   236           print >>sys.stderr, "Invalid username or password."
       
   237           continue
       
   238         if e.reason == "CaptchaRequired":
       
   239           print >>sys.stderr, (
       
   240               "Please go to\n"
       
   241               "https://www.google.com/accounts/DisplayUnlockCaptcha\n"
       
   242               "and verify you are a human.  Then try again.")
       
   243           break
       
   244         if e.reason == "NotVerified":
       
   245           print >>sys.stderr, "Account not verified."
       
   246           break
       
   247         if e.reason == "TermsNotAgreed":
       
   248           print >>sys.stderr, "User has not agreed to TOS."
       
   249           break
       
   250         if e.reason == "AccountDeleted":
       
   251           print >>sys.stderr, "The user account has been deleted."
       
   252           break
       
   253         if e.reason == "AccountDisabled":
       
   254           print >>sys.stderr, "The user account has been disabled."
       
   255           break
       
   256         if e.reason == "ServiceDisabled":
       
   257           print >>sys.stderr, ("The user's access to the service has been "
       
   258                                "disabled.")
       
   259           break
       
   260         if e.reason == "ServiceUnavailable":
       
   261           print >>sys.stderr, "The service is not available; try again later."
       
   262           break
       
   263         raise
       
   264       self._GetAuthCookie(auth_token)
       
   265       return
       
   266 
       
   267   def _DevAppServerAuthenticate(self):
       
   268     """Authenticates the user on the dev_appserver."""
       
   269     credentials = self.auth_function()
       
   270     self.extra_headers["Cookie"] = ('dev_appserver_login="%s:True"; Path=/;'  %
       
   271                                     (credentials[0],))
       
   272 
       
   273   def Send(self, request_path, payload="",
       
   274            content_type="application/octet-stream",
       
   275            timeout=None,
       
   276            **kwargs):
       
   277     """Sends an RPC and returns the response.
       
   278 
       
   279     Args:
       
   280       request_path: The path to send the request to, eg /api/appversion/create.
       
   281       payload: The body of the request, or None to send an empty request.
       
   282       content_type: The Content-Type header to use.
       
   283       timeout: timeout in seconds; default None i.e. no timeout.
       
   284         (Note: for large requests on OS X, the timeout doesn't work right.)
       
   285       kwargs: Any keyword arguments are converted into query string parameters.
       
   286 
       
   287     Returns:
       
   288       The response body, as a string.
       
   289     """
       
   290     old_timeout = socket.getdefaulttimeout()
       
   291     socket.setdefaulttimeout(timeout)
       
   292     try:
       
   293       tries = 0
       
   294       while True:
       
   295         tries += 1
       
   296         args = dict(kwargs)
       
   297         url = "http://%s%s?%s" % (self.host, request_path,
       
   298                                   urllib.urlencode(args))
       
   299         req = self._CreateRequest(url=url, data=payload)
       
   300         req.add_header("Content-Type", content_type)
       
   301         req.add_header("X-appcfg-api-version", "1")
       
   302         try:
       
   303           f = self.opener.open(req)
       
   304           response = f.read()
       
   305           f.close()
       
   306           return response
       
   307         except urllib2.HTTPError, e:
       
   308           logging.debug("Got http error, this is try #%s" % tries)
       
   309           if tries > self.auth_tries:
       
   310             raise
       
   311           elif e.code == 401:
       
   312             self._Authenticate()
       
   313           elif e.code >= 500 and e.code < 600:
       
   314             continue
       
   315           elif e.code == 302:
       
   316             loc = e.info()["location"]
       
   317             logging.debug("Got 302 redirect. Location: %s" % loc)
       
   318             if loc.startswith("https://www.google.com/accounts/ServiceLogin"):
       
   319               self._Authenticate()
       
   320             elif re.match(r"https://www.google.com/a/[a-z0-9.-]+/ServiceLogin",
       
   321                           loc):
       
   322               self.account_type = "HOSTED"
       
   323               self._Authenticate()
       
   324             elif loc.startswith("http://%s/_ah/login" % (self.host,)):
       
   325               self._DevAppServerAuthenticate()
       
   326           else:
       
   327             raise
       
   328     finally:
       
   329       socket.setdefaulttimeout(old_timeout)
       
   330 
       
   331 
       
   332 class HttpRpcServer(AbstractRpcServer):
       
   333   """Provides a simplified RPC-style interface for HTTP requests."""
       
   334 
       
   335   DEFAULT_COOKIE_FILE_PATH = "~/.appcfg_cookies"
       
   336 
       
   337   def _Authenticate(self):
       
   338     """Save the cookie jar after authentication."""
       
   339     if cert_file_available and not uses_cert_verification:
       
   340       logging.warn("ssl module not found. Without this the identity of the "
       
   341                    "remote host cannot be verified, and connections are NOT "
       
   342                    "secure. To fix this, please install the ssl module from "
       
   343                    "http://pypi.python.org/pypi/ssl")
       
   344     super(HttpRpcServer, self)._Authenticate()
       
   345     if self.cookie_jar.filename is not None and self.save_cookies:
       
   346       logging.info("Saving authentication cookies to %s" %
       
   347                    self.cookie_jar.filename)
       
   348       self.cookie_jar.save()
       
   349 
       
   350   def _GetOpener(self):
       
   351     """Returns an OpenerDirector that supports cookies and ignores redirects.
       
   352 
       
   353     Returns:
       
   354       A urllib2.OpenerDirector object.
       
   355     """
       
   356     opener = urllib2.OpenerDirector()
       
   357     opener.add_handler(urllib2.ProxyHandler())
       
   358     opener.add_handler(urllib2.UnknownHandler())
       
   359     opener.add_handler(urllib2.HTTPHandler())
       
   360     opener.add_handler(urllib2.HTTPDefaultErrorHandler())
       
   361     opener.add_handler(https_handler())
       
   362     opener.add_handler(urllib2.HTTPErrorProcessor())
       
   363 
       
   364     if self.save_cookies:
       
   365       self.cookie_jar.filename = os.path.expanduser(
       
   366           HttpRpcServer.DEFAULT_COOKIE_FILE_PATH)
       
   367 
       
   368       if os.path.exists(self.cookie_jar.filename):
       
   369         try:
       
   370           self.cookie_jar.load()
       
   371           self.authenticated = True
       
   372           logging.info("Loaded authentication cookies from %s" %
       
   373                        self.cookie_jar.filename)
       
   374         except (OSError, IOError, cookielib.LoadError), e:
       
   375           logging.debug("Could not load authentication cookies; %s: %s",
       
   376                         e.__class__.__name__, e)
       
   377           self.cookie_jar.filename = None
       
   378       else:
       
   379         try:
       
   380           fd = os.open(self.cookie_jar.filename, os.O_CREAT, 0600)
       
   381           os.close(fd)
       
   382         except (OSError, IOError), e:
       
   383           logging.debug("Could not create authentication cookies file; %s: %s",
       
   384                         e.__class__.__name__, e)
       
   385           self.cookie_jar.filename = None
       
   386 
       
   387     opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar))
       
   388     return opener