OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # coding: utf-8 | 2 # coding: utf-8 |
3 # | 3 # |
4 # Copyright 2007 Google Inc. | 4 # Copyright 2007 Google Inc. |
5 # | 5 # |
6 # Licensed under the Apache License, Version 2.0 (the "License"); | 6 # Licensed under the Apache License, Version 2.0 (the "License"); |
7 # you may not use this file except in compliance with the License. | 7 # you may not use this file except in compliance with the License. |
8 # You may obtain a copy of the License at | 8 # You may obtain a copy of the License at |
9 # | 9 # |
10 # http://www.apache.org/licenses/LICENSE-2.0 | 10 # http://www.apache.org/licenses/LICENSE-2.0 |
(...skipping 16 matching lines...) Expand all Loading... |
27 Subversion | 27 Subversion |
28 Perforce | 28 Perforce |
29 CVS | 29 CVS |
30 | 30 |
31 It is important for Git/Mercurial users to specify a tree/node/branch to diff | 31 It is important for Git/Mercurial users to specify a tree/node/branch to diff |
32 against by using the '--rev' option. | 32 against by using the '--rev' option. |
33 """ | 33 """ |
34 # This code is derived from appcfg.py in the App Engine SDK (open source), | 34 # This code is derived from appcfg.py in the App Engine SDK (open source), |
35 # and from ASPN recipe #146306. | 35 # and from ASPN recipe #146306. |
36 | 36 |
| 37 import BaseHTTPServer |
37 import ConfigParser | 38 import ConfigParser |
38 import cookielib | 39 import cookielib |
39 import errno | 40 import errno |
40 import fnmatch | 41 import fnmatch |
41 import getpass | 42 import getpass |
42 import logging | 43 import logging |
43 import marshal | 44 import marshal |
44 import mimetypes | 45 import mimetypes |
45 import optparse | 46 import optparse |
46 import os | 47 import os |
47 import re | 48 import re |
48 import socket | 49 import socket |
49 import subprocess | 50 import subprocess |
50 import sys | 51 import sys |
51 import urllib | 52 import urllib |
52 import urllib2 | 53 import urllib2 |
53 import urlparse | 54 import urlparse |
| 55 import webbrowser |
54 | 56 |
55 # The md5 module was deprecated in Python 2.5. | 57 # The md5 module was deprecated in Python 2.5. |
56 try: | 58 try: |
57 from hashlib import md5 | 59 from hashlib import md5 |
58 except ImportError: | 60 except ImportError: |
59 from md5 import md5 | 61 from md5 import md5 |
60 | 62 |
61 try: | 63 try: |
62 import readline | 64 import readline |
63 except ImportError: | 65 except ImportError: |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
99 VCS_MERCURIAL.lower(): VCS_MERCURIAL, | 101 VCS_MERCURIAL.lower(): VCS_MERCURIAL, |
100 "hg": VCS_MERCURIAL, | 102 "hg": VCS_MERCURIAL, |
101 VCS_SUBVERSION.lower(): VCS_SUBVERSION, | 103 VCS_SUBVERSION.lower(): VCS_SUBVERSION, |
102 "svn": VCS_SUBVERSION, | 104 "svn": VCS_SUBVERSION, |
103 VCS_PERFORCE.lower(): VCS_PERFORCE, | 105 VCS_PERFORCE.lower(): VCS_PERFORCE, |
104 "p4": VCS_PERFORCE, | 106 "p4": VCS_PERFORCE, |
105 VCS_GIT.lower(): VCS_GIT, | 107 VCS_GIT.lower(): VCS_GIT, |
106 VCS_CVS.lower(): VCS_CVS, | 108 VCS_CVS.lower(): VCS_CVS, |
107 } | 109 } |
108 | 110 |
| 111 # OAuth 2.0-Related Constants |
| 112 LOCALHOST_IP = '127.0.0.1' |
| 113 DEFAULT_OAUTH2_PORT = 8001 |
| 114 ACCESS_TOKEN_PARAM = 'access_token' |
| 115 OAUTH_PATH = '/get-access-token' |
| 116 OAUTH_PATH_PORT_TEMPLATE = OAUTH_PATH + '?port=%(port)d' |
| 117 AUTH_HANDLER_RESPONSE = """\ |
| 118 <html> |
| 119 <head> |
| 120 <title>Authentication Status</title> |
| 121 </head> |
| 122 <body> |
| 123 <p>The authentication flow has completed.</p> |
| 124 </body> |
| 125 </html> |
| 126 """ |
| 127 # Borrowed from google-api-python-client |
| 128 OPEN_LOCAL_MESSAGE_TEMPLATE = """\ |
| 129 Your browser has been opened to visit: |
| 130 |
| 131 %s |
| 132 |
| 133 If your browser is on a different machine then exit and re-run |
| 134 upload.py with the command-line parameter |
| 135 |
| 136 --no_oauth2_webbrowser |
| 137 """ |
| 138 NO_OPEN_LOCAL_MESSAGE_TEMPLATE = """\ |
| 139 Go to the following link in your browser: |
| 140 |
| 141 %s |
| 142 |
| 143 and copy the access token. |
| 144 """ |
| 145 |
109 # The result of parsing Subversion's [auto-props] setting. | 146 # The result of parsing Subversion's [auto-props] setting. |
110 svn_auto_props_map = None | 147 svn_auto_props_map = None |
111 | 148 |
112 def GetEmail(prompt): | 149 def GetEmail(prompt): |
113 """Prompts the user for their email address and returns it. | 150 """Prompts the user for their email address and returns it. |
114 | 151 |
115 The last used email address is saved to a file and offered up as a suggestion | 152 The last used email address is saved to a file and offered up as a suggestion |
116 to the user. If the user presses enter without typing in anything the last | 153 to the user. If the user presses enter without typing in anything the last |
117 used email address is used. If the user enters a new address, it is saved | 154 used email address is used. If the user enters a new address, it is saved |
118 for next time we prompt. | 155 for next time we prompt. |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
172 def reason(self): | 209 def reason(self): |
173 # reason is a property on python 2.7 but a member variable on <=2.6. | 210 # reason is a property on python 2.7 but a member variable on <=2.6. |
174 # self.args is modified so it cannot be used as-is so save the value in | 211 # self.args is modified so it cannot be used as-is so save the value in |
175 # self._reason. | 212 # self._reason. |
176 return self._reason | 213 return self._reason |
177 | 214 |
178 | 215 |
179 class AbstractRpcServer(object): | 216 class AbstractRpcServer(object): |
180 """Provides a common interface for a simple RPC server.""" | 217 """Provides a common interface for a simple RPC server.""" |
181 | 218 |
182 def __init__(self, host, auth_function, host_override=None, extra_headers={}, | 219 def __init__(self, host, auth_function, host_override=None, |
183 save_cookies=False, account_type=AUTH_ACCOUNT_TYPE): | 220 extra_headers=None, save_cookies=False, |
| 221 account_type=AUTH_ACCOUNT_TYPE): |
184 """Creates a new AbstractRpcServer. | 222 """Creates a new AbstractRpcServer. |
185 | 223 |
186 Args: | 224 Args: |
187 host: The host to send requests to. | 225 host: The host to send requests to. |
188 auth_function: A function that takes no arguments and returns an | 226 auth_function: A function that takes no arguments and returns an |
189 (email, password) tuple when called. Will be called if authentication | 227 (email, password) tuple when called. Will be called if authentication |
190 is required. | 228 is required. |
191 host_override: The host header to send to the server (defaults to host). | 229 host_override: The host header to send to the server (defaults to host). |
192 extra_headers: A dict of extra headers to append to every request. | 230 extra_headers: A dict of extra headers to append to every request. |
193 save_cookies: If True, save the authentication cookies to local disk. | 231 save_cookies: If True, save the authentication cookies to local disk. |
194 If False, use an in-memory cookiejar instead. Subclasses must | 232 If False, use an in-memory cookiejar instead. Subclasses must |
195 implement this functionality. Defaults to False. | 233 implement this functionality. Defaults to False. |
196 account_type: Account type used for authentication. Defaults to | 234 account_type: Account type used for authentication. Defaults to |
197 AUTH_ACCOUNT_TYPE. | 235 AUTH_ACCOUNT_TYPE. |
198 """ | 236 """ |
199 self.host = host | 237 self.host = host |
200 if (not self.host.startswith("http://") and | 238 if (not self.host.startswith("http://") and |
201 not self.host.startswith("https://")): | 239 not self.host.startswith("https://")): |
202 self.host = "http://" + self.host | 240 self.host = "http://" + self.host |
203 self.host_override = host_override | 241 self.host_override = host_override |
204 self.auth_function = auth_function | 242 self.auth_function = auth_function |
205 self.authenticated = False | 243 self.authenticated = False |
206 self.extra_headers = extra_headers | 244 self.extra_headers = extra_headers or {} |
207 self.save_cookies = save_cookies | 245 self.save_cookies = save_cookies |
208 self.account_type = account_type | 246 self.account_type = account_type |
209 self.opener = self._GetOpener() | 247 self.opener = self._GetOpener() |
210 if self.host_override: | 248 if self.host_override: |
211 logging.info("Server: %s; Host: %s", self.host, self.host_override) | 249 logging.info("Server: %s; Host: %s", self.host, self.host_override) |
212 else: | 250 else: |
213 logging.info("Server: %s", self.host) | 251 logging.info("Server: %s", self.host) |
214 | 252 |
215 def _GetOpener(self): | 253 def _GetOpener(self): |
216 """Returns an OpenerDirector for making HTTP requests. | 254 """Returns an OpenerDirector for making HTTP requests. |
(...skipping 201 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
418 raise | 456 raise |
419 finally: | 457 finally: |
420 socket.setdefaulttimeout(old_timeout) | 458 socket.setdefaulttimeout(old_timeout) |
421 | 459 |
422 | 460 |
423 class HttpRpcServer(AbstractRpcServer): | 461 class HttpRpcServer(AbstractRpcServer): |
424 """Provides a simplified RPC-style interface for HTTP requests.""" | 462 """Provides a simplified RPC-style interface for HTTP requests.""" |
425 | 463 |
426 def _Authenticate(self): | 464 def _Authenticate(self): |
427 """Save the cookie jar after authentication.""" | 465 """Save the cookie jar after authentication.""" |
428 super(HttpRpcServer, self)._Authenticate() | 466 if isinstance(self.auth_function, OAuth2Creds): |
429 if self.save_cookies: | 467 access_token = self.auth_function() |
430 StatusUpdate("Saving authentication cookies to %s" % self.cookie_file) | 468 if access_token is not None: |
431 self.cookie_jar.save() | 469 self.extra_headers['Authorization'] = 'OAuth %s' % (access_token,) |
| 470 self.authenticated = True |
| 471 else: |
| 472 super(HttpRpcServer, self)._Authenticate() |
| 473 if self.save_cookies: |
| 474 StatusUpdate("Saving authentication cookies to %s" % self.cookie_file) |
| 475 self.cookie_jar.save() |
432 | 476 |
433 def _GetOpener(self): | 477 def _GetOpener(self): |
434 """Returns an OpenerDirector that supports cookies and ignores redirects. | 478 """Returns an OpenerDirector that supports cookies and ignores redirects. |
435 | 479 |
436 Returns: | 480 Returns: |
437 A urllib2.OpenerDirector object. | 481 A urllib2.OpenerDirector object. |
438 """ | 482 """ |
439 opener = urllib2.OpenerDirector() | 483 opener = urllib2.OpenerDirector() |
440 opener.add_handler(urllib2.ProxyHandler()) | 484 opener.add_handler(urllib2.ProxyHandler()) |
441 opener.add_handler(urllib2.UnknownHandler()) | 485 opener.add_handler(urllib2.UnknownHandler()) |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
488 optlist = optstr.split(", ") | 532 optlist = optstr.split(", ") |
489 if len(optlist) > 1: | 533 if len(optlist) > 1: |
490 if option.takes_value(): | 534 if option.takes_value(): |
491 # strip METAVAR from all but the last option | 535 # strip METAVAR from all but the last option |
492 optlist = [x.split()[0] for x in optlist[:-1]] + optlist[-1:] | 536 optlist = [x.split()[0] for x in optlist[:-1]] + optlist[-1:] |
493 optstr = " ".join(optlist) | 537 optstr = " ".join(optlist) |
494 return optstr | 538 return optstr |
495 | 539 |
496 | 540 |
497 parser = optparse.OptionParser( | 541 parser = optparse.OptionParser( |
498 usage="%prog [options] [-- diff_options] [path...]", | 542 usage=("%prog [options] [-- diff_options] [path...]\n" |
| 543 "See also: http://code.google.com/p/rietveld/wiki/UploadPyUsage"), |
499 add_help_option=False, | 544 add_help_option=False, |
500 formatter=CondensedHelpFormatter() | 545 formatter=CondensedHelpFormatter() |
501 ) | 546 ) |
502 parser.add_option("-h", "--help", action="store_true", | 547 parser.add_option("-h", "--help", action="store_true", |
503 help="Show this help message and exit.") | 548 help="Show this help message and exit.") |
504 parser.add_option("-y", "--assume_yes", action="store_true", | 549 parser.add_option("-y", "--assume_yes", action="store_true", |
505 dest="assume_yes", default=False, | 550 dest="assume_yes", default=False, |
506 help="Assume that the answer to yes/no questions is 'yes'.") | 551 help="Assume that the answer to yes/no questions is 'yes'.") |
507 # Logging | 552 # Logging |
508 group = parser.add_option_group("Logging options") | 553 group = parser.add_option_group("Logging options") |
(...skipping 15 matching lines...) Expand all Loading... |
524 "Defaults to '%default'.")) | 569 "Defaults to '%default'.")) |
525 group.add_option("-e", "--email", action="store", dest="email", | 570 group.add_option("-e", "--email", action="store", dest="email", |
526 metavar="EMAIL", default=None, | 571 metavar="EMAIL", default=None, |
527 help="The username to use. Will prompt if omitted.") | 572 help="The username to use. Will prompt if omitted.") |
528 group.add_option("-H", "--host", action="store", dest="host", | 573 group.add_option("-H", "--host", action="store", dest="host", |
529 metavar="HOST", default=None, | 574 metavar="HOST", default=None, |
530 help="Overrides the Host header sent with all RPCs.") | 575 help="Overrides the Host header sent with all RPCs.") |
531 group.add_option("--no_cookies", action="store_false", | 576 group.add_option("--no_cookies", action="store_false", |
532 dest="save_cookies", default=True, | 577 dest="save_cookies", default=True, |
533 help="Do not save authentication cookies to local disk.") | 578 help="Do not save authentication cookies to local disk.") |
| 579 group.add_option("--oauth2", action="store_true", |
| 580 dest="use_oauth2", default=False, |
| 581 help="Use OAuth 2.0 instead of a password.") |
| 582 group.add_option("--oauth2_port", action="store", type="int", |
| 583 dest="oauth2_port", default=DEFAULT_OAUTH2_PORT, |
| 584 help=("Port to use to handle OAuth 2.0 redirect. Must be an " |
| 585 "integer in the range 1024-49151, defaults to " |
| 586 "'%default'.")) |
| 587 group.add_option("--no_oauth2_webbrowser", action="store_false", |
| 588 dest="open_oauth2_local_webbrowser", default=True, |
| 589 help="Don't open a browser window to get an access token.") |
534 group.add_option("--account_type", action="store", dest="account_type", | 590 group.add_option("--account_type", action="store", dest="account_type", |
535 metavar="TYPE", default=AUTH_ACCOUNT_TYPE, | 591 metavar="TYPE", default=AUTH_ACCOUNT_TYPE, |
536 choices=["GOOGLE", "HOSTED"], | 592 choices=["GOOGLE", "HOSTED"], |
537 help=("Override the default account type " | 593 help=("Override the default account type " |
538 "(defaults to '%default', " | 594 "(defaults to '%default', " |
539 "valid choices are 'GOOGLE' and 'HOSTED').")) | 595 "valid choices are 'GOOGLE' and 'HOSTED').")) |
540 # Issue | 596 # Issue |
541 group = parser.add_option_group("Issue options") | 597 group = parser.add_option_group("Issue options") |
542 group.add_option("-t", "--title", action="store", dest="title", | 598 group.add_option("-t", "--title", action="store", dest="title", |
543 help="New issue subject or new patch set title") | 599 help="New issue subject or new patch set title") |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
605 metavar="P4_CHANGELIST", default=None, | 661 metavar="P4_CHANGELIST", default=None, |
606 help=("Perforce changelist id")) | 662 help=("Perforce changelist id")) |
607 group.add_option("--p4_client", action="store", dest="p4_client", | 663 group.add_option("--p4_client", action="store", dest="p4_client", |
608 metavar="P4_CLIENT", default=None, | 664 metavar="P4_CLIENT", default=None, |
609 help=("Perforce client/workspace")) | 665 help=("Perforce client/workspace")) |
610 group.add_option("--p4_user", action="store", dest="p4_user", | 666 group.add_option("--p4_user", action="store", dest="p4_user", |
611 metavar="P4_USER", default=None, | 667 metavar="P4_USER", default=None, |
612 help=("Perforce user")) | 668 help=("Perforce user")) |
613 | 669 |
614 | 670 |
| 671 # OAuth 2.0 Methods and Helpers |
| 672 class ClientRedirectServer(BaseHTTPServer.HTTPServer): |
| 673 """A server for redirects back to localhost from the associated server. |
| 674 |
| 675 Waits for a single request and parses the query parameters for an access token |
| 676 and then stops serving. |
| 677 """ |
| 678 access_token = None |
| 679 |
| 680 |
| 681 class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 682 """A handler for redirects back to localhost from the associated server. |
| 683 |
| 684 Waits for a single request and parses the query parameters into the server's |
| 685 access_token and then stops serving. |
| 686 """ |
| 687 |
| 688 def SetAccessToken(self): |
| 689 """Stores the access token from the request on the server. |
| 690 |
| 691 Will only do this if exactly one query parameter was passed in to the |
| 692 request and that query parameter used 'access_token' as the key. |
| 693 """ |
| 694 query_string = urlparse.urlparse(self.path).query |
| 695 query_params = urlparse.parse_qs(query_string) |
| 696 |
| 697 if len(query_params) == 1: |
| 698 access_token_list = query_params.get(ACCESS_TOKEN_PARAM, []) |
| 699 if len(access_token_list) == 1: |
| 700 self.server.access_token = access_token_list[0] |
| 701 |
| 702 def do_GET(self): |
| 703 """Handle a GET request. |
| 704 |
| 705 Parses and saves the query parameters and prints a message that the server |
| 706 has completed its lone task (handling a redirect). |
| 707 |
| 708 Note that we can't detect if an error occurred. |
| 709 """ |
| 710 self.send_response(200) |
| 711 self.send_header('Content-type', 'text/html') |
| 712 self.end_headers() |
| 713 self.SetAccessToken() |
| 714 self.wfile.write(AUTH_HANDLER_RESPONSE) |
| 715 |
| 716 def log_message(self, format, *args): |
| 717 """Do not log messages to stdout while running as command line program.""" |
| 718 pass |
| 719 |
| 720 |
| 721 def OpenOAuth2ConsentPage(server=DEFAULT_REVIEW_SERVER, |
| 722 port=DEFAULT_OAUTH2_PORT): |
| 723 """Opens the OAuth 2.0 consent page or prints instructions how to. |
| 724 |
| 725 Uses the webbrowser module to open the OAuth server side page in a browser. |
| 726 |
| 727 Args: |
| 728 server: String containing the review server URL. Defaults to |
| 729 DEFAULT_REVIEW_SERVER. |
| 730 port: Integer, the port where the localhost server receiving the redirect |
| 731 is serving. Defaults to DEFAULT_OAUTH2_PORT. |
| 732 """ |
| 733 path = OAUTH_PATH_PORT_TEMPLATE % {'port': port} |
| 734 page = 'https://%s%s' % (server, path) |
| 735 webbrowser.open(page, new=1, autoraise=True) |
| 736 print OPEN_LOCAL_MESSAGE_TEMPLATE % (page,) |
| 737 |
| 738 |
| 739 def WaitForAccessToken(port=DEFAULT_OAUTH2_PORT): |
| 740 """Spins up a simple HTTP Server to handle a single request. |
| 741 |
| 742 Intended to handle a single redirect from the production server after the |
| 743 user authenticated via OAuth 2.0 with the server. |
| 744 |
| 745 Args: |
| 746 port: Integer, the port where the localhost server receiving the redirect |
| 747 is serving. Defaults to DEFAULT_OAUTH2_PORT. |
| 748 |
| 749 Returns: |
| 750 The access token passed to the localhost server, or None if no access token |
| 751 was passed. |
| 752 """ |
| 753 httpd = ClientRedirectServer((LOCALHOST_IP, port), ClientRedirectHandler) |
| 754 # Wait to serve just one request before deferring control back |
| 755 # to the caller of wait_for_refresh_token |
| 756 httpd.handle_request() |
| 757 return httpd.access_token |
| 758 |
| 759 |
| 760 def GetAccessToken(server=DEFAULT_REVIEW_SERVER, port=DEFAULT_OAUTH2_PORT, |
| 761 open_local_webbrowser=True): |
| 762 """Gets an Access Token for the current user. |
| 763 |
| 764 Args: |
| 765 server: String containing the review server URL. Defaults to |
| 766 DEFAULT_REVIEW_SERVER. |
| 767 port: Integer, the port where the localhost server receiving the redirect |
| 768 is serving. Defaults to DEFAULT_OAUTH2_PORT. |
| 769 open_local_webbrowser: Boolean, defaults to True. If set, opens a page in |
| 770 the user's browser. |
| 771 |
| 772 Returns: |
| 773 A string access token that was sent to the local server. If the serving page |
| 774 via WaitForAccessToken does not receive an access token, this method |
| 775 returns None. |
| 776 """ |
| 777 access_token = None |
| 778 if open_local_webbrowser: |
| 779 OpenOAuth2ConsentPage(server=server, port=port) |
| 780 try: |
| 781 access_token = WaitForAccessToken(port=port) |
| 782 except socket.error, e: |
| 783 print 'Can\'t start local webserver. Socket Error: %s\n' % (e.strerror,) |
| 784 |
| 785 if access_token is None: |
| 786 # TODO(dhermes): Offer to add to clipboard using xsel, xclip, pbcopy, etc. |
| 787 page = 'https://%s%s' % (server, OAUTH_PATH) |
| 788 print NO_OPEN_LOCAL_MESSAGE_TEMPLATE % (page,) |
| 789 access_token = raw_input('Enter access token: ').strip() |
| 790 |
| 791 return access_token |
| 792 |
| 793 |
615 class KeyringCreds(object): | 794 class KeyringCreds(object): |
616 def __init__(self, server, host, email): | 795 def __init__(self, server, host, email): |
617 self.server = server | 796 self.server = server |
618 self.host = host | 797 # Explicitly cast host to str to work around bug in old versions of Keyring |
| 798 # (versions before 0.10). Even though newer versions of Keyring fix this, |
| 799 # some modern linuxes (such as Ubuntu 12.04) still bundle a version with |
| 800 # the bug. |
| 801 self.host = str(host) |
619 self.email = email | 802 self.email = email |
620 self.accounts_seen = set() | 803 self.accounts_seen = set() |
621 | 804 |
622 def GetUserCredentials(self): | 805 def GetUserCredentials(self): |
623 """Prompts the user for a username and password. | 806 """Prompts the user for a username and password. |
624 | 807 |
625 Only use keyring on the initial call. If the keyring contains the wrong | 808 Only use keyring on the initial call. If the keyring contains the wrong |
626 password, we want to give the user a chance to enter another one. | 809 password, we want to give the user a chance to enter another one. |
627 """ | 810 """ |
628 # Create a local alias to the email variable to avoid Python's crazy | 811 # Create a local alias to the email variable to avoid Python's crazy |
(...skipping 17 matching lines...) Expand all Loading... |
646 else: | 829 else: |
647 password = getpass.getpass("Password for %s: " % email) | 830 password = getpass.getpass("Password for %s: " % email) |
648 if keyring: | 831 if keyring: |
649 answer = raw_input("Store password in system keyring?(y/N) ").strip() | 832 answer = raw_input("Store password in system keyring?(y/N) ").strip() |
650 if answer == "y": | 833 if answer == "y": |
651 keyring.set_password(self.host, email, password) | 834 keyring.set_password(self.host, email, password) |
652 self.accounts_seen.add(email) | 835 self.accounts_seen.add(email) |
653 return (email, password) | 836 return (email, password) |
654 | 837 |
655 | 838 |
| 839 class OAuth2Creds(object): |
| 840 """Simple object to hold server and port to be passed to GetAccessToken.""" |
| 841 |
| 842 def __init__(self, server, port, open_local_webbrowser=True): |
| 843 self.server = server |
| 844 self.port = port |
| 845 self.open_local_webbrowser = open_local_webbrowser |
| 846 |
| 847 def __call__(self): |
| 848 """Uses stored server and port to retrieve OAuth 2.0 access token.""" |
| 849 return GetAccessToken(server=self.server, port=self.port, |
| 850 open_local_webbrowser=self.open_local_webbrowser) |
| 851 |
| 852 |
656 def GetRpcServer(server, email=None, host_override=None, save_cookies=True, | 853 def GetRpcServer(server, email=None, host_override=None, save_cookies=True, |
657 account_type=AUTH_ACCOUNT_TYPE): | 854 account_type=AUTH_ACCOUNT_TYPE, use_oauth2=False, |
| 855 oauth2_port=DEFAULT_OAUTH2_PORT, |
| 856 open_oauth2_local_webbrowser=True): |
658 """Returns an instance of an AbstractRpcServer. | 857 """Returns an instance of an AbstractRpcServer. |
659 | 858 |
660 Args: | 859 Args: |
661 server: String containing the review server URL. | 860 server: String containing the review server URL. |
662 email: String containing user's email address. | 861 email: String containing user's email address. |
663 host_override: If not None, string containing an alternate hostname to use | 862 host_override: If not None, string containing an alternate hostname to use |
664 in the host header. | 863 in the host header. |
665 save_cookies: Whether authentication cookies should be saved to disk. | 864 save_cookies: Whether authentication cookies should be saved to disk. |
666 account_type: Account type for authentication, either 'GOOGLE' | 865 account_type: Account type for authentication, either 'GOOGLE' |
667 or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE. | 866 or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE. |
| 867 use_oauth2: Boolean indicating whether OAuth 2.0 should be used for |
| 868 authentication. |
| 869 oauth2_port: Integer, the port where the localhost server receiving the |
| 870 redirect is serving. Defaults to DEFAULT_OAUTH2_PORT. |
| 871 open_oauth2_local_webbrowser: Boolean, defaults to True. If True and using |
| 872 OAuth, this opens a page in the user's browser to obtain a token. |
668 | 873 |
669 Returns: | 874 Returns: |
670 A new HttpRpcServer, on which RPC calls can be made. | 875 A new HttpRpcServer, on which RPC calls can be made. |
671 """ | 876 """ |
672 | |
673 # If this is the dev_appserver, use fake authentication. | 877 # If this is the dev_appserver, use fake authentication. |
674 host = (host_override or server).lower() | 878 host = (host_override or server).lower() |
675 if re.match(r'(http://)?localhost([:/]|$)', host): | 879 if re.match(r'(http://)?localhost([:/]|$)', host): |
676 if email is None: | 880 if email is None: |
677 email = "test@example.com" | 881 email = "test@example.com" |
678 logging.info("Using debug user %s. Override with --email" % email) | 882 logging.info("Using debug user %s. Override with --email" % email) |
679 server = HttpRpcServer( | 883 server = HttpRpcServer( |
680 server, | 884 server, |
681 lambda: (email, "password"), | 885 lambda: (email, "password"), |
682 host_override=host_override, | 886 host_override=host_override, |
683 extra_headers={"Cookie": | 887 extra_headers={"Cookie": |
684 'dev_appserver_login="%s:False"' % email}, | 888 'dev_appserver_login="%s:False"' % email}, |
685 save_cookies=save_cookies, | 889 save_cookies=save_cookies, |
686 account_type=account_type) | 890 account_type=account_type) |
687 # Don't try to talk to ClientLogin. | 891 # Don't try to talk to ClientLogin. |
688 server.authenticated = True | 892 server.authenticated = True |
689 return server | 893 return server |
690 | 894 |
691 return HttpRpcServer(server, | 895 positional_args = [server] |
692 KeyringCreds(server, host, email).GetUserCredentials, | 896 if use_oauth2: |
| 897 positional_args.append( |
| 898 OAuth2Creds(server, oauth2_port, open_oauth2_local_webbrowser)) |
| 899 else: |
| 900 positional_args.append(KeyringCreds(server, host, email).GetUserCredentials) |
| 901 return HttpRpcServer(*positional_args, |
693 host_override=host_override, | 902 host_override=host_override, |
694 save_cookies=save_cookies) | 903 save_cookies=save_cookies, |
| 904 account_type=account_type) |
695 | 905 |
696 | 906 |
697 def EncodeMultipartFormData(fields, files): | 907 def EncodeMultipartFormData(fields, files): |
698 """Encode form fields for multipart/form-data. | 908 """Encode form fields for multipart/form-data. |
699 | 909 |
700 Args: | 910 Args: |
701 fields: A sequence of (name, value) elements for regular form fields. | 911 fields: A sequence of (name, value) elements for regular form fields. |
702 files: A sequence of (name, filename, value) elements for data to be | 912 files: A sequence of (name, filename, value) elements for data to be |
703 uploaded as files. | 913 uploaded as files. |
704 Returns: | 914 Returns: |
(...skipping 1497 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2202 | 2412 |
2203 Returns: | 2413 Returns: |
2204 A 2-tuple (issue id, patchset id). | 2414 A 2-tuple (issue id, patchset id). |
2205 The patchset id is None if the base files are not uploaded by this | 2415 The patchset id is None if the base files are not uploaded by this |
2206 script (applies only to SVN checkouts). | 2416 script (applies only to SVN checkouts). |
2207 """ | 2417 """ |
2208 options, args = parser.parse_args(argv[1:]) | 2418 options, args = parser.parse_args(argv[1:]) |
2209 if options.help: | 2419 if options.help: |
2210 if options.verbose < 2: | 2420 if options.verbose < 2: |
2211 # hide Perforce options | 2421 # hide Perforce options |
2212 parser.epilog = "Use '--help -v' to show additional Perforce options." | 2422 parser.epilog = ( |
| 2423 "Use '--help -v' to show additional Perforce options. " |
| 2424 "For more help, see " |
| 2425 "http://code.google.com/p/rietveld/wiki/CodeReviewHelp" |
| 2426 ) |
2213 parser.option_groups.remove(parser.get_option_group('--p4_port')) | 2427 parser.option_groups.remove(parser.get_option_group('--p4_port')) |
2214 parser.print_help() | 2428 parser.print_help() |
2215 sys.exit(0) | 2429 sys.exit(0) |
2216 | 2430 |
2217 global verbosity | 2431 global verbosity |
2218 verbosity = options.verbose | 2432 verbosity = options.verbose |
2219 if verbosity >= 3: | 2433 if verbosity >= 3: |
2220 logging.getLogger().setLevel(logging.DEBUG) | 2434 logging.getLogger().setLevel(logging.DEBUG) |
2221 elif verbosity >= 2: | 2435 elif verbosity >= 2: |
2222 logging.getLogger().setLevel(logging.INFO) | 2436 logging.getLogger().setLevel(logging.INFO) |
(...skipping 20 matching lines...) Expand all Loading... |
2243 if data is None: | 2457 if data is None: |
2244 data = vcs.GenerateDiff(args) | 2458 data = vcs.GenerateDiff(args) |
2245 data = vcs.PostProcessDiff(data) | 2459 data = vcs.PostProcessDiff(data) |
2246 if options.print_diffs: | 2460 if options.print_diffs: |
2247 print "Rietveld diff start:*****" | 2461 print "Rietveld diff start:*****" |
2248 print data | 2462 print data |
2249 print "Rietveld diff end:*****" | 2463 print "Rietveld diff end:*****" |
2250 files = vcs.GetBaseFiles(data) | 2464 files = vcs.GetBaseFiles(data) |
2251 if verbosity >= 1: | 2465 if verbosity >= 1: |
2252 print "Upload server:", options.server, "(change with -s/--server)" | 2466 print "Upload server:", options.server, "(change with -s/--server)" |
| 2467 if options.use_oauth2: |
| 2468 options.save_cookies = False |
2253 rpc_server = GetRpcServer(options.server, | 2469 rpc_server = GetRpcServer(options.server, |
2254 options.email, | 2470 options.email, |
2255 options.host, | 2471 options.host, |
2256 options.save_cookies, | 2472 options.save_cookies, |
2257 options.account_type) | 2473 options.account_type, |
| 2474 options.use_oauth2, |
| 2475 options.oauth2_port, |
| 2476 options.open_oauth2_local_webbrowser) |
2258 form_fields = [] | 2477 form_fields = [] |
2259 | 2478 |
2260 repo_guid = vcs.GetGUID() | 2479 repo_guid = vcs.GetGUID() |
2261 if repo_guid: | 2480 if repo_guid: |
2262 form_fields.append(("repo_guid", repo_guid)) | 2481 form_fields.append(("repo_guid", repo_guid)) |
2263 if base: | 2482 if base: |
2264 b = urlparse.urlparse(base) | 2483 b = urlparse.urlparse(base) |
2265 username, netloc = urllib.splituser(b.netloc) | 2484 username, netloc = urllib.splituser(b.netloc) |
2266 if username: | 2485 if username: |
2267 logging.info("Removed username from base URL") | 2486 logging.info("Removed username from base URL") |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2384 os.environ['LC_ALL'] = 'C' | 2603 os.environ['LC_ALL'] = 'C' |
2385 RealMain(sys.argv) | 2604 RealMain(sys.argv) |
2386 except KeyboardInterrupt: | 2605 except KeyboardInterrupt: |
2387 print | 2606 print |
2388 StatusUpdate("Interrupted.") | 2607 StatusUpdate("Interrupted.") |
2389 sys.exit(1) | 2608 sys.exit(1) |
2390 | 2609 |
2391 | 2610 |
2392 if __name__ == "__main__": | 2611 if __name__ == "__main__": |
2393 main() | 2612 main() |
OLD | NEW |