Merge branch 'python3' into merge

This commit is contained in:
Craig Citro
2014-11-04 11:17:01 -08:00
26 changed files with 361 additions and 234 deletions

View File

@@ -1,12 +1,16 @@
language: python language: python
python: 2.7 python: 2.7
env: env:
- TOX_ENV=py26 - TOX_ENV=py26openssl13
- TOX_ENV=py27 - TOX_ENV=py26openssl14
- TOX_ENV=pypy - TOX_ENV=py27openssl13
- TOX_ENV=py27openssl14
- TOX_ENV=py33openssl14
- TOX_ENV=py34openssl14
- TOX_ENV=pypyopenssl13
- TOX_ENV=pypyopenssl14
install: install:
- pip install tox - pip install tox
- pip install .
script: script:
- tox -e $TOX_ENV - tox -e $TOX_ENV
notifications: notifications:

View File

@@ -1,5 +1,11 @@
[![Build Status](https://travis-ci.org/google/oauth2client.svg?branch=master)](https://travis-ci.org/google/oauth2client) [![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. This is a client library for accessing resources protected by OAuth 2.0.
[Full documentation](http://google.github.io/oauth2client/) [Full documentation](http://google.github.io/oauth2client/)

View File

@@ -28,8 +28,8 @@ import logging
import os import os
import sys import sys
import time import time
import urllib import six
import urlparse from six.moves import urllib
import httplib2 import httplib2
from oauth2client import clientsecrets from oauth2client import clientsecrets
@@ -231,6 +231,9 @@ class Credentials(object):
# Add in information we will need later to reconsistitue this instance. # Add in information we will need later to reconsistitue this instance.
d['_class'] = t.__name__ d['_class'] = t.__name__
d['_module'] = t.__module__ d['_module'] = t.__module__
for key, val in d.items():
if isinstance(val, bytes):
d[key] = val.decode('utf-8')
return json.dumps(d) return json.dumps(d)
def to_json(self): def to_json(self):
@@ -254,6 +257,8 @@ class Credentials(object):
An instance of the subclass of Credentials that was serialized with An instance of the subclass of Credentials that was serialized with
to_json(). to_json().
""" """
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s) data = json.loads(s)
# Find and call the right classmethod from_json() to restore the object. # Find and call the right classmethod from_json() to restore the object.
module = data['_module'] module = data['_module']
@@ -398,7 +403,7 @@ def clean_headers(headers):
""" """
clean = {} clean = {}
try: try:
for k, v in headers.iteritems(): for k, v in six.iteritems(headers):
clean[str(k)] = str(v) clean[str(k)] = str(v)
except UnicodeEncodeError: except UnicodeEncodeError:
raise NonAsciiHeaderError(k + ': ' + v) raise NonAsciiHeaderError(k + ': ' + v)
@@ -415,11 +420,11 @@ def _update_query_params(uri, params):
Returns: Returns:
The same URI but with the new query parameters added. The same URI but with the new query parameters added.
""" """
parts = urlparse.urlparse(uri) parts = urllib.parse.urlparse(uri)
query_params = dict(urlparse.parse_qsl(parts.query)) query_params = dict(urllib.parse.parse_qsl(parts.query))
query_params.update(params) query_params.update(params)
new_parts = parts._replace(query=urllib.urlencode(query_params)) new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
return urlparse.urlunparse(new_parts) return urllib.parse.urlunparse(new_parts)
class OAuth2Credentials(Credentials): class OAuth2Credentials(Credentials):
@@ -589,6 +594,8 @@ class OAuth2Credentials(Credentials):
Returns: Returns:
An instance of a Credentials subclass. An instance of a Credentials subclass.
""" """
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s) data = json.loads(s)
if (data.get('token_expiry') and if (data.get('token_expiry') and
not isinstance(data['token_expiry'], datetime.datetime)): not isinstance(data['token_expiry'], datetime.datetime)):
@@ -691,7 +698,7 @@ class OAuth2Credentials(Credentials):
def _generate_refresh_request_body(self): def _generate_refresh_request_body(self):
"""Generate the body that will be used in the refresh request.""" """Generate the body that will be used in the refresh request."""
body = urllib.urlencode({ body = urllib.parse.urlencode({
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
@@ -755,8 +762,9 @@ class OAuth2Credentials(Credentials):
logger.info('Refreshing access_token') logger.info('Refreshing access_token')
resp, content = http_request( resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers) self.token_uri, method='POST', body=body, headers=headers)
if six.PY3:
content = content.decode('utf-8')
if resp.status == 200: if resp.status == 200:
# TODO(jcgregorio) Raise an error if loads fails?
d = json.loads(content) d = json.loads(content)
self.token_response = d self.token_response = d
self.access_token = d['access_token'] self.access_token = d['access_token']
@@ -785,7 +793,7 @@ class OAuth2Credentials(Credentials):
self.invalid = True self.invalid = True
if self.store: if self.store:
self.store.locked_put(self) self.store.locked_put(self)
except StandardError: except (TypeError, ValueError):
pass pass
raise AccessTokenRefreshError(error_msg) raise AccessTokenRefreshError(error_msg)
@@ -822,7 +830,7 @@ class OAuth2Credentials(Credentials):
d = json.loads(content) d = json.loads(content)
if 'error' in d: if 'error' in d:
error_msg = d['error'] error_msg = d['error']
except StandardError: except (TypeError, ValueError):
pass pass
raise TokenRevokeError(error_msg) raise TokenRevokeError(error_msg)
@@ -880,10 +888,12 @@ class AccessTokenCredentials(OAuth2Credentials):
@classmethod @classmethod
def from_json(cls, s): def from_json(cls, s):
if six.PY3 and isinstance(s, bytes):
s = s.decode('utf-8')
data = json.loads(s) data = json.loads(s)
retval = AccessTokenCredentials( retval = AccessTokenCredentials(
data['access_token'], data['access_token'],
data['user_agent']) data['user_agent'])
return retval return retval
def _refresh(self, http_request): def _refresh(self, http_request):
@@ -903,7 +913,7 @@ class AccessTokenCredentials(OAuth2Credentials):
_env_name = None _env_name = None
def _get_environment(urllib2_urlopen=None): def _get_environment(urlopen=None):
"""Detect the environment the code is being run on.""" """Detect the environment the code is being run on."""
global _env_name global _env_name
@@ -917,16 +927,15 @@ def _get_environment(urllib2_urlopen=None):
elif server_software.startswith('Development/'): elif server_software.startswith('Development/'):
_env_name = 'GAE_LOCAL' _env_name = 'GAE_LOCAL'
else: else:
import urllib2
try: try:
if urllib2_urlopen is None: if urlopen is None:
urllib2_urlopen = urllib2.urlopen urlopen = urllib.request.urlopen
response = urllib2_urlopen('http://metadata.google.internal') response = urlopen('http://metadata.google.internal')
if any('Metadata-Flavor: Google' in h for h in response.info().headers): if any('Metadata-Flavor: Google' in h for h in response.info().headers):
_env_name = 'GCE_PRODUCTION' _env_name = 'GCE_PRODUCTION'
else: else:
_env_name = 'UNKNOWN' _env_name = 'UNKNOWN'
except urllib2.URLError: except urllib.error.URLError:
_env_name = 'UNKNOWN' _env_name = 'UNKNOWN'
return _env_name return _env_name
@@ -956,7 +965,7 @@ class GoogleCredentials(OAuth2Credentials):
request = service.instances().list(project=PROJECT, zone=ZONE) request = service.instances().list(project=PROJECT, zone=ZONE)
response = request.execute() response = request.execute()
print response print(response)
</code> </code>
A service that does not require authentication does not need credentials A service that does not require authentication does not need credentials
@@ -970,7 +979,7 @@ class GoogleCredentials(OAuth2Credentials):
request = service.apis().list() request = service.apis().list()
response = request.execute() response = request.execute()
print response print(response)
</code> </code>
""" """
@@ -1168,7 +1177,7 @@ def _get_application_default_credential_from_file(
application_default_credential_filename): application_default_credential_filename):
"""Build the Application Default Credentials from file.""" """Build the Application Default Credentials from file."""
import service_account from oauth2client import service_account
# read the credentials from the file # read the credentials from the file
with open(application_default_credential_filename) as ( with open(application_default_credential_filename) as (
@@ -1274,7 +1283,7 @@ class AssertionCredentials(GoogleCredentials):
def _generate_refresh_request_body(self): def _generate_refresh_request_body(self):
assertion = self._generate_assertion() assertion = self._generate_assertion()
body = urllib.urlencode({ body = urllib.parse.urlencode({
'assertion': assertion, 'assertion': assertion,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', '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. # Keep base64 encoded so it can be stored in JSON.
self.private_key = base64.b64encode(private_key) 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.private_key_password = private_key_password
self.service_account_name = service_account_name self.service_account_name = service_account_name
@@ -1386,7 +1397,7 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
def _generate_assertion(self): def _generate_assertion(self):
"""Generate the assertion that will be used in the request.""" """Generate the assertion that will be used in the request."""
now = long(time.time()) now = int(time.time())
payload = { payload = {
'aud': self.token_uri, 'aud': self.token_uri,
'scope': self.scope, 'scope': self.scope,
@@ -1435,7 +1446,7 @@ def verify_id_token(id_token, audience, http=None,
resp, content = http.request(cert_uri) resp, content = http.request(cert_uri)
if resp.status == 200: 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) return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
else: else:
raise VerifyJwtTokenError('Status code: %d' % resp.status) raise VerifyJwtTokenError('Status code: %d' % resp.status)
@@ -1443,8 +1454,9 @@ def verify_id_token(id_token, audience, http=None,
def _urlsafe_b64decode(b64string): def _urlsafe_b64decode(b64string):
# Guard against unicode strings, which base64 can't handle. # Guard against unicode strings, which base64 can't handle.
b64string = b64string.encode('ascii') if isinstance(b64string, six.text_type):
padded = b64string + '=' * (4 - len(b64string) % 4) b64string = b64string.encode('ascii')
padded = b64string + b'=' * (4 - len(b64string) % 4)
return base64.urlsafe_b64decode(padded) return base64.urlsafe_b64decode(padded)
@@ -1465,7 +1477,7 @@ def _extract_id_token(id_token):
raise VerifyJwtTokenError( raise VerifyJwtTokenError(
'Wrong number of segments in token: %s' % id_token) '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): def _parse_exchange_token_response(content):
@@ -1483,11 +1495,11 @@ def _parse_exchange_token_response(content):
""" """
resp = {} resp = {}
try: try:
resp = json.loads(content) resp = json.loads(content.decode('utf-8'))
except StandardError: except Exception:
# different JSON libs raise different exceptions, # different JSON libs raise different exceptions,
# so we just do a catch-all here # 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' # some providers respond with 'expires', others with 'expires_in'
if resp and 'expires' in resp: if resp and 'expires' in resp:
@@ -1810,7 +1822,7 @@ class OAuth2WebServerFlow(Flow):
else: else:
post_data['grant_type'] = 'authorization_code' post_data['grant_type'] = 'authorization_code'
post_data['redirect_uri'] = self.redirect_uri post_data['redirect_uri'] = self.redirect_uri
body = urllib.urlencode(post_data) body = urllib.parse.urlencode(post_data)
headers = { headers = {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
} }
@@ -1851,7 +1863,7 @@ class OAuth2WebServerFlow(Flow):
logger.info('Failed to retrieve access token: %s', content) logger.info('Failed to retrieve access token: %s', content)
if 'error' in d: if 'error' in d:
# you never know what those providers got to say # you never know what those providers got to say
error_msg = unicode(d['error']) error_msg = str(d['error'])
else: else:
error_msg = 'Invalid response: %s.' % str(resp.status) error_msg = 'Invalid response: %s.' % str(resp.status)
raise FlowExchangeError(error_msg) raise FlowExchangeError(error_msg)

View File

@@ -21,6 +21,7 @@ an OAuth 2.0 protected service.
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import json import json
import six
# Properties that make a client_secrets.json file valid. # Properties that make a client_secrets.json file valid.
@@ -70,9 +71,9 @@ class InvalidClientSecretsError(Error):
def _validate_clientsecrets(obj): def _validate_clientsecrets(obj):
if obj is None or len(obj) != 1: if obj is None or len(obj) != 1:
raise InvalidClientSecretsError('Invalid file format.') raise InvalidClientSecretsError('Invalid file format.')
client_type = obj.keys()[0] client_type = tuple(obj)[0]
if client_type not in VALID_CLIENT.keys(): if client_type not in VALID_CLIENT:
raise InvalidClientSecretsError('Unknown client type: %s.' % client_type) raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,))
client_info = obj[client_type] client_info = obj[client_type]
for prop_name in VALID_CLIENT[client_type]['required']: for prop_name in VALID_CLIENT[client_type]['required']:
if prop_name not in client_info: if prop_name not in client_info:
@@ -98,11 +99,8 @@ def loads(s):
def _loadfile(filename): def _loadfile(filename):
try: try:
fp = file(filename, 'r') with open(filename, 'r') as fp:
try:
obj = json.load(fp) obj = json.load(fp)
finally:
fp.close()
except IOError: except IOError:
raise InvalidClientSecretsError('File not found: "%s"' % filename) raise InvalidClientSecretsError('File not found: "%s"' % filename)
return _validate_clientsecrets(obj) return _validate_clientsecrets(obj)
@@ -150,4 +148,4 @@ def loadfile(filename, cache=None):
obj = {client_type: client_info} obj = {client_type: client_info}
cache.set(filename, obj, namespace=_SECRET_NAMESPACE) cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
return obj.iteritems().next() return next(six.iteritems(obj))

View File

@@ -18,8 +18,11 @@
import base64 import base64
import json import json
import logging import logging
import sys
import time import time
import six
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
AUTH_TOKEN_LIFETIME_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. key that this object was constructed with.
""" """
try: try:
if isinstance(message, six.text_type):
message = message.encode('utf-8')
crypto.verify(self._pubkey, signature, message, 'sha256') crypto.verify(self._pubkey, signature, message, 'sha256')
return True return True
except: except:
@@ -101,15 +106,17 @@ try:
"""Signs a message. """Signs a message.
Args: Args:
message: string, Message to be signed. message: bytes, Message to be signed.
Returns: Returns:
string, The signature of the message for the given key. 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') return crypto.sign(self._key, message, 'sha256')
@staticmethod @staticmethod
def from_string(key, password='notasecret'): def from_string(key, password=b'notasecret'):
"""Construct a Signer instance from a string. """Construct a Signer instance from a string.
Args: Args:
@@ -126,7 +133,9 @@ try:
if parsed_pem_key: if parsed_pem_key:
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key) pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
else: 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) return OpenSSLSigner(pkey)
except ImportError: except ImportError:
@@ -182,8 +191,10 @@ try:
Verifier instance. Verifier instance.
""" """
if is_x509_cert: if is_x509_cert:
pemLines = key_pem.replace(' ', '').split() if isinstance(key_pem, six.text_type):
certDer = _urlsafe_b64decode(''.join(pemLines[1:-1])) key_pem = key_pem.encode('ascii')
pemLines = key_pem.replace(b' ', b'').split()
certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1]))
certSeq = DerSequence() certSeq = DerSequence()
certSeq.decode(certDer) certSeq.decode(certDer)
tbsSeq = DerSequence() tbsSeq = DerSequence()
@@ -214,6 +225,8 @@ try:
Returns: Returns:
string, The signature of the message for the given key. 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)) return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
@staticmethod @staticmethod
@@ -269,19 +282,22 @@ def _parse_pem_key(raw_key_input):
Returns: Returns:
string, The actual key if the contents are from a PEM file, or else None. 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: if offset != -1:
return raw_key_input[offset:] return raw_key_input[offset:]
def _urlsafe_b64encode(raw_bytes): 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): def _urlsafe_b64decode(b64string):
# Guard against unicode strings, which base64 can't handle. # Guard against unicode strings, which base64 can't handle.
b64string = b64string.encode('ascii') if isinstance(b64string, six.text_type):
padded = b64string + '=' * (4 - len(b64string) % 4) b64string = b64string.encode('ascii')
padded = b64string + b'=' * (4 - len(b64string) % 4)
return base64.urlsafe_b64decode(padded) return base64.urlsafe_b64decode(padded)
@@ -345,13 +361,13 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
# Parse token. # Parse token.
json_body = _urlsafe_b64decode(segments[1]) json_body = _urlsafe_b64decode(segments[1])
try: try:
parsed = json.loads(json_body) parsed = json.loads(json_body.decode('utf-8'))
except: except:
raise AppIdentityError('Can\'t parse token: %s' % json_body) raise AppIdentityError('Can\'t parse token: %s' % json_body)
# Check signature. # Check signature.
verified = False verified = False
for _, pem in certs.items(): for pem in certs.values():
verifier = Verifier.from_string(pem, True) verifier = Verifier.from_string(pem, True)
if verifier.verify(signed, signature): if verifier.verify(signed, signature):
verified = True verified = True
@@ -366,7 +382,7 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
earliest = iat - CLOCK_SKEW_SECS earliest = iat - CLOCK_SKEW_SECS
# Check expiration timestamp. # Check expiration timestamp.
now = long(time.time()) now = int(time.time())
exp = parsed.get('exp') exp = parsed.get('exp')
if exp is None: if exp is None:
raise AppIdentityError('No exp field in token: %s' % json_body) raise AppIdentityError('No exp field in token: %s' % json_body)

View File

@@ -90,7 +90,7 @@ class Storage(BaseStorage):
simple version of "touch" to ensure the file has been created. simple version of "touch" to ensure the file has been created.
""" """
if not os.path.exists(self._filename): if not os.path.exists(self._filename):
old_umask = os.umask(0177) old_umask = os.umask(0o177)
try: try:
open(self._filename, 'a+b').close() open(self._filename, 'a+b').close()
finally: finally:
@@ -108,7 +108,7 @@ class Storage(BaseStorage):
self._create_file_if_needed() self._create_file_if_needed()
self._validate_file() self._validate_file()
f = open(self._filename, 'wb') f = open(self._filename, 'w')
f.write(credentials.to_json()) f.write(credentials.to_json())
f.close() f.close()

View File

@@ -21,7 +21,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import json import json
import logging import logging
import urllib from six.moves import urllib
from oauth2client import util from oauth2client import util
from oauth2client.client import AccessTokenRefreshError from oauth2client.client import AccessTokenRefreshError
@@ -78,13 +78,13 @@ class AppAssertionCredentials(AssertionCredentials):
Raises: Raises:
AccessTokenRefreshError: When the refresh fails. 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) uri = META.replace('{?scope}', query)
response, content = http_request(uri) response, content = http_request(uri)
if response.status == 200: if response.status == 200:
try: try:
d = json.loads(content) d = json.loads(content)
except StandardError as e: except Exception as e:
raise AccessTokenRefreshError(str(e)) raise AccessTokenRefreshError(str(e))
self.access_token = d['accessToken'] self.access_token = d['accessToken']
else: else:

View File

@@ -21,10 +21,10 @@ Usage:
f = LockedFile('filename', 'r+b', 'rb') f = LockedFile('filename', 'r+b', 'rb')
f.open_and_lock() f.open_and_lock()
if f.is_locked(): if f.is_locked():
print 'Acquired filename with r+b mode' print('Acquired filename with r+b mode')
f.file_handle().write('locked data') f.file_handle().write('locked data')
else: else:
print 'Aquired filename with rb mode' print('Aquired filename with rb mode')
f.unlock_and_close() f.unlock_and_close()
""" """

View File

@@ -191,7 +191,7 @@ class _MultiStore(object):
This will create the file if necessary. 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._thread_lock = threading.Lock()
self._read_only = False self._read_only = False
self._warn_on_readonly = warn_on_readonly 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. simple version of "touch" to ensure the file has been created.
""" """
if not os.path.exists(self._file.filename()): if not os.path.exists(self._file.filename()):
old_umask = os.umask(0177) old_umask = os.umask(0o177)
try: try:
open(self._file.filename(), 'a+b').close() open(self._file.filename(), 'a+b').close()
finally: finally:

View File

@@ -25,8 +25,8 @@ import gflags
from oauth2client import client from oauth2client import client
from oauth2client import util from oauth2client import util
from tools import ClientRedirectHandler from oauth2client.tools import ClientRedirectHandler
from tools import ClientRedirectServer from oauth2client.tools import ClientRedirectServer
FLAGS = gflags.FLAGS FLAGS = gflags.FLAGS
@@ -103,13 +103,13 @@ def run(flow, storage, http=None):
break break
FLAGS.auth_local_webserver = success FLAGS.auth_local_webserver = success
if not success: if not success:
print 'Failed to start a local webserver listening on either port 8080' print('Failed to start a local webserver listening on either port 8080')
print 'or port 9090. Please check your firewall settings and locally' print('or port 9090. Please check your firewall settings and locally')
print 'running programs that may be blocking or using those ports.' print('running programs that may be blocking or using those ports.')
print print()
print 'Falling back to --noauth_local_webserver and continuing with', print('Falling back to --noauth_local_webserver and continuing with')
print 'authorization.' print('authorization.')
print print()
if FLAGS.auth_local_webserver: if FLAGS.auth_local_webserver:
oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) 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: if FLAGS.auth_local_webserver:
webbrowser.open(authorize_url, new=1, autoraise=True) webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:' print('Your browser has been opened to visit:')
print print()
print ' ' + authorize_url print(' ' + authorize_url)
print print()
print 'If your browser is on a different machine then exit and re-run' print('If your browser is on a different machine then exit and re-run')
print 'this application with the command-line parameter ' print('this application with the command-line parameter ')
print print()
print ' --noauth_local_webserver' print(' --noauth_local_webserver')
print print()
else: else:
print 'Go to the following link in your browser:' print('Go to the following link in your browser:')
print print()
print ' ' + authorize_url print(' ' + authorize_url)
print print()
code = None code = None
if FLAGS.auth_local_webserver: if FLAGS.auth_local_webserver:
@@ -143,7 +143,7 @@ def run(flow, storage, http=None):
if 'code' in httpd.query_params: if 'code' in httpd.query_params:
code = httpd.query_params['code'] code = httpd.query_params['code']
else: 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.') sys.exit('Try running with --noauth_local_webserver.')
else: else:
code = raw_input('Enter verification code: ').strip() code = raw_input('Enter verification code: ').strip()
@@ -155,6 +155,6 @@ def run(flow, storage, http=None):
storage.put(credential) storage.put(credential)
credential.set_store(storage) credential.set_store(storage)
print 'Authentication successful.' print('Authentication successful.')
return credential return credential

View File

@@ -64,7 +64,7 @@ class _ServiceAccountCredentials(AssertionCredentials):
'kid': self._private_key_id 'kid': self._private_key_id
} }
now = long(time.time()) now = int(time.time())
payload = { payload = {
'aud': self._token_uri, 'aud': self._token_uri,
'scope': self._scopes, 'scope': self._scopes,
@@ -77,14 +77,20 @@ class _ServiceAccountCredentials(AssertionCredentials):
assertion_input = '%s.%s' % ( assertion_input = '%s.%s' % (
_urlsafe_b64encode(header), _urlsafe_b64encode(header),
_urlsafe_b64encode(payload)) _urlsafe_b64encode(payload))
assertion_input = assertion_input.encode('utf-8')
# Sign the assertion. # Sign the assertion.
signature = base64.urlsafe_b64encode(rsa.pkcs1.sign( signature = bytes.decode(base64.urlsafe_b64encode(rsa.pkcs1.sign(
assertion_input, self._private_key, 'SHA-256')).rstrip('=') assertion_input, self._private_key, 'SHA-256'))).rstrip('=')
return '%s.%s' % (assertion_input, signature) return '%s.%s' % (assertion_input, signature)
def sign_blob(self, blob): def sign_blob(self, blob):
# Ensure that it is bytes
try:
blob = blob.encode('utf-8')
except AttributeError:
pass
return (self._private_key_id, return (self._private_key_id,
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
@@ -119,7 +125,7 @@ class _ServiceAccountCredentials(AssertionCredentials):
def _urlsafe_b64encode(data): def _urlsafe_b64encode(data):
return base64.urlsafe_b64encode( 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): def _get_private_key(private_key_pkcs8_text):

View File

@@ -28,12 +28,14 @@ import BaseHTTPServer
import logging import logging
import socket import socket
import sys import sys
import urlparse
import webbrowser import webbrowser
from six.moves import urllib
from oauth2client import client from oauth2client import client
from oauth2client import util from oauth2client import util
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0 _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
To make this sample run you will need to populate the client_secrets.json file 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.send_header("Content-type", "text/html")
self.end_headers() self.end_headers()
query = self.path.split('?', 1)[-1] 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.server.query_params = query
self.wfile.write("<html><head><title>Authentication Status</title></head>") self.wfile.write("<html><head><title>Authentication Status</title></head>")
self.wfile.write("<body><p>The authentication flow has completed.</p>") self.wfile.write("<body><p>The authentication flow has completed.</p>")
@@ -155,20 +157,20 @@ def run_flow(flow, storage, flags, http=None):
try: try:
httpd = ClientRedirectServer((flags.auth_host_name, port), httpd = ClientRedirectServer((flags.auth_host_name, port),
ClientRedirectHandler) ClientRedirectHandler)
except socket.error as e: except socket.error:
pass pass
else: else:
success = True success = True
break break
flags.noauth_local_webserver = not success flags.noauth_local_webserver = not success
if not success: if not success:
print 'Failed to start a local webserver listening on either port 8080' print('Failed to start a local webserver listening on either port 8080')
print 'or port 9090. Please check your firewall settings and locally' print('or port 9090. Please check your firewall settings and locally')
print 'running programs that may be blocking or using those ports.' print('running programs that may be blocking or using those ports.')
print print()
print 'Falling back to --noauth_local_webserver and continuing with', print('Falling back to --noauth_local_webserver and continuing with')
print 'authorization.' print('authorization.')
print print()
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) 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: if not flags.noauth_local_webserver:
webbrowser.open(authorize_url, new=1, autoraise=True) webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:' print('Your browser has been opened to visit:')
print print()
print ' ' + authorize_url print(' ' + authorize_url)
print print()
print 'If your browser is on a different machine then exit and re-run this' print('If your browser is on a different machine then exit and re-run this')
print 'application with the command-line parameter ' print('application with the command-line parameter ')
print print()
print ' --noauth_local_webserver' print(' --noauth_local_webserver')
print print()
else: else:
print 'Go to the following link in your browser:' print('Go to the following link in your browser:')
print print()
print ' ' + authorize_url print(' ' + authorize_url)
print print()
code = None code = None
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
@@ -202,7 +204,7 @@ def run_flow(flow, storage, flags, http=None):
if 'code' in httpd.query_params: if 'code' in httpd.query_params:
code = httpd.query_params['code'] code = httpd.query_params['code']
else: 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.') sys.exit('Try running with --noauth_local_webserver.')
else: else:
code = raw_input('Enter verification code: ').strip() code = raw_input('Enter verification code: ').strip()
@@ -214,7 +216,7 @@ def run_flow(flow, storage, flags, http=None):
storage.put(credential) storage.put(credential)
credential.set_store(storage) credential.set_store(storage)
print 'Authentication successful.' print('Authentication successful.')
return credential return credential

View File

@@ -31,9 +31,12 @@ __all__ = [
import inspect import inspect
import logging import logging
import sys
import types import types
import urllib
import urlparse import six
from six.moves import urllib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -129,7 +132,7 @@ def positional(max_positional_args):
return wrapped(*args, **kwargs) return wrapped(*args, **kwargs)
return positional_wrapper return positional_wrapper
if isinstance(max_positional_args, (int, long)): if isinstance(max_positional_args, six.integer_types):
return positional_decorator return positional_decorator
else: else:
args, _, _, defaults = inspect.getargspec(max_positional_args) args, _, _, defaults = inspect.getargspec(max_positional_args)
@@ -149,7 +152,11 @@ def scopes_to_string(scopes):
Returns: Returns:
The scopes formatted as a single string. 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 return scopes
else: else:
return ' '.join(scopes) return ' '.join(scopes)
@@ -186,8 +193,8 @@ def _add_query_parameter(url, name, value):
if value is None: if value is None:
return url return url
else: else:
parsed = list(urlparse.urlparse(url)) parsed = list(urllib.parse.urlparse(url))
q = dict(urlparse.parse_qsl(parsed[4])) q = dict(urllib.parse.parse_qsl(parsed[4]))
q[name] = value q[name] = value
parsed[4] = urllib.urlencode(q) parsed[4] = urllib.parse.urlencode(q)
return urlparse.urlunparse(parsed) return urllib.parse.urlunparse(parsed)

View File

@@ -1,4 +1,3 @@
#!/usr/bin/python2.5
# #
# Copyright 2014 the Melange authors. # Copyright 2014 the Melange authors.
# #
@@ -26,15 +25,27 @@ import base64
import hmac import hmac
import time import time
import six
from oauth2client import util from oauth2client import util
# Delimiter character # Delimiter character
DELIMITER = ':' DELIMITER = b':'
# 1 hour in seconds # 1 hour in seconds
DEFAULT_TIMEOUT_SECS = 1*60*60 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) @util.positional(2)
def generate_token(key, user_id, action_id="", when=None): def generate_token(key, user_id, action_id="", when=None):
"""Generates a URL-safe token for the given user, action, time tuple. """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: Returns:
A string XSRF protection token. A string XSRF protection token.
""" """
when = when or int(time.time()) when = _force_bytes(when or int(time.time()))
digester = hmac.new(key) digester = hmac.new(_force_bytes(key))
digester.update(str(user_id)) digester.update(_force_bytes(user_id))
digester.update(DELIMITER) digester.update(DELIMITER)
digester.update(action_id) digester.update(_force_bytes(action_id))
digester.update(DELIMITER) digester.update(DELIMITER)
digester.update(str(when)) digester.update(when)
digest = digester.digest() digest = digester.digest()
token = base64.urlsafe_b64encode('%s%s%d' % (digest, token = base64.urlsafe_b64encode(digest + DELIMITER + when)
DELIMITER,
when))
return token return token
@@ -86,8 +95,8 @@ def validate_token(key, token, user_id, action_id="", current_time=None):
if not token: if not token:
return False return False
try: try:
decoded = base64.urlsafe_b64decode(str(token)) decoded = base64.urlsafe_b64decode(token)
token_time = long(decoded.split(DELIMITER)[-1]) token_time = int(decoded.split(DELIMITER)[-1])
except (TypeError, ValueError): except (TypeError, ValueError):
return False return False
if current_time is None: 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 # Perform constant time comparison to avoid timing attacks
different = 0 different = 0
for x, y in zip(token, expected_token): for x, y in zip(bytearray(token), bytearray(expected_token)):
different |= ord(x) ^ ord(y) different |= x ^ y
if different: return different == 0
return False
return True

View File

@@ -67,8 +67,8 @@ class HttpMockSequence(object):
and content and then use as if an httplib2.Http instance. and content and then use as if an httplib2.Http instance.
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '401'}, ''), ({'status': '401'}, b''),
({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
({'status': '200'}, 'echo_request_headers'), ({'status': '200'}, 'echo_request_headers'),
]) ])
resp, content = http.request("http://examples.com") resp, content = http.request("http://examples.com")
@@ -111,4 +111,6 @@ class HttpMockSequence(object):
content = body content = body
elif content == 'echo_request_uri': elif content == 'echo_request_uri':
content = uri content = uri
elif not isinstance(content, bytes):
raise TypeError("http content should be bytes: %r" % (content,))
return httplib2.Response(resp), content return httplib2.Response(resp), content

