diff -r 88c186556a80 -r f5fd65cc3bf3 thirdparty/google_appengine/google/appengine/tools/appengine_rpc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/google_appengine/google/appengine/tools/appengine_rpc.py Tue Jan 20 13:19:45 2009 +0000 @@ -0,0 +1,388 @@ +#!/usr/bin/env python +# +# Copyright 2007 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. +# + +"""Tool for performing authenticated RPCs against App Engine.""" + + +import cookielib +import logging +import os +import re +import socket +import sys +import urllib +import urllib2 + + +https_handler = urllib2.HTTPSHandler +uses_cert_verification = False +certpath = os.path.join(os.path.dirname(__file__), "cacerts.txt") +cert_file_available = os.path.exists(certpath) +try: + import https_wrapper + if cert_file_available: + https_handler = lambda: https_wrapper.CertValidatingHTTPSHandler( + ca_certs=certpath) + uses_cert_verification = True +except ImportError: + pass + + +def GetPlatformToken(os_module=os, sys_module=sys, platform=sys.platform): + """Returns a 'User-agent' token for the host system platform. + + Args: + os_module, sys_module, platform: Used for testing. + + Returns: + String containing the platform token for the host system. + """ + if hasattr(sys_module, "getwindowsversion"): + windows_version = sys_module.getwindowsversion() + version_info = ".".join(str(i) for i in windows_version[:4]) + return platform + "/" + version_info + elif hasattr(os_module, "uname"): + uname = os_module.uname() + return "%s/%s" % (uname[0], uname[2]) + else: + return "unknown" + + +class ClientLoginError(urllib2.HTTPError): + """Raised to indicate there was 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 AbstractRpcServer(object): + """Provides a common interface for a simple RPC server.""" + + def __init__(self, host, auth_function, user_agent, source, + host_override=None, extra_headers=None, save_cookies=False, + auth_tries=3, account_type=None): + """Creates a new HttpRpcServer. + + 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. + user_agent: The user-agent string to send to the server. Specify None to + omit the user-agent header. + source: The source to specify in authentication requests. + 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. Values + supplied here will override other default headers that are supplied. + save_cookies: If True, save the authentication cookies to local disk. + If False, use an in-memory cookiejar instead. Subclasses must + implement this functionality. Defaults to False. + auth_tries: The number of times to attempt auth_function before failing. + account_type: One of GOOGLE, HOSTED_OR_GOOGLE, or None for automatic. + """ + self.host = host + self.host_override = host_override + self.auth_function = auth_function + self.source = source + self.authenticated = False + self.auth_tries = auth_tries + + self.account_type = account_type + + self.extra_headers = {} + if user_agent: + self.extra_headers["User-Agent"] = user_agent + if extra_headers: + self.extra_headers.update(extra_headers) + + self.save_cookies = save_cookies + self.cookie_jar = cookielib.MozillaCookieJar() + 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) + + if ((self.host_override and self.host_override == "localhost") or + self.host == "localhost" or self.host.startswith("localhost:")): + self._DevAppServerAuthenticate() + + def _GetOpener(self): + """Returns an OpenerDirector for making HTTP requests. + + Returns: + A urllib2.OpenerDirector object. + """ + raise NotImplemented() + + def _CreateRequest(self, url, data=None): + """Creates a new urllib request.""" + 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 = self.account_type + if not account_type: + if (self.host.split(':')[0].endswith(".google.com") + or (self.host_override + and self.host_override.split(':')[0].endswith(".google.com"))): + account_type = "HOSTED_OR_GOOGLE" + else: + account_type = "GOOGLE" + data = { + "Email": email, + "Passwd": password, + "service": "ah", + "source": self.source, + "accountType": account_type + } + + req = self._CreateRequest( + url="https://www.google.com/accounts/ClientLogin", + data=urllib.urlencode(data)) + 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. + """ + continue_location = "http://localhost/" + args = {"continue": continue_location, "auth": auth_token} + login_path = os.environ.get("APPCFG_LOGIN_PATH", "/_ah") + req = self._CreateRequest("http://%s%s/login?%s" % + (self.host, login_path, 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) + self.authenticated = True + + 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. + """ + for unused_i in range(self.auth_tries): + credentials = self.auth_function() + try: + auth_token = self._GetAuthToken(credentials[0], credentials[1]) + except ClientLoginError, e: + if e.reason == "BadAuthentication": + print >>sys.stderr, "Invalid username or password." + continue + if e.reason == "CaptchaRequired": + print >>sys.stderr, ( + "Please go to\n" + "https://www.google.com/accounts/DisplayUnlockCaptcha\n" + "and verify you are a human. Then try again.") + break + if e.reason == "NotVerified": + print >>sys.stderr, "Account not verified." + break + if e.reason == "TermsNotAgreed": + print >>sys.stderr, "User has not agreed to TOS." + break + if e.reason == "AccountDeleted": + print >>sys.stderr, "The user account has been deleted." + break + if e.reason == "AccountDisabled": + print >>sys.stderr, "The user account has been disabled." + break + if e.reason == "ServiceDisabled": + print >>sys.stderr, ("The user's access to the service has been " + "disabled.") + break + if e.reason == "ServiceUnavailable": + print >>sys.stderr, "The service is not available; try again later." + break + raise + self._GetAuthCookie(auth_token) + return + + def _DevAppServerAuthenticate(self): + """Authenticates the user on the dev_appserver.""" + credentials = self.auth_function() + self.extra_headers["Cookie"] = ('dev_appserver_login="%s:True"; Path=/;' % + (credentials[0],)) + + def Send(self, request_path, payload="", + content_type="application/octet-stream", + timeout=None, + **kwargs): + """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. + timeout: timeout in seconds; default None i.e. no timeout. + (Note: for large requests on OS X, the timeout doesn't work right.) + kwargs: Any keyword arguments are converted into query string parameters. + + Returns: + The response body, as a string. + """ + old_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(timeout) + try: + tries = 0 + while True: + tries += 1 + args = dict(kwargs) + url = "http://%s%s?%s" % (self.host, request_path, + urllib.urlencode(args)) + req = self._CreateRequest(url=url, data=payload) + req.add_header("Content-Type", content_type) + req.add_header("X-appcfg-api-version", "1") + try: + f = self.opener.open(req) + response = f.read() + f.close() + return response + except urllib2.HTTPError, e: + logging.debug("Got http error, this is try #%s" % tries) + if tries > self.auth_tries: + raise + elif e.code == 401: + self._Authenticate() + elif e.code >= 500 and e.code < 600: + continue + elif e.code == 302: + loc = e.info()["location"] + logging.debug("Got 302 redirect. Location: %s" % loc) + if loc.startswith("https://www.google.com/accounts/ServiceLogin"): + self._Authenticate() + elif re.match(r"https://www.google.com/a/[a-z0-9.-]+/ServiceLogin", + loc): + self.account_type = "HOSTED" + self._Authenticate() + elif loc.startswith("http://%s/_ah/login" % (self.host,)): + self._DevAppServerAuthenticate() + else: + raise + finally: + socket.setdefaulttimeout(old_timeout) + + +class HttpRpcServer(AbstractRpcServer): + """Provides a simplified RPC-style interface for HTTP requests.""" + + DEFAULT_COOKIE_FILE_PATH = "~/.appcfg_cookies" + + def _Authenticate(self): + """Save the cookie jar after authentication.""" + if cert_file_available and not uses_cert_verification: + logging.warn("ssl module not found. Without this the identity of the " + "remote host cannot be verified, and connections are NOT " + "secure. To fix this, please install the ssl module from " + "http://pypi.python.org/pypi/ssl") + super(HttpRpcServer, self)._Authenticate() + if self.cookie_jar.filename is not None and self.save_cookies: + logging.info("Saving authentication cookies to %s" % + self.cookie_jar.filename) + self.cookie_jar.save() + + 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(https_handler()) + opener.add_handler(urllib2.HTTPErrorProcessor()) + + if self.save_cookies: + self.cookie_jar.filename = os.path.expanduser( + HttpRpcServer.DEFAULT_COOKIE_FILE_PATH) + + if os.path.exists(self.cookie_jar.filename): + try: + self.cookie_jar.load() + self.authenticated = True + logging.info("Loaded authentication cookies from %s" % + self.cookie_jar.filename) + except (OSError, IOError, cookielib.LoadError), e: + logging.debug("Could not load authentication cookies; %s: %s", + e.__class__.__name__, e) + self.cookie_jar.filename = None + else: + try: + fd = os.open(self.cookie_jar.filename, os.O_CREAT, 0600) + os.close(fd) + except (OSError, IOError), e: + logging.debug("Could not create authentication cookies file; %s: %s", + e.__class__.__name__, e) + self.cookie_jar.filename = None + + opener.add_handler(urllib2.HTTPCookieProcessor(self.cookie_jar)) + return opener