|
|
|
@ -34,6 +34,7 @@ against by using the '--rev' option.
|
|
|
|
|
# This code is derived from appcfg.py in the App Engine SDK (open source),
|
|
|
|
|
# and from ASPN recipe #146306.
|
|
|
|
|
|
|
|
|
|
import BaseHTTPServer
|
|
|
|
|
import ConfigParser
|
|
|
|
|
import cookielib
|
|
|
|
|
import errno
|
|
|
|
@ -51,6 +52,7 @@ import sys
|
|
|
|
|
import urllib
|
|
|
|
|
import urllib2
|
|
|
|
|
import urlparse
|
|
|
|
|
import webbrowser
|
|
|
|
|
|
|
|
|
|
# The md5 module was deprecated in Python 2.5.
|
|
|
|
|
try:
|
|
|
|
@ -106,6 +108,41 @@ VCS_ABBREVIATIONS = {
|
|
|
|
|
VCS_CVS.lower(): VCS_CVS,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# OAuth 2.0-Related Constants
|
|
|
|
|
LOCALHOST_IP = '127.0.0.1'
|
|
|
|
|
DEFAULT_OAUTH2_PORT = 8001
|
|
|
|
|
ACCESS_TOKEN_PARAM = 'access_token'
|
|
|
|
|
OAUTH_PATH = '/get-access-token'
|
|
|
|
|
OAUTH_PATH_PORT_TEMPLATE = OAUTH_PATH + '?port=%(port)d'
|
|
|
|
|
AUTH_HANDLER_RESPONSE = """\
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>Authentication Status</title>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<p>The authentication flow has completed.</p>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
"""
|
|
|
|
|
# Borrowed from google-api-python-client
|
|
|
|
|
OPEN_LOCAL_MESSAGE_TEMPLATE = """\
|
|
|
|
|
Your browser has been opened to visit:
|
|
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
If your browser is on a different machine then exit and re-run
|
|
|
|
|
upload.py with the command-line parameter
|
|
|
|
|
|
|
|
|
|
--no_oauth2_webbrowser
|
|
|
|
|
"""
|
|
|
|
|
NO_OPEN_LOCAL_MESSAGE_TEMPLATE = """\
|
|
|
|
|
Go to the following link in your browser:
|
|
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
|
|
and copy the access token.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# The result of parsing Subversion's [auto-props] setting.
|
|
|
|
|
svn_auto_props_map = None
|
|
|
|
|
|
|
|
|
@ -179,8 +216,9 @@ class ClientLoginError(urllib2.HTTPError):
|
|
|
|
|
class AbstractRpcServer(object):
|
|
|
|
|
"""Provides a common interface for a simple RPC server."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, host, auth_function, host_override=None, extra_headers={},
|
|
|
|
|
save_cookies=False, account_type=AUTH_ACCOUNT_TYPE):
|
|
|
|
|
def __init__(self, host, auth_function, host_override=None,
|
|
|
|
|
extra_headers=None, save_cookies=False,
|
|
|
|
|
account_type=AUTH_ACCOUNT_TYPE):
|
|
|
|
|
"""Creates a new AbstractRpcServer.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
@ -203,7 +241,7 @@ class AbstractRpcServer(object):
|
|
|
|
|
self.host_override = host_override
|
|
|
|
|
self.auth_function = auth_function
|
|
|
|
|
self.authenticated = False
|
|
|
|
|
self.extra_headers = extra_headers
|
|
|
|
|
self.extra_headers = extra_headers or {}
|
|
|
|
|
self.save_cookies = save_cookies
|
|
|
|
|
self.account_type = account_type
|
|
|
|
|
self.opener = self._GetOpener()
|
|
|
|
@ -425,10 +463,16 @@ class HttpRpcServer(AbstractRpcServer):
|
|
|
|
|
|
|
|
|
|
def _Authenticate(self):
|
|
|
|
|
"""Save the cookie jar after authentication."""
|
|
|
|
|
super(HttpRpcServer, self)._Authenticate()
|
|
|
|
|
if self.save_cookies:
|
|
|
|
|
StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
|
|
|
|
|
self.cookie_jar.save()
|
|
|
|
|
if isinstance(self.auth_function, OAuth2Creds):
|
|
|
|
|
access_token = self.auth_function()
|
|
|
|
|
if access_token is not None:
|
|
|
|
|
self.extra_headers['Authorization'] = 'OAuth %s' % (access_token,)
|
|
|
|
|
self.authenticated = True
|
|
|
|
|
else:
|
|
|
|
|
super(HttpRpcServer, self)._Authenticate()
|
|
|
|
|
if self.save_cookies:
|
|
|
|
|
StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
|
|
|
|
|
self.cookie_jar.save()
|
|
|
|
|
|
|
|
|
|
def _GetOpener(self):
|
|
|
|
|
"""Returns an OpenerDirector that supports cookies and ignores redirects.
|
|
|
|
@ -495,7 +539,8 @@ class CondensedHelpFormatter(optparse.IndentedHelpFormatter):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parser = optparse.OptionParser(
|
|
|
|
|
usage="%prog [options] [-- diff_options] [path...]",
|
|
|
|
|
usage=("%prog [options] [-- diff_options] [path...]\n"
|
|
|
|
|
"See also: http://code.google.com/p/rietveld/wiki/UploadPyUsage"),
|
|
|
|
|
add_help_option=False,
|
|
|
|
|
formatter=CondensedHelpFormatter()
|
|
|
|
|
)
|
|
|
|
@ -531,6 +576,17 @@ group.add_option("-H", "--host", action="store", dest="host",
|
|
|
|
|
group.add_option("--no_cookies", action="store_false",
|
|
|
|
|
dest="save_cookies", default=True,
|
|
|
|
|
help="Do not save authentication cookies to local disk.")
|
|
|
|
|
group.add_option("--oauth2", action="store_true",
|
|
|
|
|
dest="use_oauth2", default=False,
|
|
|
|
|
help="Use OAuth 2.0 instead of a password.")
|
|
|
|
|
group.add_option("--oauth2_port", action="store", type="int",
|
|
|
|
|
dest="oauth2_port", default=DEFAULT_OAUTH2_PORT,
|
|
|
|
|
help=("Port to use to handle OAuth 2.0 redirect. Must be an "
|
|
|
|
|
"integer in the range 1024-49151, defaults to "
|
|
|
|
|
"'%default'."))
|
|
|
|
|
group.add_option("--no_oauth2_webbrowser", action="store_false",
|
|
|
|
|
dest="open_oauth2_local_webbrowser", default=True,
|
|
|
|
|
help="Don't open a browser window to get an access token.")
|
|
|
|
|
group.add_option("--account_type", action="store", dest="account_type",
|
|
|
|
|
metavar="TYPE", default=AUTH_ACCOUNT_TYPE,
|
|
|
|
|
choices=["GOOGLE", "HOSTED"],
|
|
|
|
@ -612,10 +668,137 @@ group.add_option("--p4_user", action="store", dest="p4_user",
|
|
|
|
|
help=("Perforce user"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# OAuth 2.0 Methods and Helpers
|
|
|
|
|
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
|
|
|
|
"""A server for redirects back to localhost from the associated server.
|
|
|
|
|
|
|
|
|
|
Waits for a single request and parses the query parameters for an access token
|
|
|
|
|
and then stops serving.
|
|
|
|
|
"""
|
|
|
|
|
access_token = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|
|
|
|
"""A handler for redirects back to localhost from the associated server.
|
|
|
|
|
|
|
|
|
|
Waits for a single request and parses the query parameters into the server's
|
|
|
|
|
access_token and then stops serving.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def SetAccessToken(self):
|
|
|
|
|
"""Stores the access token from the request on the server.
|
|
|
|
|
|
|
|
|
|
Will only do this if exactly one query parameter was passed in to the
|
|
|
|
|
request and that query parameter used 'access_token' as the key.
|
|
|
|
|
"""
|
|
|
|
|
query_string = urlparse.urlparse(self.path).query
|
|
|
|
|
query_params = urlparse.parse_qs(query_string)
|
|
|
|
|
|
|
|
|
|
if len(query_params) == 1:
|
|
|
|
|
access_token_list = query_params.get(ACCESS_TOKEN_PARAM, [])
|
|
|
|
|
if len(access_token_list) == 1:
|
|
|
|
|
self.server.access_token = access_token_list[0]
|
|
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
|
"""Handle a GET request.
|
|
|
|
|
|
|
|
|
|
Parses and saves the query parameters and prints a message that the server
|
|
|
|
|
has completed its lone task (handling a redirect).
|
|
|
|
|
|
|
|
|
|
Note that we can't detect if an error occurred.
|
|
|
|
|
"""
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
self.send_header('Content-type', 'text/html')
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.SetAccessToken()
|
|
|
|
|
self.wfile.write(AUTH_HANDLER_RESPONSE)
|
|
|
|
|
|
|
|
|
|
def log_message(self, format, *args):
|
|
|
|
|
"""Do not log messages to stdout while running as command line program."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def OpenOAuth2ConsentPage(server=DEFAULT_REVIEW_SERVER,
|
|
|
|
|
port=DEFAULT_OAUTH2_PORT):
|
|
|
|
|
"""Opens the OAuth 2.0 consent page or prints instructions how to.
|
|
|
|
|
|
|
|
|
|
Uses the webbrowser module to open the OAuth server side page in a browser.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
server: String containing the review server URL. Defaults to
|
|
|
|
|
DEFAULT_REVIEW_SERVER.
|
|
|
|
|
port: Integer, the port where the localhost server receiving the redirect
|
|
|
|
|
is serving. Defaults to DEFAULT_OAUTH2_PORT.
|
|
|
|
|
"""
|
|
|
|
|
path = OAUTH_PATH_PORT_TEMPLATE % {'port': port}
|
|
|
|
|
page = 'https://%s%s' % (server, path)
|
|
|
|
|
webbrowser.open(page, new=1, autoraise=True)
|
|
|
|
|
print OPEN_LOCAL_MESSAGE_TEMPLATE % (page,)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def WaitForAccessToken(port=DEFAULT_OAUTH2_PORT):
|
|
|
|
|
"""Spins up a simple HTTP Server to handle a single request.
|
|
|
|
|
|
|
|
|
|
Intended to handle a single redirect from the production server after the
|
|
|
|
|
user authenticated via OAuth 2.0 with the server.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
port: Integer, the port where the localhost server receiving the redirect
|
|
|
|
|
is serving. Defaults to DEFAULT_OAUTH2_PORT.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The access token passed to the localhost server, or None if no access token
|
|
|
|
|
was passed.
|
|
|
|
|
"""
|
|
|
|
|
httpd = ClientRedirectServer((LOCALHOST_IP, port), ClientRedirectHandler)
|
|
|
|
|
# Wait to serve just one request before deferring control back
|
|
|
|
|
# to the caller of wait_for_refresh_token
|
|
|
|
|
httpd.handle_request()
|
|
|
|
|
return httpd.access_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def GetAccessToken(server=DEFAULT_REVIEW_SERVER, port=DEFAULT_OAUTH2_PORT,
|
|
|
|
|
open_local_webbrowser=True):
|
|
|
|
|
"""Gets an Access Token for the current user.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
server: String containing the review server URL. Defaults to
|
|
|
|
|
DEFAULT_REVIEW_SERVER.
|
|
|
|
|
port: Integer, the port where the localhost server receiving the redirect
|
|
|
|
|
is serving. Defaults to DEFAULT_OAUTH2_PORT.
|
|
|
|
|
open_local_webbrowser: Boolean, defaults to True. If set, opens a page in
|
|
|
|
|
the user's browser.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
A string access token that was sent to the local server. If the serving page
|
|
|
|
|
via WaitForAccessToken does not receive an access token, this method
|
|
|
|
|
returns None.
|
|
|
|
|
"""
|
|
|
|
|
access_token = None
|
|
|
|
|
if open_local_webbrowser:
|
|
|
|
|
OpenOAuth2ConsentPage(server=server, port=port)
|
|
|
|
|
try:
|
|
|
|
|
access_token = WaitForAccessToken(port=port)
|
|
|
|
|
except socket.error, e:
|
|
|
|
|
print 'Can\'t start local webserver. Socket Error: %s\n' % (e.strerror,)
|
|
|
|
|
|
|
|
|
|
if access_token is None:
|
|
|
|
|
# TODO(dhermes): Offer to add to clipboard using xsel, xclip, pbcopy, etc.
|
|
|
|
|
page = 'https://%s%s' % (server, OAUTH_PATH)
|
|
|
|
|
print NO_OPEN_LOCAL_MESSAGE_TEMPLATE % (page,)
|
|
|
|
|
access_token = raw_input('Enter access token: ').strip()
|
|
|
|
|
|
|
|
|
|
return access_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KeyringCreds(object):
|
|
|
|
|
def __init__(self, server, host, email):
|
|
|
|
|
self.server = server
|
|
|
|
|
self.host = host
|
|
|
|
|
# Explicitly cast host to str to work around bug in old versions of Keyring
|
|
|
|
|
# (versions before 0.10). Even though newer versions of Keyring fix this,
|
|
|
|
|
# some modern linuxes (such as Ubuntu 12.04) still bundle a version with
|
|
|
|
|
# the bug.
|
|
|
|
|
self.host = str(host)
|
|
|
|
|
self.email = email
|
|
|
|
|
self.accounts_seen = set()
|
|
|
|
|
|
|
|
|
@ -653,8 +836,24 @@ class KeyringCreds(object):
|
|
|
|
|
return (email, password)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OAuth2Creds(object):
|
|
|
|
|
"""Simple object to hold server and port to be passed to GetAccessToken."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, server, port, open_local_webbrowser=True):
|
|
|
|
|
self.server = server
|
|
|
|
|
self.port = port
|
|
|
|
|
self.open_local_webbrowser = open_local_webbrowser
|
|
|
|
|
|
|
|
|
|
def __call__(self):
|
|
|
|
|
"""Uses stored server and port to retrieve OAuth 2.0 access token."""
|
|
|
|
|
return GetAccessToken(server=self.server, port=self.port,
|
|
|
|
|
open_local_webbrowser=self.open_local_webbrowser)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
|
|
|
|
account_type=AUTH_ACCOUNT_TYPE):
|
|
|
|
|
account_type=AUTH_ACCOUNT_TYPE, use_oauth2=False,
|
|
|
|
|
oauth2_port=DEFAULT_OAUTH2_PORT,
|
|
|
|
|
open_oauth2_local_webbrowser=True):
|
|
|
|
|
"""Returns an instance of an AbstractRpcServer.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
@ -665,11 +864,16 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
|
|
|
|
save_cookies: Whether authentication cookies should be saved to disk.
|
|
|
|
|
account_type: Account type for authentication, either 'GOOGLE'
|
|
|
|
|
or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE.
|
|
|
|
|
use_oauth2: Boolean indicating whether OAuth 2.0 should be used for
|
|
|
|
|
authentication.
|
|
|
|
|
oauth2_port: Integer, the port where the localhost server receiving the
|
|
|
|
|
redirect is serving. Defaults to DEFAULT_OAUTH2_PORT.
|
|
|
|
|
open_oauth2_local_webbrowser: Boolean, defaults to True. If True and using
|
|
|
|
|
OAuth, this opens a page in the user's browser to obtain a token.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
A new HttpRpcServer, on which RPC calls can be made.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# If this is the dev_appserver, use fake authentication.
|
|
|
|
|
host = (host_override or server).lower()
|
|
|
|
|
if re.match(r'(http://)?localhost([:/]|$)', host):
|
|
|
|
@ -688,10 +892,16 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
|
|
|
|
|
server.authenticated = True
|
|
|
|
|
return server
|
|
|
|
|
|
|
|
|
|
return HttpRpcServer(server,
|
|
|
|
|
KeyringCreds(server, host, email).GetUserCredentials,
|
|
|
|
|
positional_args = [server]
|
|
|
|
|
if use_oauth2:
|
|
|
|
|
positional_args.append(
|
|
|
|
|
OAuth2Creds(server, oauth2_port, open_oauth2_local_webbrowser))
|
|
|
|
|
else:
|
|
|
|
|
positional_args.append(KeyringCreds(server, host, email).GetUserCredentials)
|
|
|
|
|
return HttpRpcServer(*positional_args,
|
|
|
|
|
host_override=host_override,
|
|
|
|
|
save_cookies=save_cookies)
|
|
|
|
|
save_cookies=save_cookies,
|
|
|
|
|
account_type=account_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def EncodeMultipartFormData(fields, files):
|
|
|
|
@ -2209,7 +2419,11 @@ def RealMain(argv, data=None):
|
|
|
|
|
if options.help:
|
|
|
|
|
if options.verbose < 2:
|
|
|
|
|
# hide Perforce options
|
|
|
|
|
parser.epilog = "Use '--help -v' to show additional Perforce options."
|
|
|
|
|
parser.epilog = (
|
|
|
|
|
"Use '--help -v' to show additional Perforce options. "
|
|
|
|
|
"For more help, see "
|
|
|
|
|
"http://code.google.com/p/rietveld/wiki/CodeReviewHelp"
|
|
|
|
|
)
|
|
|
|
|
parser.option_groups.remove(parser.get_option_group('--p4_port'))
|
|
|
|
|
parser.print_help()
|
|
|
|
|
sys.exit(0)
|
|
|
|
@ -2250,11 +2464,16 @@ def RealMain(argv, data=None):
|
|
|
|
|
files = vcs.GetBaseFiles(data)
|
|
|
|
|
if verbosity >= 1:
|
|
|
|
|
print "Upload server:", options.server, "(change with -s/--server)"
|
|
|
|
|
if options.use_oauth2:
|
|
|
|
|
options.save_cookies = False
|
|
|
|
|
rpc_server = GetRpcServer(options.server,
|
|
|
|
|
options.email,
|
|
|
|
|
options.host,
|
|
|
|
|
options.save_cookies,
|
|
|
|
|
options.account_type)
|
|
|
|
|
options.account_type,
|
|
|
|
|
options.use_oauth2,
|
|
|
|
|
options.oauth2_port,
|
|
|
|
|
options.open_oauth2_local_webbrowser)
|
|
|
|
|
form_fields = []
|
|
|
|
|
|
|
|
|
|
repo_guid = vcs.GetGUID()
|
|
|
|
|