View File

@@ -46,7 +46,6 @@ from google.appengine.ext import db
from google.appengine.ext import ndb from google.appengine.ext import ndb
from google.appengine.ext import testbed from google.appengine.ext import testbed
from google.appengine.runtime import apiproxy_errors from google.appengine.runtime import apiproxy_errors
from http_mock import HttpMockSequence
from oauth2client import appengine from oauth2client import appengine
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.clientsecrets import _loadfile from oauth2client.clientsecrets import _loadfile

View File

@@ -19,7 +19,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import os import os
import unittest import unittest
import StringIO from io import StringIO
import httplib2 import httplib2
@@ -57,6 +57,11 @@ class OAuth2CredentialsTests(unittest.TestCase):
""", 'Property'), """, 'Property'),
] ]
for src, match in ERRORS: for src, match in ERRORS:
# Ensure that it is unicode
try:
src = src.decode('utf-8')
except AttributeError:
pass
# Test load(s) # Test load(s)
try: try:
clientsecrets.loads(src) clientsecrets.loads(src)
@@ -66,7 +71,7 @@ class OAuth2CredentialsTests(unittest.TestCase):
# Test loads(fp) # Test loads(fp)
try: try:
fp = StringIO.StringIO(src) fp = StringIO(src)
clientsecrets.load(fp) clientsecrets.load(fp)
self.fail(src + ' should not be a valid client_secrets file.') self.fail(src + ' should not be a valid client_secrets file.')
except clientsecrets.InvalidClientSecretsError as e: except clientsecrets.InvalidClientSecretsError as e:
@@ -104,25 +109,25 @@ class CachedClientsecretsTests(unittest.TestCase):
def test_cache_miss(self): def test_cache_miss(self):
client_type, client_info = clientsecrets.loadfile( client_type, client_info = clientsecrets.loadfile(
VALID_FILE, cache=self.cache_mock) VALID_FILE, cache=self.cache_mock)
self.assertEquals('web', client_type) self.assertEqual('web', client_type)
self.assertEquals('foo_client_secret', client_info['client_secret']) self.assertEqual('foo_client_secret', client_info['client_secret'])
cached = self.cache_mock.cache[VALID_FILE] 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 # make sure we're using non-empty namespace
ns = self.cache_mock.last_set_ns ns = self.cache_mock.last_set_ns
self.assertTrue(bool(ns)) self.assertTrue(bool(ns))
# make sure they're equal # 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): def test_cache_hit(self):
self.cache_mock.cache[NONEXISTENT_FILE] = { 'web': 'secret info' } self.cache_mock.cache[NONEXISTENT_FILE] = { 'web': 'secret info' }
client_type, client_info = clientsecrets.loadfile( client_type, client_info = clientsecrets.loadfile(
NONEXISTENT_FILE, cache=self.cache_mock) NONEXISTENT_FILE, cache=self.cache_mock)
self.assertEquals('web', client_type) self.assertEqual('web', client_type)
self.assertEquals('secret info', client_info) self.assertEqual('secret info', client_info)
# make sure we didn't do any set() RPCs # make sure we didn't do any set() RPCs
self.assertEqual(None, self.cache_mock.last_set_ns) self.assertEqual(None, self.cache_mock.last_set_ns)
@@ -137,8 +142,8 @@ class CachedClientsecretsTests(unittest.TestCase):
def test_without_cache(self): def test_without_cache(self):
# this also ensures loadfile() is backward compatible # this also ensures loadfile() is backward compatible
client_type, client_info = clientsecrets.loadfile(VALID_FILE) client_type, client_info = clientsecrets.loadfile(VALID_FILE)
self.assertEquals('web', client_type) self.assertEqual('web', client_type)
self.assertEquals('foo_client_secret', client_info['client_secret']) self.assertEqual('foo_client_secret', client_info['client_secret'])
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -40,6 +40,8 @@ from oauth2client.client import Credentials
from oauth2client.client import Flow from oauth2client.client import Flow
# Mock a Django environment # Mock a Django environment
from django.conf import global_settings
global_settings.SECRET_KEY = 'NotASecret'
os.environ['DJANGO_SETTINGS_MODULE'] = 'django_settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'django_settings'
sys.modules['django_settings'] = django_settings = imp.new_module('django_settings') sys.modules['django_settings'] = django_settings = imp.new_module('django_settings')
django_settings.SECRET_KEY = 'xyzzy' django_settings.SECRET_KEY = 'xyzzy'

