diff --git a/.travis.yml b/.travis.yml index 39df0ef..a30cb68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: python python: 2.7 env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=pypy + - TOX_ENV=py26openssl13 + - TOX_ENV=py26openssl14 + - TOX_ENV=py27openssl13 + - TOX_ENV=py27openssl14 + - TOX_ENV=py33openssl14 + - TOX_ENV=py34openssl14 + - TOX_ENV=pypyopenssl13 + - TOX_ENV=pypyopenssl14 install: - pip install tox - - pip install . script: - tox -e $TOX_ENV notifications: diff --git a/README.md b/README.md index ad92d06..1664d13 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ [![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client) +NOTE +==== + +This is a work-in-progress branch to add python3 support to oauth2client. Most +of the work was done by @pferate. + This is a client library for accessing resources protected by OAuth 2.0. [Full documentation](http://google.github.io/oauth2client/) diff --git a/oauth2client/client.py b/oauth2client/client.py index b6a7840..4b9304a 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -28,8 +28,8 @@ import logging import os import sys import time -import urllib -import urlparse +import six +from six.moves import urllib import httplib2 from oauth2client import clientsecrets @@ -231,6 +231,9 @@ class Credentials(object): # Add in information we will need later to reconsistitue this instance. d['_class'] = t.__name__ d['_module'] = t.__module__ + for key, val in d.items(): + if isinstance(val, bytes): + d[key] = val.decode('utf-8') return json.dumps(d) def to_json(self): @@ -254,6 +257,8 @@ class Credentials(object): An instance of the subclass of Credentials that was serialized with to_json(). """ + if six.PY3 and isinstance(s, bytes): + s = s.decode('utf-8') data = json.loads(s) # Find and call the right classmethod from_json() to restore the object. module = data['_module'] @@ -398,7 +403,7 @@ def clean_headers(headers): """ clean = {} try: - for k, v in headers.iteritems(): + for k, v in six.iteritems(headers): clean[str(k)] = str(v) except UnicodeEncodeError: raise NonAsciiHeaderError(k + ': ' + v) @@ -415,11 +420,11 @@ def _update_query_params(uri, params): Returns: The same URI but with the new query parameters added. """ - parts = urlparse.urlparse(uri) - query_params = dict(urlparse.parse_qsl(parts.query)) + parts = urllib.parse.urlparse(uri) + query_params = dict(urllib.parse.parse_qsl(parts.query)) query_params.update(params) - new_parts = parts._replace(query=urllib.urlencode(query_params)) - return urlparse.urlunparse(new_parts) + new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) + return urllib.parse.urlunparse(new_parts) class OAuth2Credentials(Credentials): @@ -589,6 +594,8 @@ class OAuth2Credentials(Credentials): Returns: An instance of a Credentials subclass. """ + if six.PY3 and isinstance(s, bytes): + s = s.decode('utf-8') data = json.loads(s) if (data.get('token_expiry') and not isinstance(data['token_expiry'], datetime.datetime)): @@ -691,7 +698,7 @@ class OAuth2Credentials(Credentials): def _generate_refresh_request_body(self): """Generate the body that will be used in the refresh request.""" - body = urllib.urlencode({ + body = urllib.parse.urlencode({ 'grant_type': 'refresh_token', 'client_id': self.client_id, 'client_secret': self.client_secret, @@ -755,8 +762,9 @@ class OAuth2Credentials(Credentials): logger.info('Refreshing access_token') resp, content = http_request( self.token_uri, method='POST', body=body, headers=headers) + if six.PY3: + content = content.decode('utf-8') if resp.status == 200: - # TODO(jcgregorio) Raise an error if loads fails? d = json.loads(content) self.token_response = d self.access_token = d['access_token'] @@ -785,7 +793,7 @@ class OAuth2Credentials(Credentials): self.invalid = True if self.store: self.store.locked_put(self) - except StandardError: + except (TypeError, ValueError): pass raise AccessTokenRefreshError(error_msg) @@ -822,7 +830,7 @@ class OAuth2Credentials(Credentials): d = json.loads(content) if 'error' in d: error_msg = d['error'] - except StandardError: + except (TypeError, ValueError): pass raise TokenRevokeError(error_msg) @@ -880,10 +888,12 @@ class AccessTokenCredentials(OAuth2Credentials): @classmethod def from_json(cls, s): + if six.PY3 and isinstance(s, bytes): + s = s.decode('utf-8') data = json.loads(s) retval = AccessTokenCredentials( - data['access_token'], - data['user_agent']) + data['access_token'], + data['user_agent']) return retval def _refresh(self, http_request): @@ -903,7 +913,7 @@ class AccessTokenCredentials(OAuth2Credentials): _env_name = None -def _get_environment(urllib2_urlopen=None): +def _get_environment(urlopen=None): """Detect the environment the code is being run on.""" global _env_name @@ -917,16 +927,15 @@ def _get_environment(urllib2_urlopen=None): elif server_software.startswith('Development/'): _env_name = 'GAE_LOCAL' else: - import urllib2 try: - if urllib2_urlopen is None: - urllib2_urlopen = urllib2.urlopen - response = urllib2_urlopen('http://metadata.google.internal') + if urlopen is None: + urlopen = urllib.request.urlopen + response = urlopen('http://metadata.google.internal') if any('Metadata-Flavor: Google' in h for h in response.info().headers): _env_name = 'GCE_PRODUCTION' else: _env_name = 'UNKNOWN' - except urllib2.URLError: + except urllib.error.URLError: _env_name = 'UNKNOWN' return _env_name @@ -956,7 +965,7 @@ class GoogleCredentials(OAuth2Credentials): request = service.instances().list(project=PROJECT, zone=ZONE) response = request.execute() - print response + print(response) A service that does not require authentication does not need credentials @@ -970,7 +979,7 @@ class GoogleCredentials(OAuth2Credentials): request = service.apis().list() response = request.execute() - print response + print(response) """ @@ -1168,7 +1177,7 @@ def _get_application_default_credential_from_file( application_default_credential_filename): """Build the Application Default Credentials from file.""" - import service_account + from oauth2client import service_account # read the credentials from the file with open(application_default_credential_filename) as ( @@ -1274,7 +1283,7 @@ class AssertionCredentials(GoogleCredentials): def _generate_refresh_request_body(self): assertion = self._generate_assertion() - body = urllib.urlencode({ + body = urllib.parse.urlencode({ 'assertion': assertion, 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', }) @@ -1363,6 +1372,8 @@ class SignedJwtAssertionCredentials(AssertionCredentials): # Keep base64 encoded so it can be stored in JSON. self.private_key = base64.b64encode(private_key) + if isinstance(self.private_key, six.text_type): + self.private_key = self.private_key.encode('utf-8') self.private_key_password = private_key_password self.service_account_name = service_account_name @@ -1386,7 +1397,7 @@ class SignedJwtAssertionCredentials(AssertionCredentials): def _generate_assertion(self): """Generate the assertion that will be used in the request.""" - now = long(time.time()) + now = int(time.time()) payload = { 'aud': self.token_uri, 'scope': self.scope, @@ -1435,7 +1446,7 @@ def verify_id_token(id_token, audience, http=None, resp, content = http.request(cert_uri) if resp.status == 200: - certs = json.loads(content) + certs = json.loads(content.decode('utf-8')) return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) else: raise VerifyJwtTokenError('Status code: %d' % resp.status) @@ -1443,8 +1454,9 @@ def verify_id_token(id_token, audience, http=None, def _urlsafe_b64decode(b64string): # Guard against unicode strings, which base64 can't handle. - b64string = b64string.encode('ascii') - padded = b64string + '=' * (4 - len(b64string) % 4) + if isinstance(b64string, six.text_type): + b64string = b64string.encode('ascii') + padded = b64string + b'=' * (4 - len(b64string) % 4) return base64.urlsafe_b64decode(padded) @@ -1465,7 +1477,7 @@ def _extract_id_token(id_token): raise VerifyJwtTokenError( 'Wrong number of segments in token: %s' % id_token) - return json.loads(_urlsafe_b64decode(segments[1])) + return json.loads(_urlsafe_b64decode(segments[1]).decode('utf-8')) def _parse_exchange_token_response(content): @@ -1483,11 +1495,11 @@ def _parse_exchange_token_response(content): """ resp = {} try: - resp = json.loads(content) - except StandardError: + resp = json.loads(content.decode('utf-8')) + except Exception: # different JSON libs raise different exceptions, # so we just do a catch-all here - resp = dict(urlparse.parse_qsl(content)) + resp = dict(urllib.parse.parse_qsl(content)) # some providers respond with 'expires', others with 'expires_in' if resp and 'expires' in resp: @@ -1810,7 +1822,7 @@ class OAuth2WebServerFlow(Flow): else: post_data['grant_type'] = 'authorization_code' post_data['redirect_uri'] = self.redirect_uri - body = urllib.urlencode(post_data) + body = urllib.parse.urlencode(post_data) headers = { 'content-type': 'application/x-www-form-urlencoded', } @@ -1851,7 +1863,7 @@ class OAuth2WebServerFlow(Flow): logger.info('Failed to retrieve access token: %s', content) if 'error' in d: # you never know what those providers got to say - error_msg = unicode(d['error']) + error_msg = str(d['error']) else: error_msg = 'Invalid response: %s.' % str(resp.status) raise FlowExchangeError(error_msg) diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py index bfe51a6..9c94471 100644 --- a/oauth2client/clientsecrets.py +++ b/oauth2client/clientsecrets.py @@ -21,6 +21,7 @@ an OAuth 2.0 protected service. __author__ = 'jcgregorio@google.com (Joe Gregorio)' import json +import six # Properties that make a client_secrets.json file valid. @@ -70,9 +71,9 @@ class InvalidClientSecretsError(Error): def _validate_clientsecrets(obj): if obj is None or len(obj) != 1: raise InvalidClientSecretsError('Invalid file format.') - client_type = obj.keys()[0] - if client_type not in VALID_CLIENT.keys(): - raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) + client_type = tuple(obj)[0] + if client_type not in VALID_CLIENT: + raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,)) client_info = obj[client_type] for prop_name in VALID_CLIENT[client_type]['required']: if prop_name not in client_info: @@ -98,11 +99,8 @@ def loads(s): def _loadfile(filename): try: - fp = file(filename, 'r') - try: + with open(filename, 'r') as fp: obj = json.load(fp) - finally: - fp.close() except IOError: raise InvalidClientSecretsError('File not found: "%s"' % filename) return _validate_clientsecrets(obj) @@ -150,4 +148,4 @@ def loadfile(filename, cache=None): obj = {client_type: client_info} cache.set(filename, obj, namespace=_SECRET_NAMESPACE) - return obj.iteritems().next() + return next(six.iteritems(obj)) diff --git a/oauth2client/crypt.py b/oauth2client/crypt.py index 7811266..1c5748e 100644 --- a/oauth2client/crypt.py +++ b/oauth2client/crypt.py @@ -18,8 +18,11 @@ import base64 import json import logging +import sys import time +import six + CLOCK_SKEW_SECS = 300 # 5 minutes in seconds AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds @@ -59,6 +62,8 @@ try: key that this object was constructed with. """ try: + if isinstance(message, six.text_type): + message = message.encode('utf-8') crypto.verify(self._pubkey, signature, message, 'sha256') return True except: @@ -101,15 +106,17 @@ try: """Signs a message. Args: - message: string, Message to be signed. + message: bytes, Message to be signed. Returns: string, The signature of the message for the given key. """ + if isinstance(message, six.text_type): + message = message.encode('utf-8') return crypto.sign(self._key, message, 'sha256') @staticmethod - def from_string(key, password='notasecret'): + def from_string(key, password=b'notasecret'): """Construct a Signer instance from a string. Args: @@ -126,7 +133,9 @@ try: if parsed_pem_key: pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) else: - pkey = crypto.load_pkcs12(key, password.encode('utf8')).get_privatekey() + if isinstance(password, six.text_type): + password = password.encode('utf-8') + pkey = crypto.load_pkcs12(key, password).get_privatekey() return OpenSSLSigner(pkey) except ImportError: @@ -182,8 +191,10 @@ try: Verifier instance. """ if is_x509_cert: - pemLines = key_pem.replace(' ', '').split() - certDer = _urlsafe_b64decode(''.join(pemLines[1:-1])) + if isinstance(key_pem, six.text_type): + key_pem = key_pem.encode('ascii') + pemLines = key_pem.replace(b' ', b'').split() + certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1])) certSeq = DerSequence() certSeq.decode(certDer) tbsSeq = DerSequence() @@ -214,6 +225,8 @@ try: Returns: string, The signature of the message for the given key. """ + if isinstance(message, six.text_type): + message = message.encode('utf-8') return PKCS1_v1_5.new(self._key).sign(SHA256.new(message)) @staticmethod @@ -269,19 +282,22 @@ def _parse_pem_key(raw_key_input): Returns: string, The actual key if the contents are from a PEM file, or else None. """ - offset = raw_key_input.find('-----BEGIN ') + offset = raw_key_input.find(b'-----BEGIN ') if offset != -1: return raw_key_input[offset:] def _urlsafe_b64encode(raw_bytes): - return base64.urlsafe_b64encode(raw_bytes).rstrip('=') + if isinstance(raw_bytes, six.text_type): + raw_bytes = raw_bytes.encode('utf-8') + return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=') def _urlsafe_b64decode(b64string): # Guard against unicode strings, which base64 can't handle. - b64string = b64string.encode('ascii') - padded = b64string + '=' * (4 - len(b64string) % 4) + if isinstance(b64string, six.text_type): + b64string = b64string.encode('ascii') + padded = b64string + b'=' * (4 - len(b64string) % 4) return base64.urlsafe_b64decode(padded) @@ -345,13 +361,13 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): # Parse token. json_body = _urlsafe_b64decode(segments[1]) try: - parsed = json.loads(json_body) + parsed = json.loads(json_body.decode('utf-8')) except: raise AppIdentityError('Can\'t parse token: %s' % json_body) # Check signature. verified = False - for _, pem in certs.items(): + for pem in certs.values(): verifier = Verifier.from_string(pem, True) if verifier.verify(signed, signature): verified = True @@ -366,7 +382,7 @@ def verify_signed_jwt_with_certs(jwt, certs, audience): earliest = iat - CLOCK_SKEW_SECS # Check expiration timestamp. - now = long(time.time()) + now = int(time.time()) exp = parsed.get('exp') if exp is None: raise AppIdentityError('No exp field in token: %s' % json_body) diff --git a/oauth2client/file.py b/oauth2client/file.py index 4d4ed24..9d0ae7f 100644 --- a/oauth2client/file.py +++ b/oauth2client/file.py @@ -90,7 +90,7 @@ class Storage(BaseStorage): simple version of "touch" to ensure the file has been created. """ if not os.path.exists(self._filename): - old_umask = os.umask(0177) + old_umask = os.umask(0o177) try: open(self._filename, 'a+b').close() finally: @@ -108,7 +108,7 @@ class Storage(BaseStorage): self._create_file_if_needed() self._validate_file() - f = open(self._filename, 'wb') + f = open(self._filename, 'w') f.write(credentials.to_json()) f.close() diff --git a/oauth2client/gce.py b/oauth2client/gce.py index 5a3e824..fc3bd77 100644 --- a/oauth2client/gce.py +++ b/oauth2client/gce.py @@ -21,7 +21,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import json import logging -import urllib +from six.moves import urllib from oauth2client import util from oauth2client.client import AccessTokenRefreshError @@ -78,13 +78,13 @@ class AppAssertionCredentials(AssertionCredentials): Raises: AccessTokenRefreshError: When the refresh fails. """ - query = '?scope=%s' % urllib.quote(self.scope, '') + query = '?scope=%s' % urllib.parse.quote(self.scope, '') uri = META.replace('{?scope}', query) response, content = http_request(uri) if response.status == 200: try: d = json.loads(content) - except StandardError as e: + except Exception as e: raise AccessTokenRefreshError(str(e)) self.access_token = d['accessToken'] else: diff --git a/oauth2client/locked_file.py b/oauth2client/locked_file.py index 27fc1ea..225ebcc 100644 --- a/oauth2client/locked_file.py +++ b/oauth2client/locked_file.py @@ -21,10 +21,10 @@ Usage: f = LockedFile('filename', 'r+b', 'rb') f.open_and_lock() if f.is_locked(): - print 'Acquired filename with r+b mode' + print('Acquired filename with r+b mode') f.file_handle().write('locked data') else: - print 'Aquired filename with rb mode' + print('Aquired filename with rb mode') f.unlock_and_close() """ diff --git a/oauth2client/multistore_file.py b/oauth2client/multistore_file.py index 6d2ef57..135f224 100644 --- a/oauth2client/multistore_file.py +++ b/oauth2client/multistore_file.py @@ -191,7 +191,7 @@ class _MultiStore(object): This will create the file if necessary. """ - self._file = LockedFile(filename, 'r+b', 'rb') + self._file = LockedFile(filename, 'r+', 'r') self._thread_lock = threading.Lock() self._read_only = False self._warn_on_readonly = warn_on_readonly @@ -269,7 +269,7 @@ class _MultiStore(object): simple version of "touch" to ensure the file has been created. """ if not os.path.exists(self._file.filename()): - old_umask = os.umask(0177) + old_umask = os.umask(0o177) try: open(self._file.filename(), 'a+b').close() finally: diff --git a/oauth2client/old_run.py b/oauth2client/old_run.py index c7383c3..7267313 100644 --- a/oauth2client/old_run.py +++ b/oauth2client/old_run.py @@ -25,8 +25,8 @@ import gflags from oauth2client import client from oauth2client import util -from tools import ClientRedirectHandler -from tools import ClientRedirectServer +from oauth2client.tools import ClientRedirectHandler +from oauth2client.tools import ClientRedirectServer FLAGS = gflags.FLAGS @@ -103,13 +103,13 @@ def run(flow, storage, http=None): break FLAGS.auth_local_webserver = success if not success: - print 'Failed to start a local webserver listening on either port 8080' - print 'or port 9090. Please check your firewall settings and locally' - print 'running programs that may be blocking or using those ports.' - print - print 'Falling back to --noauth_local_webserver and continuing with', - print 'authorization.' - print + print('Failed to start a local webserver listening on either port 8080') + print('or port 9090. Please check your firewall settings and locally') + print('running programs that may be blocking or using those ports.') + print() + print('Falling back to --noauth_local_webserver and continuing with') + print('authorization.') + print() if FLAGS.auth_local_webserver: oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) @@ -120,20 +120,20 @@ def run(flow, storage, http=None): if FLAGS.auth_local_webserver: webbrowser.open(authorize_url, new=1, autoraise=True) - print 'Your browser has been opened to visit:' - print - print ' ' + authorize_url - print - print 'If your browser is on a different machine then exit and re-run' - print 'this application with the command-line parameter ' - print - print ' --noauth_local_webserver' - print + print('Your browser has been opened to visit:') + print() + print(' ' + authorize_url) + print() + print('If your browser is on a different machine then exit and re-run') + print('this application with the command-line parameter ') + print() + print(' --noauth_local_webserver') + print() else: - print 'Go to the following link in your browser:' - print - print ' ' + authorize_url - print + print('Go to the following link in your browser:') + print() + print(' ' + authorize_url) + print() code = None if FLAGS.auth_local_webserver: @@ -143,7 +143,7 @@ def run(flow, storage, http=None): if 'code' in httpd.query_params: code = httpd.query_params['code'] else: - print 'Failed to find "code" in the query parameters of the redirect.' + print('Failed to find "code" in the query parameters of the redirect.') sys.exit('Try running with --noauth_local_webserver.') else: code = raw_input('Enter verification code: ').strip() @@ -155,6 +155,6 @@ def run(flow, storage, http=None): storage.put(credential) credential.set_store(storage) - print 'Authentication successful.' + print('Authentication successful.') return credential diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py index 45d955b..1415d08 100644 --- a/oauth2client/service_account.py +++ b/oauth2client/service_account.py @@ -64,7 +64,7 @@ class _ServiceAccountCredentials(AssertionCredentials): 'kid': self._private_key_id } - now = long(time.time()) + now = int(time.time()) payload = { 'aud': self._token_uri, 'scope': self._scopes, @@ -77,14 +77,20 @@ class _ServiceAccountCredentials(AssertionCredentials): assertion_input = '%s.%s' % ( _urlsafe_b64encode(header), _urlsafe_b64encode(payload)) + assertion_input = assertion_input.encode('utf-8') # Sign the assertion. - signature = base64.urlsafe_b64encode(rsa.pkcs1.sign( - assertion_input, self._private_key, 'SHA-256')).rstrip('=') + signature = bytes.decode(base64.urlsafe_b64encode(rsa.pkcs1.sign( + assertion_input, self._private_key, 'SHA-256'))).rstrip('=') return '%s.%s' % (assertion_input, signature) def sign_blob(self, blob): + # Ensure that it is bytes + try: + blob = blob.encode('utf-8') + except AttributeError: + pass return (self._private_key_id, rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) @@ -119,7 +125,7 @@ class _ServiceAccountCredentials(AssertionCredentials): def _urlsafe_b64encode(data): return base64.urlsafe_b64encode( - json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip('=') + json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip(b'=') def _get_private_key(private_key_pkcs8_text): diff --git a/oauth2client/tools.py b/oauth2client/tools.py index da31d81..f7edd97 100644 --- a/oauth2client/tools.py +++ b/oauth2client/tools.py @@ -28,12 +28,14 @@ import BaseHTTPServer import logging import socket import sys -import urlparse import webbrowser +from six.moves import urllib + from oauth2client import client from oauth2client import util + _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 To make this sample run you will need to populate the client_secrets.json file @@ -88,7 +90,7 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.send_header("Content-type", "text/html") self.end_headers() query = self.path.split('?', 1)[-1] - query = dict(urlparse.parse_qsl(query)) + query = dict(urllib.parse.parse_qsl(query)) self.server.query_params = query self.wfile.write("Authentication Status") self.wfile.write("

The authentication flow has completed.

") @@ -155,20 +157,20 @@ def run_flow(flow, storage, flags, http=None): try: httpd = ClientRedirectServer((flags.auth_host_name, port), ClientRedirectHandler) - except socket.error as e: + except socket.error: pass else: success = True break flags.noauth_local_webserver = not success if not success: - print 'Failed to start a local webserver listening on either port 8080' - print 'or port 9090. Please check your firewall settings and locally' - print 'running programs that may be blocking or using those ports.' - print - print 'Falling back to --noauth_local_webserver and continuing with', - print 'authorization.' - print + print('Failed to start a local webserver listening on either port 8080') + print('or port 9090. Please check your firewall settings and locally') + print('running programs that may be blocking or using those ports.') + print() + print('Falling back to --noauth_local_webserver and continuing with') + print('authorization.') + print() if not flags.noauth_local_webserver: oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) @@ -179,20 +181,20 @@ def run_flow(flow, storage, flags, http=None): if not flags.noauth_local_webserver: webbrowser.open(authorize_url, new=1, autoraise=True) - print 'Your browser has been opened to visit:' - print - print ' ' + authorize_url - print - print 'If your browser is on a different machine then exit and re-run this' - print 'application with the command-line parameter ' - print - print ' --noauth_local_webserver' - print + print('Your browser has been opened to visit:') + print() + print(' ' + authorize_url) + print() + print('If your browser is on a different machine then exit and re-run this') + print('application with the command-line parameter ') + print() + print(' --noauth_local_webserver') + print() else: - print 'Go to the following link in your browser:' - print - print ' ' + authorize_url - print + print('Go to the following link in your browser:') + print() + print(' ' + authorize_url) + print() code = None if not flags.noauth_local_webserver: @@ -202,7 +204,7 @@ def run_flow(flow, storage, flags, http=None): if 'code' in httpd.query_params: code = httpd.query_params['code'] else: - print 'Failed to find "code" in the query parameters of the redirect.' + print('Failed to find "code" in the query parameters of the redirect.') sys.exit('Try running with --noauth_local_webserver.') else: code = raw_input('Enter verification code: ').strip() @@ -214,7 +216,7 @@ def run_flow(flow, storage, flags, http=None): storage.put(credential) credential.set_store(storage) - print 'Authentication successful.' + print('Authentication successful.') return credential diff --git a/oauth2client/util.py b/oauth2client/util.py index 292f1df..72b7e89 100644 --- a/oauth2client/util.py +++ b/oauth2client/util.py @@ -31,9 +31,12 @@ __all__ = [ import inspect import logging +import sys import types -import urllib -import urlparse + +import six +from six.moves import urllib + logger = logging.getLogger(__name__) @@ -129,7 +132,7 @@ def positional(max_positional_args): return wrapped(*args, **kwargs) return positional_wrapper - if isinstance(max_positional_args, (int, long)): + if isinstance(max_positional_args, six.integer_types): return positional_decorator else: args, _, _, defaults = inspect.getargspec(max_positional_args) @@ -149,7 +152,11 @@ def scopes_to_string(scopes): Returns: The scopes formatted as a single string. """ - if isinstance(scopes, types.StringTypes): + try: + is_string = isinstance(scopes, basestring) + except NameError: + is_string = isinstance(scopes, str) + if is_string: return scopes else: return ' '.join(scopes) @@ -186,8 +193,8 @@ def _add_query_parameter(url, name, value): if value is None: return url else: - parsed = list(urlparse.urlparse(url)) - q = dict(urlparse.parse_qsl(parsed[4])) + parsed = list(urllib.parse.urlparse(url)) + q = dict(urllib.parse.parse_qsl(parsed[4])) q[name] = value - parsed[4] = urllib.urlencode(q) - return urlparse.urlunparse(parsed) + parsed[4] = urllib.parse.urlencode(q) + return urllib.parse.urlunparse(parsed) diff --git a/oauth2client/xsrfutil.py b/oauth2client/xsrfutil.py index 1dc5b5c..3928dd0 100644 --- a/oauth2client/xsrfutil.py +++ b/oauth2client/xsrfutil.py @@ -1,4 +1,3 @@ -#!/usr/bin/python2.5 # # Copyright 2014 the Melange authors. # @@ -26,15 +25,27 @@ import base64 import hmac import time +import six from oauth2client import util # Delimiter character -DELIMITER = ':' +DELIMITER = b':' + # 1 hour in seconds DEFAULT_TIMEOUT_SECS = 1*60*60 + +def _force_bytes(s): + if isinstance(s, bytes): + return s + s = str(s) + if isinstance(s, six.text_type): + return s.encode('utf-8') + return s + + @util.positional(2) def generate_token(key, user_id, action_id="", when=None): """Generates a URL-safe token for the given user, action, time tuple. @@ -50,18 +61,16 @@ def generate_token(key, user_id, action_id="", when=None): Returns: A string XSRF protection token. """ - when = when or int(time.time()) - digester = hmac.new(key) - digester.update(str(user_id)) + when = _force_bytes(when or int(time.time())) + digester = hmac.new(_force_bytes(key)) + digester.update(_force_bytes(user_id)) digester.update(DELIMITER) - digester.update(action_id) + digester.update(_force_bytes(action_id)) digester.update(DELIMITER) - digester.update(str(when)) + digester.update(when) digest = digester.digest() - token = base64.urlsafe_b64encode('%s%s%d' % (digest, - DELIMITER, - when)) + token = base64.urlsafe_b64encode(digest + DELIMITER + when) return token @@ -86,8 +95,8 @@ def validate_token(key, token, user_id, action_id="", current_time=None): if not token: return False try: - decoded = base64.urlsafe_b64decode(str(token)) - token_time = long(decoded.split(DELIMITER)[-1]) + decoded = base64.urlsafe_b64decode(token) + token_time = int(decoded.split(DELIMITER)[-1]) except (TypeError, ValueError): return False if current_time is None: @@ -104,9 +113,6 @@ def validate_token(key, token, user_id, action_id="", current_time=None): # Perform constant time comparison to avoid timing attacks different = 0 - for x, y in zip(token, expected_token): - different |= ord(x) ^ ord(y) - if different: - return False - - return True + for x, y in zip(bytearray(token), bytearray(expected_token)): + different |= x ^ y + return different == 0 diff --git a/tests/http_mock.py b/tests/http_mock.py index 9e1ad4a..b059b5f 100644 --- a/tests/http_mock.py +++ b/tests/http_mock.py @@ -67,8 +67,8 @@ class HttpMockSequence(object): and content and then use as if an httplib2.Http instance. http = HttpMockSequence([ - ({'status': '401'}, ''), - ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), + ({'status': '401'}, b''), + ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'), ({'status': '200'}, 'echo_request_headers'), ]) resp, content = http.request("http://examples.com") @@ -111,4 +111,6 @@ class HttpMockSequence(object): content = body elif content == 'echo_request_uri': content = uri + elif not isinstance(content, bytes): + raise TypeError("http content should be bytes: %r" % (content,)) return httplib2.Response(resp), content diff --git a/tests/test_appengine.py b/tests/test_appengine.py index 84df9c3..7261ee6 100644 --- a/tests/test_appengine.py +++ b/tests/test_appengine.py @@ -46,7 +46,6 @@ from google.appengine.ext import db from google.appengine.ext import ndb from google.appengine.ext import testbed from google.appengine.runtime import apiproxy_errors -from http_mock import HttpMockSequence from oauth2client import appengine from oauth2client import GOOGLE_TOKEN_URI from oauth2client.clientsecrets import _loadfile diff --git a/tests/test_clientsecrets.py b/tests/test_clientsecrets.py index fa3e418..6212b37 100644 --- a/tests/test_clientsecrets.py +++ b/tests/test_clientsecrets.py @@ -19,7 +19,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import os import unittest -import StringIO +from io import StringIO import httplib2 @@ -57,6 +57,11 @@ class OAuth2CredentialsTests(unittest.TestCase): """, 'Property'), ] for src, match in ERRORS: + # Ensure that it is unicode + try: + src = src.decode('utf-8') + except AttributeError: + pass # Test load(s) try: clientsecrets.loads(src) @@ -66,7 +71,7 @@ class OAuth2CredentialsTests(unittest.TestCase): # Test loads(fp) try: - fp = StringIO.StringIO(src) + fp = StringIO(src) clientsecrets.load(fp) self.fail(src + ' should not be a valid client_secrets file.') except clientsecrets.InvalidClientSecretsError as e: @@ -104,25 +109,25 @@ class CachedClientsecretsTests(unittest.TestCase): def test_cache_miss(self): client_type, client_info = clientsecrets.loadfile( VALID_FILE, cache=self.cache_mock) - self.assertEquals('web', client_type) - self.assertEquals('foo_client_secret', client_info['client_secret']) + self.assertEqual('web', client_type) + self.assertEqual('foo_client_secret', client_info['client_secret']) cached = self.cache_mock.cache[VALID_FILE] - self.assertEquals({client_type: client_info}, cached) + self.assertEqual({client_type: client_info}, cached) # make sure we're using non-empty namespace ns = self.cache_mock.last_set_ns self.assertTrue(bool(ns)) # make sure they're equal - self.assertEquals(ns, self.cache_mock.last_get_ns) + self.assertEqual(ns, self.cache_mock.last_get_ns) def test_cache_hit(self): self.cache_mock.cache[NONEXISTENT_FILE] = { 'web': 'secret info' } client_type, client_info = clientsecrets.loadfile( NONEXISTENT_FILE, cache=self.cache_mock) - self.assertEquals('web', client_type) - self.assertEquals('secret info', client_info) + self.assertEqual('web', client_type) + self.assertEqual('secret info', client_info) # make sure we didn't do any set() RPCs self.assertEqual(None, self.cache_mock.last_set_ns) @@ -137,8 +142,8 @@ class CachedClientsecretsTests(unittest.TestCase): def test_without_cache(self): # this also ensures loadfile() is backward compatible client_type, client_info = clientsecrets.loadfile(VALID_FILE) - self.assertEquals('web', client_type) - self.assertEquals('foo_client_secret', client_info['client_secret']) + self.assertEqual('web', client_type) + self.assertEqual('foo_client_secret', client_info['client_secret']) if __name__ == '__main__': diff --git a/tests/test_django_orm.py b/tests/test_django_orm.py index 38318b2..02c0b69 100644 --- a/tests/test_django_orm.py +++ b/tests/test_django_orm.py @@ -40,6 +40,8 @@ from oauth2client.client import Credentials from oauth2client.client import Flow # Mock a Django environment +from django.conf import global_settings +global_settings.SECRET_KEY = 'NotASecret' os.environ['DJANGO_SETTINGS_MODULE'] = 'django_settings' sys.modules['django_settings'] = django_settings = imp.new_module('django_settings') django_settings.SECRET_KEY = 'xyzzy' diff --git a/tests/test_file.py b/tests/test_file.py index 02c7a61..ac81bce 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -32,7 +32,6 @@ import stat import tempfile import unittest -from http_mock import HttpMockSequence from oauth2client import GOOGLE_TOKEN_URI from oauth2client import file from oauth2client import locked_file @@ -41,6 +40,11 @@ from oauth2client import util from oauth2client.client import AccessTokenCredentials from oauth2client.client import AssertionCredentials from oauth2client.client import OAuth2Credentials +try: + # Python2 + from future_builtins import oct +except: + pass FILENAME = tempfile.mktemp('oauth2client_test.data') @@ -96,7 +100,7 @@ class OAuth2ClientFileTests(unittest.TestCase): # Write a file with a pickled OAuth2Credentials. credentials = self.create_test_credentials() - f = open(FILENAME, 'w') + f = open(FILENAME, 'wb') pickle.dump(credentials, f) f.close() @@ -154,13 +158,13 @@ class OAuth2ClientFileTests(unittest.TestCase): mode = os.stat(FILENAME).st_mode if os.name == 'posix': - self.assertEquals('0600', oct(stat.S_IMODE(os.stat(FILENAME).st_mode))) + self.assertEquals('0o600', oct(stat.S_IMODE(os.stat(FILENAME).st_mode))) def test_read_only_file_fail_lock(self): credentials = self.create_test_credentials() open(FILENAME, 'a+b').close() - os.chmod(FILENAME, 0400) + os.chmod(FILENAME, 0o400) store = multistore_file.get_credential_storage( FILENAME, @@ -171,7 +175,7 @@ class OAuth2ClientFileTests(unittest.TestCase): store.put(credentials) if os.name == 'posix': self.assertTrue(store._multistore._read_only) - os.chmod(FILENAME, 0600) + os.chmod(FILENAME, 0o600) def test_multistore_no_symbolic_link_files(self): if hasattr(os, 'symlink'): @@ -221,7 +225,7 @@ class OAuth2ClientFileTests(unittest.TestCase): self.assertEquals(None, credentials) if os.name == 'posix': - self.assertEquals('0600', oct(stat.S_IMODE(os.stat(FILENAME).st_mode))) + self.assertEquals('0o600', oct(stat.S_IMODE(os.stat(FILENAME).st_mode))) def test_multistore_file_custom_key(self): credentials = self.create_test_credentials() diff --git a/tests/test_gce.py b/tests/test_gce.py index 5d0721d..a700c81 100644 --- a/tests/test_gce.py +++ b/tests/test_gce.py @@ -21,7 +21,10 @@ Unit tests for oauth2client.gce. __author__ = 'jcgregorio@google.com (Joe Gregorio)' import httplib2 -import mox +try: + from mox3 import mox +except ImportError: + import mox import unittest from oauth2client.client import AccessTokenRefreshError diff --git a/tests/test_jwt.py b/tests/test_jwt.py index 1161ccd..169e5f8 100644 --- a/tests/test_jwt.py +++ b/tests/test_jwt.py @@ -23,11 +23,12 @@ Unit tests for oauth2client. __author__ = 'jcgregorio@google.com (Joe Gregorio)' import os +import sys import tempfile import time import unittest -from http_mock import HttpMockSequence +from .http_mock import HttpMockSequence from oauth2client import crypt from oauth2client.client import Credentials from oauth2client.client import SignedJwtAssertionCredentials @@ -39,7 +40,7 @@ from oauth2client.file import Storage def datafile(filename): - f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'r') + f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'rb') data = f.read() f.close() return data @@ -67,11 +68,10 @@ class CryptTests(unittest.TestCase): signature = signer.sign('foo') verifier = self.verifier.from_string(public_key, True) + self.assertTrue(verifier.verify(b'foo', signature)) - self.assertTrue(verifier.verify('foo', signature)) - - self.assertFalse(verifier.verify('bar', signature)) - self.assertFalse(verifier.verify('foo', 'bad signature')) + self.assertFalse(verifier.verify(b'bar', signature)) + self.assertFalse(verifier.verify(b'foo', 'bad signagure')) def _check_jwt_failure(self, jwt, expected_error): public_key = datafile('publickey.pem') @@ -88,7 +88,7 @@ class CryptTests(unittest.TestCase): private_key = datafile('privatekey.%s' % self.format) signer = self.signer.from_string(private_key) audience = 'some_audience_address@testing.gserviceaccount.com' - now = long(time.time()) + now = int(time.time()) return crypt.make_signed_jwt(signer, { 'aud': audience, @@ -212,11 +212,11 @@ class SignedJwtAssertionCredentialsTests(unittest.TestCase): scope='read+write', sub='joe@example.org') http = HttpMockSequence([ - ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), + ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'), ({'status': '200'}, 'echo_request_headers'), ]) http = credentials.authorize(http) - _, content = http.request('http://example.org') + resp, content = http.request('http://example.org') self.assertEqual('Bearer 1/3w', content['Authorization']) def test_credentials_to_from_json(self): @@ -235,9 +235,9 @@ class SignedJwtAssertionCredentialsTests(unittest.TestCase): def _credentials_refresh(self, credentials): http = HttpMockSequence([ - ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), - ({'status': '401'}, ''), - ({'status': '200'}, '{"access_token":"3/3w","expires_in":3600}'), + ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'), + ({'status': '401'}, b''), + ({'status': '200'}, b'{"access_token":"3/3w","expires_in":3600}'), ({'status': '200'}, 'echo_request_headers'), ]) http = credentials.authorize(http) diff --git a/tests/test_keyring.py b/tests/test_keyring.py index d8b366c..0c74516 100644 --- a/tests/test_keyring.py +++ b/tests/test_keyring.py @@ -23,7 +23,10 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import datetime import keyring import unittest -import mox +try: + from mox3 import mox +except ImportError: + import mox from oauth2client import GOOGLE_TOKEN_URI from oauth2client.client import OAuth2Credentials diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py old mode 100644 new mode 100755 index 2db3e3c..bb85c88 --- a/tests/test_oauth2client.py +++ b/tests/test_oauth2client.py @@ -25,14 +25,18 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import base64 import datetime import json -import mox +try: + from mox3 import mox +except ImportError: + import mox import os import time import unittest -import urlparse +import six +from six.moves import urllib -from http_mock import HttpMock -from http_mock import HttpMockSequence +from .http_mock import HttpMock +from .http_mock import HttpMockSequence from oauth2client import GOOGLE_REVOKE_URI from oauth2client import GOOGLE_TOKEN_URI from oauth2client.client import AccessTokenCredentials @@ -79,15 +83,15 @@ DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') # googleapiclient.test_discovery; consolidate these definitions. def assertUrisEqual(testcase, expected, actual): """Test that URIs are the same, up to reordering of query parameters.""" - expected = urlparse.urlparse(expected) - actual = urlparse.urlparse(actual) + expected = urllib.parse.urlparse(expected) + actual = urllib.parse.urlparse(actual) testcase.assertEqual(expected.scheme, actual.scheme) testcase.assertEqual(expected.netloc, actual.netloc) testcase.assertEqual(expected.path, actual.path) testcase.assertEqual(expected.params, actual.params) testcase.assertEqual(expected.fragment, actual.fragment) - expected_query = urlparse.parse_qs(expected.query) - actual_query = urlparse.parse_qs(actual.query) + expected_query = urllib.parse.parse_qs(expected.query) + actual_query = urllib.parse.parse_qs(actual.query) for name in expected_query.keys(): testcase.assertEqual(expected_query[name], actual_query[name]) for name in actual_query.keys(): @@ -534,8 +538,8 @@ class BasicCredentialsTests(unittest.TestCase): for status_code in REFRESH_STATUS_CODES: token_response = {'access_token': '1/3w', 'expires_in': 3600} http = HttpMockSequence([ - ({'status': status_code}, ''), - ({'status': '200'}, json.dumps(token_response)), + ({'status': status_code}, b''), + ({'status': '200'}, json.dumps(token_response).encode('utf-8')), ({'status': '200'}, 'echo_request_headers'), ]) http = self.credentials.authorize(http) @@ -547,8 +551,8 @@ class BasicCredentialsTests(unittest.TestCase): def test_token_refresh_failure(self): for status_code in REFRESH_STATUS_CODES: http = HttpMockSequence([ - ({'status': status_code}, ''), - ({'status': '400'}, '{"error":"access_denied"}'), + ({'status': status_code}, b''), + ({'status': '400'}, b'{"error":"access_denied"}'), ]) http = self.credentials.authorize(http) try: @@ -571,7 +575,7 @@ class BasicCredentialsTests(unittest.TestCase): def test_non_401_error_response(self): http = HttpMockSequence([ - ({'status': '400'}, ''), + ({'status': '400'}, b''), ]) http = self.credentials.authorize(http) resp, content = http.request('http://example.com') @@ -598,9 +602,9 @@ class BasicCredentialsTests(unittest.TestCase): client_id = u'some_client_id' client_secret = u'cOuDdkfjxxnv+' refresh_token = u'1/0/a.df219fjls0' - token_expiry = unicode(datetime.datetime.utcnow()) - token_uri = unicode(GOOGLE_TOKEN_URI) - revoke_uri = unicode(GOOGLE_REVOKE_URI) + token_expiry = str(datetime.datetime.utcnow()) + token_uri = str(GOOGLE_TOKEN_URI) + revoke_uri = str(GOOGLE_REVOKE_URI) user_agent = u'refresh_checker/1.0' credentials = OAuth2Credentials(access_token, client_id, client_secret, refresh_token, token_expiry, token_uri, @@ -609,7 +613,7 @@ class BasicCredentialsTests(unittest.TestCase): http = HttpMock(headers={'status': '200'}) http = credentials.authorize(http) http.request(u'http://example.com', method=u'GET', headers={u'foo': u'bar'}) - for k, v in http.headers.iteritems(): + for k, v in six.iteritems(http.headers): self.assertEqual(str, type(k)) self.assertEqual(str, type(v)) @@ -630,8 +634,8 @@ class BasicCredentialsTests(unittest.TestCase): token_response_first = {'access_token': 'first_token', 'expires_in': S} token_response_second = {'access_token': 'second_token', 'expires_in': S} http = HttpMockSequence([ - ({'status': '200'}, json.dumps(token_response_first)), - ({'status': '200'}, json.dumps(token_response_second)), + ({'status': '200'}, json.dumps(token_response_first).encode('utf-8')), + ({'status': '200'}, json.dumps(token_response_second).encode('utf-8')), ]) token = self.credentials.get_access_token(http=http) @@ -667,7 +671,7 @@ class AccessTokenCredentialsTests(unittest.TestCase): def test_token_refresh_success(self): for status_code in REFRESH_STATUS_CODES: http = HttpMockSequence([ - ({'status': status_code}, ''), + ({'status': status_code}, b''), ]) http = self.credentials.authorize(http) try: @@ -690,7 +694,7 @@ class AccessTokenCredentialsTests(unittest.TestCase): def test_non_401_error_response(self): http = HttpMockSequence([ - ({'status': '400'}, ''), + ({'status': '400'}, b''), ]) http = self.credentials.authorize(http) resp, content = http.request('http://example.com') @@ -720,14 +724,15 @@ class TestAssertionCredentials(unittest.TestCase): user_agent=user_agent) def test_assertion_body(self): - body = urlparse.parse_qs(self.credentials._generate_refresh_request_body()) + body = urllib.parse.parse_qs( + self.credentials._generate_refresh_request_body()) self.assertEqual(self.assertion_text, body['assertion'][0]) self.assertEqual('urn:ietf:params:oauth:grant-type:jwt-bearer', body['grant_type'][0]) def test_assertion_refresh(self): http = HttpMockSequence([ - ({'status': '200'}, '{"access_token":"1/3w"}'), + ({'status': '200'}, b'{"access_token":"1/3w"}'), ({'status': '200'}, 'echo_request_headers'), ]) http = self.credentials.authorize(http) @@ -792,8 +797,8 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_construct_authorize_url(self): authorize_url = self.flow.step1_get_authorize_url() - parsed = urlparse.urlparse(authorize_url) - q = urlparse.parse_qs(parsed[4]) + parsed = urllib.parse.urlparse(authorize_url) + q = urllib.parse.parse_qs(parsed[4]) self.assertEqual('client_id+1', q['client_id'][0]) self.assertEqual('code', q['response_type'][0]) self.assertEqual('foo', q['scope'][0]) @@ -813,8 +818,8 @@ class OAuth2WebServerFlowTest(unittest.TestCase): ) authorize_url = flow.step1_get_authorize_url() - parsed = urlparse.urlparse(authorize_url) - q = urlparse.parse_qs(parsed[4]) + parsed = urllib.parse.urlparse(authorize_url) + q = urllib.parse.parse_qs(parsed[4]) self.assertEqual('client_id+1', q['client_id'][0]) self.assertEqual('token', q['response_type'][0]) self.assertEqual('foo', q['scope'][0]) @@ -823,7 +828,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_exchange_failure(self): http = HttpMockSequence([ - ({'status': '400'}, '{"error":"invalid_request"}'), + ({'status': '400'}, b'{"error":"invalid_request"}'), ]) try: @@ -850,9 +855,9 @@ class OAuth2WebServerFlowTest(unittest.TestCase): # exceptions are being raised instead of FlowExchangeError. http = HttpMockSequence([ ({'status': '400'}, - """ {"error": { - "type": "OAuthException", - "message": "Error validating verification code."} }"""), + b""" {"error": { + "type": "OAuthException", + "message": "Error validating verification code."} }"""), ]) try: @@ -864,7 +869,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_exchange_success(self): http = HttpMockSequence([ ({'status': '200'}, - """{ "access_token":"SlAV32hkKG", + b"""{ "access_token":"SlAV32hkKG", "expires_in":3600, "refresh_token":"8xLOxBtZp8" }"""), ]) @@ -905,7 +910,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_urlencoded_exchange_success(self): http = HttpMockSequence([ - ({'status': '200'}, 'access_token=SlAV32hkKG&expires_in=3600'), + ({'status': '200'}, b'access_token=SlAV32hkKG&expires_in=3600'), ]) credentials = self.flow.step2_exchange('some random code', http=http) @@ -916,7 +921,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): http = HttpMockSequence([ # Note the 'expires=3600' where you'd normally # have if named 'expires_in' - ({'status': '200'}, 'access_token=SlAV32hkKG&expires=3600'), + ({'status': '200'}, b'access_token=SlAV32hkKG&expires=3600'), ]) credentials = self.flow.step2_exchange('some random code', http=http) @@ -924,7 +929,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_exchange_no_expires_in(self): http = HttpMockSequence([ - ({'status': '200'}, """{ "access_token":"SlAV32hkKG", + ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG", "refresh_token":"8xLOxBtZp8" }"""), ]) @@ -935,7 +940,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): http = HttpMockSequence([ # This might be redundant but just to make sure # urlencoded access_token gets parsed correctly - ({'status': '200'}, 'access_token=SlAV32hkKG'), + ({'status': '200'}, b'access_token=SlAV32hkKG'), ]) credentials = self.flow.step2_exchange('some random code', http=http) @@ -943,7 +948,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_exchange_fails_if_no_code(self): http = HttpMockSequence([ - ({'status': '200'}, """{ "access_token":"SlAV32hkKG", + ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG", "refresh_token":"8xLOxBtZp8" }"""), ]) @@ -956,7 +961,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase): def test_exchange_id_token_fail(self): http = HttpMockSequence([ - ({'status': '200'}, """{ "access_token":"SlAV32hkKG", + ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG", "refresh_token":"8xLOxBtZp8", "id_token": "stuff.payload"}"""), ]) @@ -971,9 +976,9 @@ class OAuth2WebServerFlowTest(unittest.TestCase): base64.urlsafe_b64encode('signature')) http = HttpMockSequence([ - ({'status': '200'}, """{ "access_token":"SlAV32hkKG", + ({'status': '200'}, ("""{ "access_token":"SlAV32hkKG", "refresh_token":"8xLOxBtZp8", - "id_token": "%s"}""" % jwt), + "id_token": "%s"}""" % jwt).encode('utf-8')), ]) credentials = self.flow.step2_exchange('some random code', http=http) @@ -1003,7 +1008,7 @@ class CredentialsFromCodeTests(unittest.TestCase): token = 'asdfghjkl' payload = json.dumps({'access_token': token, 'expires_in': 3600}) http = HttpMockSequence([ - ({'status': '200'}, payload), + ({'status': '200'}, payload.encode('utf-8')), ]) credentials = credentials_from_code(self.client_id, self.client_secret, self.scope, self.code, redirect_uri=self.redirect_uri, @@ -1013,7 +1018,7 @@ class CredentialsFromCodeTests(unittest.TestCase): def test_exchange_code_for_token_fail(self): http = HttpMockSequence([ - ({'status': '400'}, '{"error":"invalid_request"}'), + ({'status': '400'}, b'{"error":"invalid_request"}'), ]) try: @@ -1027,7 +1032,7 @@ class CredentialsFromCodeTests(unittest.TestCase): def test_exchange_code_and_file_for_token(self): http = HttpMockSequence([ ({'status': '200'}, - """{ "access_token":"asdfghjkl", + b"""{ "access_token":"asdfghjkl", "expires_in":3600 }"""), ]) credentials = credentials_from_clientsecrets_and_code( @@ -1038,7 +1043,7 @@ class CredentialsFromCodeTests(unittest.TestCase): def test_exchange_code_and_cached_file_for_token(self): http = HttpMockSequence([ - ({'status': '200'}, '{ "access_token":"asdfghjkl"}'), + ({'status': '200'}, b'{ "access_token":"asdfghjkl"}'), ]) cache_mock = CacheMock() load_and_cache('client_secrets.json', 'some_secrets', cache_mock) @@ -1050,7 +1055,7 @@ class CredentialsFromCodeTests(unittest.TestCase): def test_exchange_code_and_file_for_token_fail(self): http = HttpMockSequence([ - ({'status': '400'}, '{"error":"invalid_request"}'), + ({'status': '400'}, b'{"error":"invalid_request"}'), ]) try: diff --git a/tests/test_service_account.py b/tests/test_service_account.py index 0455a7e..4b9dce9 100644 --- a/tests/test_service_account.py +++ b/tests/test_service_account.py @@ -26,13 +26,13 @@ import rsa import time import unittest -from http_mock import HttpMockSequence +from .http_mock import HttpMockSequence from oauth2client.service_account import _ServiceAccountCredentials def datafile(filename): # TODO(orestica): Refactor this using pkgutil.get_data - f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'r') + f = open(os.path.join(os.path.dirname(__file__), 'data', filename), 'rb') data = f.read() f.close() return data @@ -58,16 +58,16 @@ class ServiceAccountCredentialsTests(unittest.TestCase): pub_key = rsa.PublicKey.load_pkcs1_openssl_pem( datafile('publickey_openssl.pem')) - self.assertTrue(rsa.pkcs1.verify('Google', signature, pub_key)) + self.assertTrue(rsa.pkcs1.verify(b'Google', signature, pub_key)) try: - rsa.pkcs1.verify('Orest', signature, pub_key) + rsa.pkcs1.verify(b'Orest', signature, pub_key) self.fail('Verification should have failed!') except rsa.pkcs1.VerificationError: pass # Expected try: - rsa.pkcs1.verify('Google', 'bad signature', pub_key) + rsa.pkcs1.verify(b'Google', b'bad signature', pub_key) self.fail('Verification should have failed!') except rsa.pkcs1.VerificationError: pass # Expected @@ -98,8 +98,8 @@ class ServiceAccountCredentialsTests(unittest.TestCase): token_response_first = {'access_token': 'first_token', 'expires_in': S} token_response_second = {'access_token': 'second_token', 'expires_in': S} http = HttpMockSequence([ - ({'status': '200'}, json.dumps(token_response_first)), - ({'status': '200'}, json.dumps(token_response_second)), + ({'status': '200'}, json.dumps(token_response_first).encode('utf-8')), + ({'status': '200'}, json.dumps(token_response_second).encode('utf-8')), ]) token = self.credentials.get_access_token(http=http) diff --git a/tests/test_xsrfutil.py b/tests/test_xsrfutil.py index a2a379e..5825b5e 100644 --- a/tests/test_xsrfutil.py +++ b/tests/test_xsrfutil.py @@ -96,7 +96,7 @@ class XsrfUtilTests(unittest.TestCase): # Invalid with extra garbage self.assertFalse(xsrfutil.validate_token(TEST_KEY, - token + 'x', + token + b'x', TEST_USER_ID_1, action_id=TEST_ACTION_ID_1, current_time=later15mins)) diff --git a/tox.ini b/tox.ini index 0f54225..5c48fb6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,60 @@ [tox] -envlist = py26, py27 +envlist = py26openssl13, py26openssl14, + py27openssl13, py27openssl14, + py33openssl14, + py34openssl14, + pypyopenssl13, pypyopenssl14 [testenv] deps = keyring - mox - pyopenssl + mox3 pycrypto==2.6 django>=1.5,<1.6 webtest nose setenv = PYTHONPATH=../google_appengine commands = nosetests --ignore-files=test_appengine\.py + +# whitelist +branches: + only: + - master + - python3 + +[testenv:py26openssl13] +basepython = python2.6 +deps = {[testenv]deps} + pyopenssl<0.14 + +[testenv:py26openssl14] +basepython = python2.6 +deps = {[testenv]deps} + pyopenssl==0.14 + +[testenv:py27openssl13] +basepython = python2.7 +deps = {[testenv]deps} + pyopenssl<0.14 + +[testenv:py27openssl14] +basepython = python2.7 +deps = {[testenv]deps} + pyopenssl==0.14 + +[testenv:py33openssl14] +basepython = python3.3 +deps = {[testenv]deps} + pyopenssl==0.14 + +[testenv:py34openssl14] +basepython = python3.4 +deps = {[testenv]deps} + pyopenssl==0.14 + +[testenv:pypyopenssl13] +deps = {[testenv]deps} + pyopenssl<0.14 + +[testenv:pypyopenssl14] +deps = {[testenv]deps} + pyopenssl==0.14