View File

@@ -32,7 +32,6 @@ import stat
import tempfile import tempfile
import unittest import unittest
from http_mock import HttpMockSequence
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import GOOGLE_TOKEN_URI
from oauth2client import file from oauth2client import file
from oauth2client import locked_file from oauth2client import locked_file
@@ -41,6 +40,11 @@ from oauth2client import util
from oauth2client.client import AccessTokenCredentials from oauth2client.client import AccessTokenCredentials
from oauth2client.client import AssertionCredentials from oauth2client.client import AssertionCredentials
from oauth2client.client import OAuth2Credentials from oauth2client.client import OAuth2Credentials
try:
# Python2
from future_builtins import oct
except:
pass
FILENAME = tempfile.mktemp('oauth2client_test.data') FILENAME = tempfile.mktemp('oauth2client_test.data')
@@ -96,7 +100,7 @@ class OAuth2ClientFileTests(unittest.TestCase):
# Write a file with a pickled OAuth2Credentials. # Write a file with a pickled OAuth2Credentials.
credentials = self.create_test_credentials() credentials = self.create_test_credentials()
f = open(FILENAME, 'w') f = open(FILENAME, 'wb')
pickle.dump(credentials, f) pickle.dump(credentials, f)
f.close() f.close()
@@ -154,13 +158,13 @@ class OAuth2ClientFileTests(unittest.TestCase):
mode = os.stat(FILENAME).st_mode mode = os.stat(FILENAME).st_mode
if os.name == 'posix': 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): def test_read_only_file_fail_lock(self):
credentials = self.create_test_credentials() credentials = self.create_test_credentials()
open(FILENAME, 'a+b').close() open(FILENAME, 'a+b').close()
os.chmod(FILENAME, 0400) os.chmod(FILENAME, 0o400)
store = multistore_file.get_credential_storage( store = multistore_file.get_credential_storage(
FILENAME, FILENAME,
@@ -171,7 +175,7 @@ class OAuth2ClientFileTests(unittest.TestCase):
store.put(credentials) store.put(credentials)
if os.name == 'posix': if os.name == 'posix':
self.assertTrue(store._multistore._read_only) self.assertTrue(store._multistore._read_only)
os.chmod(FILENAME, 0600) os.chmod(FILENAME, 0o600)
def test_multistore_no_symbolic_link_files(self): def test_multistore_no_symbolic_link_files(self):
if hasattr(os, 'symlink'): if hasattr(os, 'symlink'):
@@ -221,7 +225,7 @@ class OAuth2ClientFileTests(unittest.TestCase):
self.assertEquals(None, credentials) self.assertEquals(None, credentials)
if os.name == 'posix': 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): def test_multistore_file_custom_key(self):
credentials = self.create_test_credentials() credentials = self.create_test_credentials()

View File

@@ -21,7 +21,10 @@ Unit tests for oauth2client.gce.
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import httplib2 import httplib2
import mox try:
from mox3 import mox
except ImportError:
import mox
import unittest import unittest
from oauth2client.client import AccessTokenRefreshError from oauth2client.client import AccessTokenRefreshError

View File

@@ -23,11 +23,12 @@ Unit tests for oauth2client.
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import os import os
import sys
import tempfile import tempfile
import time import time
import unittest import unittest
from http_mock import HttpMockSequence from .http_mock import HttpMockSequence
from oauth2client import crypt from oauth2client import crypt
from oauth2client.client import Credentials from oauth2client.client import Credentials
from oauth2client.client import SignedJwtAssertionCredentials from oauth2client.client import SignedJwtAssertionCredentials
@@ -39,7 +40,7 @@ from oauth2client.file import Storage
def datafile(filename): 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() data = f.read()
f.close() f.close()
return data return data
@@ -67,11 +68,10 @@ class CryptTests(unittest.TestCase):
signature = signer.sign('foo') signature = signer.sign('foo')
verifier = self.verifier.from_string(public_key, True) 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(b'bar', signature))
self.assertFalse(verifier.verify(b'foo', 'bad signagure'))
self.assertFalse(verifier.verify('bar', signature))
self.assertFalse(verifier.verify('foo', 'bad signature'))
def _check_jwt_failure(self, jwt, expected_error): def _check_jwt_failure(self, jwt, expected_error):
public_key = datafile('publickey.pem') public_key = datafile('publickey.pem')
@@ -88,7 +88,7 @@ class CryptTests(unittest.TestCase):
private_key = datafile('privatekey.%s' % self.format) private_key = datafile('privatekey.%s' % self.format)
signer = self.signer.from_string(private_key) signer = self.signer.from_string(private_key)
audience = 'some_audience_address@testing.gserviceaccount.com' audience = 'some_audience_address@testing.gserviceaccount.com'
now = long(time.time()) now = int(time.time())
return crypt.make_signed_jwt(signer, { return crypt.make_signed_jwt(signer, {
'aud': audience, 'aud': audience,
@@ -212,11 +212,11 @@ class SignedJwtAssertionCredentialsTests(unittest.TestCase):
scope='read+write', scope='read+write',
sub='joe@example.org') sub='joe@example.org')
http = HttpMockSequence([ 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'), ({'status': '200'}, 'echo_request_headers'),
]) ])
http = credentials.authorize(http) http = credentials.authorize(http)
_, content = http.request('http://example.org') resp, content = http.request('http://example.org')
self.assertEqual('Bearer 1/3w', content['Authorization']) self.assertEqual('Bearer 1/3w', content['Authorization'])
def test_credentials_to_from_json(self): def test_credentials_to_from_json(self):
@@ -235,9 +235,9 @@ class SignedJwtAssertionCredentialsTests(unittest.TestCase):
def _credentials_refresh(self, credentials): def _credentials_refresh(self, credentials):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
({'status': '401'}, ''), ({'status': '401'}, b''),
({'status': '200'}, '{"access_token":"3/3w","expires_in":3600}'), ({'status': '200'}, b'{"access_token":"3/3w","expires_in":3600}'),
({'status': '200'}, 'echo_request_headers'), ({'status': '200'}, 'echo_request_headers'),
]) ])
http = credentials.authorize(http) http = credentials.authorize(http)

View File

@@ -23,7 +23,10 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import datetime import datetime
import keyring import keyring
import unittest import unittest
import mox try:
from mox3 import mox
except ImportError:
import mox
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.client import OAuth2Credentials from oauth2client.client import OAuth2Credentials

95
tests/test_oauth2client.py Normal file → Executable file
View File

@@ -25,14 +25,18 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import base64 import base64
import datetime import datetime
import json import json
import mox try:
from mox3 import mox
except ImportError:
import mox
import os import os
import time import time
import unittest import unittest
import urlparse import six
from six.moves import urllib
from http_mock import HttpMock from .http_mock import HttpMock
from http_mock import HttpMockSequence from .http_mock import HttpMockSequence
from oauth2client import GOOGLE_REVOKE_URI from oauth2client import GOOGLE_REVOKE_URI
from oauth2client import GOOGLE_TOKEN_URI from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.client import AccessTokenCredentials 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. # googleapiclient.test_discovery; consolidate these definitions.
def assertUrisEqual(testcase, expected, actual): def assertUrisEqual(testcase, expected, actual):
"""Test that URIs are the same, up to reordering of query parameters.""" """Test that URIs are the same, up to reordering of query parameters."""
expected = urlparse.urlparse(expected) expected = urllib.parse.urlparse(expected)
actual = urlparse.urlparse(actual) actual = urllib.parse.urlparse(actual)
testcase.assertEqual(expected.scheme, actual.scheme) testcase.assertEqual(expected.scheme, actual.scheme)
testcase.assertEqual(expected.netloc, actual.netloc) testcase.assertEqual(expected.netloc, actual.netloc)
testcase.assertEqual(expected.path, actual.path) testcase.assertEqual(expected.path, actual.path)
testcase.assertEqual(expected.params, actual.params) testcase.assertEqual(expected.params, actual.params)
testcase.assertEqual(expected.fragment, actual.fragment) testcase.assertEqual(expected.fragment, actual.fragment)
expected_query = urlparse.parse_qs(expected.query) expected_query = urllib.parse.parse_qs(expected.query)
actual_query = urlparse.parse_qs(actual.query) actual_query = urllib.parse.parse_qs(actual.query)
for name in expected_query.keys(): for name in expected_query.keys():
testcase.assertEqual(expected_query[name], actual_query[name]) testcase.assertEqual(expected_query[name], actual_query[name])
for name in actual_query.keys(): for name in actual_query.keys():
@@ -534,8 +538,8 @@ class BasicCredentialsTests(unittest.TestCase):
for status_code in REFRESH_STATUS_CODES: for status_code in REFRESH_STATUS_CODES:
token_response = {'access_token': '1/3w', 'expires_in': 3600} token_response = {'access_token': '1/3w', 'expires_in': 3600}
http = HttpMockSequence([ http = HttpMockSequence([
({'status': status_code}, ''), ({'status': status_code}, b''),
({'status': '200'}, json.dumps(token_response)), ({'status': '200'}, json.dumps(token_response).encode('utf-8')),
({'status': '200'}, 'echo_request_headers'), ({'status': '200'}, 'echo_request_headers'),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
@@ -547,8 +551,8 @@ class BasicCredentialsTests(unittest.TestCase):
def test_token_refresh_failure(self): def test_token_refresh_failure(self):
for status_code in REFRESH_STATUS_CODES: for status_code in REFRESH_STATUS_CODES:
http = HttpMockSequence([ http = HttpMockSequence([
({'status': status_code}, ''), ({'status': status_code}, b''),
({'status': '400'}, '{"error":"access_denied"}'), ({'status': '400'}, b'{"error":"access_denied"}'),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
try: try:
@@ -571,7 +575,7 @@ class BasicCredentialsTests(unittest.TestCase):
def test_non_401_error_response(self): def test_non_401_error_response(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, ''), ({'status': '400'}, b''),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
resp, content = http.request('http://example.com') resp, content = http.request('http://example.com')
@@ -598,9 +602,9 @@ class BasicCredentialsTests(unittest.TestCase):
client_id = u'some_client_id' client_id = u'some_client_id'
client_secret = u'cOuDdkfjxxnv+' client_secret = u'cOuDdkfjxxnv+'
refresh_token = u'1/0/a.df219fjls0' refresh_token = u'1/0/a.df219fjls0'
token_expiry = unicode(datetime.datetime.utcnow()) token_expiry = str(datetime.datetime.utcnow())
token_uri = unicode(GOOGLE_TOKEN_URI) token_uri = str(GOOGLE_TOKEN_URI)
revoke_uri = unicode(GOOGLE_REVOKE_URI) revoke_uri = str(GOOGLE_REVOKE_URI)
user_agent = u'refresh_checker/1.0' user_agent = u'refresh_checker/1.0'
credentials = OAuth2Credentials(access_token, client_id, client_secret, credentials = OAuth2Credentials(access_token, client_id, client_secret,
refresh_token, token_expiry, token_uri, refresh_token, token_expiry, token_uri,
@@ -609,7 +613,7 @@ class BasicCredentialsTests(unittest.TestCase):
http = HttpMock(headers={'status': '200'}) http = HttpMock(headers={'status': '200'})
http = credentials.authorize(http) http = credentials.authorize(http)
http.request(u'http://example.com', method=u'GET', headers={u'foo': u'bar'}) 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(k))
self.assertEqual(str, type(v)) 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_first = {'access_token': 'first_token', 'expires_in': S}
token_response_second = {'access_token': 'second_token', 'expires_in': S} token_response_second = {'access_token': 'second_token', 'expires_in': S}
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, json.dumps(token_response_first)), ({'status': '200'}, json.dumps(token_response_first).encode('utf-8')),
({'status': '200'}, json.dumps(token_response_second)), ({'status': '200'}, json.dumps(token_response_second).encode('utf-8')),
]) ])
token = self.credentials.get_access_token(http=http) token = self.credentials.get_access_token(http=http)
@@ -667,7 +671,7 @@ class AccessTokenCredentialsTests(unittest.TestCase):
def test_token_refresh_success(self): def test_token_refresh_success(self):
for status_code in REFRESH_STATUS_CODES: for status_code in REFRESH_STATUS_CODES:
http = HttpMockSequence([ http = HttpMockSequence([
({'status': status_code}, ''), ({'status': status_code}, b''),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
try: try:
@@ -690,7 +694,7 @@ class AccessTokenCredentialsTests(unittest.TestCase):
def test_non_401_error_response(self): def test_non_401_error_response(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, ''), ({'status': '400'}, b''),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
resp, content = http.request('http://example.com') resp, content = http.request('http://example.com')
@@ -720,14 +724,15 @@ class TestAssertionCredentials(unittest.TestCase):
user_agent=user_agent) user_agent=user_agent)
def test_assertion_body(self): 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(self.assertion_text, body['assertion'][0])
self.assertEqual('urn:ietf:params:oauth:grant-type:jwt-bearer', self.assertEqual('urn:ietf:params:oauth:grant-type:jwt-bearer',
body['grant_type'][0]) body['grant_type'][0])
def test_assertion_refresh(self): def test_assertion_refresh(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, '{"access_token":"1/3w"}'), ({'status': '200'}, b'{"access_token":"1/3w"}'),
({'status': '200'}, 'echo_request_headers'), ({'status': '200'}, 'echo_request_headers'),
]) ])
http = self.credentials.authorize(http) http = self.credentials.authorize(http)
@@ -792,8 +797,8 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_construct_authorize_url(self): def test_construct_authorize_url(self):
authorize_url = self.flow.step1_get_authorize_url() authorize_url = self.flow.step1_get_authorize_url()
parsed = urlparse.urlparse(authorize_url) parsed = urllib.parse.urlparse(authorize_url)
q = urlparse.parse_qs(parsed[4]) q = urllib.parse.parse_qs(parsed[4])
self.assertEqual('client_id+1', q['client_id'][0]) self.assertEqual('client_id+1', q['client_id'][0])
self.assertEqual('code', q['response_type'][0]) self.assertEqual('code', q['response_type'][0])
self.assertEqual('foo', q['scope'][0]) self.assertEqual('foo', q['scope'][0])
@@ -813,8 +818,8 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
) )
authorize_url = flow.step1_get_authorize_url() authorize_url = flow.step1_get_authorize_url()
parsed = urlparse.urlparse(authorize_url) parsed = urllib.parse.urlparse(authorize_url)
q = urlparse.parse_qs(parsed[4]) q = urllib.parse.parse_qs(parsed[4])
self.assertEqual('client_id+1', q['client_id'][0]) self.assertEqual('client_id+1', q['client_id'][0])
self.assertEqual('token', q['response_type'][0]) self.assertEqual('token', q['response_type'][0])
self.assertEqual('foo', q['scope'][0]) self.assertEqual('foo', q['scope'][0])
@@ -823,7 +828,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_exchange_failure(self): def test_exchange_failure(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, '{"error":"invalid_request"}'), ({'status': '400'}, b'{"error":"invalid_request"}'),
]) ])
try: try:
@@ -850,9 +855,9 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
# exceptions are being raised instead of FlowExchangeError. # exceptions are being raised instead of FlowExchangeError.
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, ({'status': '400'},
""" {"error": { b""" {"error": {
"type": "OAuthException", "type": "OAuthException",
"message": "Error validating verification code."} }"""), "message": "Error validating verification code."} }"""),
]) ])
try: try:
@@ -864,7 +869,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_exchange_success(self): def test_exchange_success(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, ({'status': '200'},
"""{ "access_token":"SlAV32hkKG", b"""{ "access_token":"SlAV32hkKG",
"expires_in":3600, "expires_in":3600,
"refresh_token":"8xLOxBtZp8" }"""), "refresh_token":"8xLOxBtZp8" }"""),
]) ])
@@ -905,7 +910,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_urlencoded_exchange_success(self): def test_urlencoded_exchange_success(self):
http = HttpMockSequence([ 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) credentials = self.flow.step2_exchange('some random code', http=http)
@@ -916,7 +921,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
http = HttpMockSequence([ http = HttpMockSequence([
# Note the 'expires=3600' where you'd normally # Note the 'expires=3600' where you'd normally
# have if named 'expires_in' # 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) 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): def test_exchange_no_expires_in(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG", ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG",
"refresh_token":"8xLOxBtZp8" }"""), "refresh_token":"8xLOxBtZp8" }"""),
]) ])
@@ -935,7 +940,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
http = HttpMockSequence([ http = HttpMockSequence([
# This might be redundant but just to make sure # This might be redundant but just to make sure
# urlencoded access_token gets parsed correctly # 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) 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): def test_exchange_fails_if_no_code(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG", ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG",
"refresh_token":"8xLOxBtZp8" }"""), "refresh_token":"8xLOxBtZp8" }"""),
]) ])
@@ -956,7 +961,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_exchange_id_token_fail(self): def test_exchange_id_token_fail(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG", ({'status': '200'}, b"""{ "access_token":"SlAV32hkKG",
"refresh_token":"8xLOxBtZp8", "refresh_token":"8xLOxBtZp8",
"id_token": "stuff.payload"}"""), "id_token": "stuff.payload"}"""),
]) ])
@@ -971,9 +976,9 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
base64.urlsafe_b64encode('signature')) base64.urlsafe_b64encode('signature'))
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG", ({'status': '200'}, ("""{ "access_token":"SlAV32hkKG",
"refresh_token":"8xLOxBtZp8", "refresh_token":"8xLOxBtZp8",
"id_token": "%s"}""" % jwt), "id_token": "%s"}""" % jwt).encode('utf-8')),
]) ])
credentials = self.flow.step2_exchange('some random code', http=http) credentials = self.flow.step2_exchange('some random code', http=http)
@@ -1003,7 +1008,7 @@ class CredentialsFromCodeTests(unittest.TestCase):
token = 'asdfghjkl' token = 'asdfghjkl'
payload = json.dumps({'access_token': token, 'expires_in': 3600}) payload = json.dumps({'access_token': token, 'expires_in': 3600})
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, payload), ({'status': '200'}, payload.encode('utf-8')),
]) ])
credentials = credentials_from_code(self.client_id, self.client_secret, credentials = credentials_from_code(self.client_id, self.client_secret,
self.scope, self.code, redirect_uri=self.redirect_uri, 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): def test_exchange_code_for_token_fail(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, '{"error":"invalid_request"}'), ({'status': '400'}, b'{"error":"invalid_request"}'),
]) ])
try: try:
@@ -1027,7 +1032,7 @@ class CredentialsFromCodeTests(unittest.TestCase):
def test_exchange_code_and_file_for_token(self): def test_exchange_code_and_file_for_token(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, ({'status': '200'},
"""{ "access_token":"asdfghjkl", b"""{ "access_token":"asdfghjkl",
"expires_in":3600 }"""), "expires_in":3600 }"""),
]) ])
credentials = credentials_from_clientsecrets_and_code( credentials = credentials_from_clientsecrets_and_code(
@@ -1038,7 +1043,7 @@ class CredentialsFromCodeTests(unittest.TestCase):
def test_exchange_code_and_cached_file_for_token(self): def test_exchange_code_and_cached_file_for_token(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, '{ "access_token":"asdfghjkl"}'), ({'status': '200'}, b'{ "access_token":"asdfghjkl"}'),
]) ])
cache_mock = CacheMock() cache_mock = CacheMock()
load_and_cache('client_secrets.json', 'some_secrets', cache_mock) 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): def test_exchange_code_and_file_for_token_fail(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, '{"error":"invalid_request"}'), ({'status': '400'}, b'{"error":"invalid_request"}'),
]) ])
try: try:

View File

@@ -26,13 +26,13 @@ import rsa
import time import time
import unittest import unittest
from http_mock import HttpMockSequence from .http_mock import HttpMockSequence
from oauth2client.service_account import _ServiceAccountCredentials from oauth2client.service_account import _ServiceAccountCredentials
def datafile(filename): def datafile(filename):
# TODO(orestica): Refactor this using pkgutil.get_data # 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() data = f.read()
f.close() f.close()
return data return data
@@ -58,16 +58,16 @@ class ServiceAccountCredentialsTests(unittest.TestCase):
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem( pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(
datafile('publickey_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: try:
rsa.pkcs1.verify('Orest', signature, pub_key) rsa.pkcs1.verify(b'Orest', signature, pub_key)
self.fail('Verification should have failed!') self.fail('Verification should have failed!')
except rsa.pkcs1.VerificationError: except rsa.pkcs1.VerificationError:
pass # Expected pass # Expected
try: 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!') self.fail('Verification should have failed!')
except rsa.pkcs1.VerificationError: except rsa.pkcs1.VerificationError:
pass # Expected pass # Expected
@@ -98,8 +98,8 @@ class ServiceAccountCredentialsTests(unittest.TestCase):
token_response_first = {'access_token': 'first_token', 'expires_in': S} token_response_first = {'access_token': 'first_token', 'expires_in': S}
token_response_second = {'access_token': 'second_token', 'expires_in': S} token_response_second = {'access_token': 'second_token', 'expires_in': S}
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, json.dumps(token_response_first)), ({'status': '200'}, json.dumps(token_response_first).encode('utf-8')),
({'status': '200'}, json.dumps(token_response_second)), ({'status': '200'}, json.dumps(token_response_second).encode('utf-8')),
]) ])
token = self.credentials.get_access_token(http=http) token = self.credentials.get_access_token(http=http)

View File

@@ -96,7 +96,7 @@ class XsrfUtilTests(unittest.TestCase):
# Invalid with extra garbage # Invalid with extra garbage
self.assertFalse(xsrfutil.validate_token(TEST_KEY, self.assertFalse(xsrfutil.validate_token(TEST_KEY,
token + 'x', token + b'x',
TEST_USER_ID_1, TEST_USER_ID_1,
action_id=TEST_ACTION_ID_1, action_id=TEST_ACTION_ID_1,
current_time=later15mins)) current_time=later15mins))

53
tox.ini
View File

@@ -1,13 +1,60 @@
[tox] [tox]
envlist = py26, py27 envlist = py26openssl13, py26openssl14,
py27openssl13, py27openssl14,
py33openssl14,
py34openssl14,
pypyopenssl13, pypyopenssl14
[testenv] [testenv]
deps = keyring deps = keyring
mox mox3
pyopenssl
pycrypto==2.6 pycrypto==2.6
django>=1.5,<1.6 django>=1.5,<1.6
webtest webtest
nose nose
setenv = PYTHONPATH=../google_appengine setenv = PYTHONPATH=../google_appengine
commands = nosetests --ignore-files=test_appengine\.py 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