Permalink
Browse files

Merge pull request #27 from peterbe/upgrade-django-browserid

upgrade to django-browserid==0.7.1
  • Loading branch information...
2 parents d1d4eed + a78e698 commit 5f367d6563172a4b2e143bd2e1ecc112940bda31 @peterbe peterbe committed Dec 14, 2012
Showing with 15,120 additions and 609 deletions.
  1. +4 −2 lib/python/django_browserid/__init__.py
  2. +24 −20 lib/python/django_browserid/auth.py
  3. +12 −47 lib/python/django_browserid/base.py
  4. +1 −1 lib/python/django_browserid/forms.py
  5. +61 −9 lib/python/django_browserid/static/browserid/browserid.js
  6. +1 −1 lib/python/django_browserid/tests/__init__.py
  7. +23 −0 lib/python/django_browserid/tests/models.py
  8. +3 −0 lib/python/django_browserid/tests/settings.py
  9. +71 −33 lib/python/django_browserid/tests/test_auth.py
  10. +33 −13 lib/python/django_browserid/tests/test_verification.py
  11. +29 −13 lib/python/django_browserid/tests/test_views.py
  12. +11 −3 lib/python/django_browserid/views.py
  13. +31 −4 lib/python/requests/__init__.py
  14. +23 −0 lib/python/requests/_oauth.py
  15. +58 −0 lib/python/requests/adapters.py
  16. +14 −3 lib/python/requests/api.py
  17. +257 −104 lib/python/requests/auth.py
  18. +3,338 −0 lib/python/requests/cacert.pem
  19. +27 −0 lib/python/requests/certs.py
  20. +19 −5 lib/python/requests/compat.py
  21. +29 −15 lib/python/requests/cookies.py
  22. +4 −7 lib/python/requests/defaults.py
  23. +10 −0 lib/python/requests/exceptions.py
  24. +3 −6 lib/python/requests/hooks.py
  25. +152 −77 lib/python/requests/models.py
  26. +26 −0 lib/python/requests/packages/chardet/__init__.py
  27. +923 −0 lib/python/requests/packages/chardet/big5freq.py
  28. +41 −0 lib/python/requests/packages/chardet/big5prober.py
  29. +200 −0 lib/python/requests/packages/chardet/chardistribution.py
  30. +96 −0 lib/python/requests/packages/chardet/charsetgroupprober.py
  31. +60 −0 lib/python/requests/packages/chardet/charsetprober.py
  32. +56 −0 lib/python/requests/packages/chardet/codingstatemachine.py
  33. +47 −0 lib/python/requests/packages/chardet/constants.py
  34. +79 −0 lib/python/requests/packages/chardet/escprober.py
  35. +240 −0 lib/python/requests/packages/chardet/escsm.py
  36. +85 −0 lib/python/requests/packages/chardet/eucjpprober.py
  37. +594 −0 lib/python/requests/packages/chardet/euckrfreq.py
  38. +41 −0 lib/python/requests/packages/chardet/euckrprober.py
  39. +426 −0 lib/python/requests/packages/chardet/euctwfreq.py
  40. +41 −0 lib/python/requests/packages/chardet/euctwprober.py
  41. +471 −0 lib/python/requests/packages/chardet/gb2312freq.py
  42. +41 −0 lib/python/requests/packages/chardet/gb2312prober.py
  43. +269 −0 lib/python/requests/packages/chardet/hebrewprober.py
  44. +567 −0 lib/python/requests/packages/chardet/jisfreq.py
  45. +210 −0 lib/python/requests/packages/chardet/jpcntx.py
  46. +228 −0 lib/python/requests/packages/chardet/langbulgarianmodel.py
  47. +329 −0 lib/python/requests/packages/chardet/langcyrillicmodel.py
  48. +225 −0 lib/python/requests/packages/chardet/langgreekmodel.py
  49. +201 −0 lib/python/requests/packages/chardet/langhebrewmodel.py
  50. +225 −0 lib/python/requests/packages/chardet/langhungarianmodel.py
  51. +200 −0 lib/python/requests/packages/chardet/langthaimodel.py
  52. +136 −0 lib/python/requests/packages/chardet/latin1prober.py
  53. +82 −0 lib/python/requests/packages/chardet/mbcharsetprober.py
  54. +50 −0 lib/python/requests/packages/chardet/mbcsgroupprober.py
  55. +514 −0 lib/python/requests/packages/chardet/mbcssm.py
  56. +106 −0 lib/python/requests/packages/chardet/sbcharsetprober.py
  57. +64 −0 lib/python/requests/packages/chardet/sbcsgroupprober.py
  58. +85 −0 lib/python/requests/packages/chardet/sjisprober.py
  59. +154 −0 lib/python/requests/packages/chardet/universaldetector.py
  60. +76 −0 lib/python/requests/packages/chardet/utf8prober.py
  61. 0 lib/python/requests/packages/oauthlib/__init__.py
  62. +229 −0 lib/python/requests/packages/oauthlib/common.py
  63. +13 −0 lib/python/requests/packages/oauthlib/oauth1/__init__.py
  64. +889 −0 lib/python/requests/packages/oauthlib/oauth1/rfc5849/__init__.py
  65. +134 −0 lib/python/requests/packages/oauthlib/oauth1/rfc5849/parameters.py
  66. +551 −0 lib/python/requests/packages/oauthlib/oauth1/rfc5849/signature.py
  67. +99 −0 lib/python/requests/packages/oauthlib/oauth1/rfc5849/utils.py
  68. +13 −0 lib/python/requests/packages/oauthlib/oauth2/__init__.py
  69. +497 −0 lib/python/requests/packages/oauthlib/oauth2/draft25/__init__.py
  70. +256 −0 lib/python/requests/packages/oauthlib/oauth2/draft25/parameters.py
  71. +132 −0 lib/python/requests/packages/oauthlib/oauth2/draft25/tokens.py
  72. +39 −0 lib/python/requests/packages/oauthlib/oauth2/draft25/utils.py
  73. +18 −2 lib/python/requests/packages/urllib3/__init__.py
  74. +66 −103 lib/python/requests/packages/urllib3/_collections.py
  75. +66 −26 lib/python/requests/packages/urllib3/connectionpool.py
  76. +10 −0 lib/python/requests/packages/urllib3/exceptions.py
  77. +8 −5 lib/python/requests/packages/urllib3/filepost.py
  78. +260 −0 lib/python/requests/packages/urllib3/packages/ordered_dict.py
  79. +42 −22 lib/python/requests/packages/urllib3/poolmanager.py
  80. +4 −4 lib/python/requests/packages/urllib3/response.py
  81. +155 −39 lib/python/requests/packages/urllib3/util.py
  82. +7 −5 lib/python/requests/safe_mode.py
  83. +32 −28 lib/python/requests/sessions.py
  84. +1 −1 lib/python/requests/status_codes.py
  85. +1 −0 lib/python/requests/structures.py
  86. +172 −11 lib/python/requests/utils.py
View
6 lib/python/django_browserid/__init__.py
@@ -5,5 +5,7 @@
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
-from django_browserid.auth import BrowserIDBackend
-from django_browserid.base import get_audience, verify
+__version__ = '0.7.1'
+
+from django_browserid.auth import BrowserIDBackend # NOQA
+from django_browserid.base import get_audience, verify # NOQA
View
44 lib/python/django_browserid/auth.py
@@ -6,24 +6,24 @@
import base64
import hashlib
import logging
-from warnings import warn
from django.conf import settings
-from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
-from django_browserid.base import get_audience as base_get_audience, verify
+from django_browserid.base import verify
from django_browserid.signals import user_created
+try:
+ from django.contrib.auth import get_user_model
+except ImportError:
+ from django.contrib.auth.models import User
-log = logging.getLogger(__name__)
+ def get_user_model(*args, **kwargs):
+ return User
-def get_audience(*args):
- warn('Deprecated, please use the standalone function '
- 'django_browserid.get_audience instead.', DeprecationWarning)
- return base_get_audience(*args)
+log = logging.getLogger(__name__)
def default_username_algo(email):
@@ -40,14 +40,17 @@ class BrowserIDBackend(object):
supports_inactive_user = True
supports_object_permissions = False
- def verify(self, *args):
- warn('Deprecated, please use the standalone function '
- 'django_browserid.verify instead.', DeprecationWarning)
- return verify(*args)
+ def __init__(self):
+ """
+ Store the current user model on creation to avoid issues if
+ settings.AUTH_USER_MODEL changes, which usually only happens during
+ tests.
+ """
+ self.User = get_user_model()
def filter_users_by_email(self, email):
"""Return all users matching the specified email."""
- return User.objects.filter(email=email)
+ return self.User.objects.filter(email=email)
def create_user(self, email):
"""Return object for a newly created user account."""
@@ -57,7 +60,7 @@ def create_user(self, email):
else:
username = default_username_algo(email)
- return User.objects.create_user(username, email)
+ return self.User.objects.create_user(username, email)
def authenticate(self, assertion=None, audience=None):
"""``django.contrib.auth`` compatible authentication method.
@@ -81,7 +84,8 @@ def authenticate(self, assertion=None, audience=None):
# log and bail. randomly selecting one seems really wrong.
users = self.filter_users_by_email(email=email)
if len(users) > 1:
- log.warn('%d users with email address %s.' % (len(users), email))
+ log.warn('{0} users with email address {1}.'.format(len(users),
+ email))
return None
if len(users) == 1:
return users[0]
@@ -90,7 +94,7 @@ def authenticate(self, assertion=None, audience=None):
if not create_user:
return None
else:
- if create_user == True:
+ if create_user is True:
create_function = self.create_user
else:
# Find the function to call.
@@ -102,8 +106,8 @@ def authenticate(self, assertion=None, audience=None):
def get_user(self, user_id):
try:
- return User.objects.get(pk=user_id)
- except User.DoesNotExist:
+ return self.User.objects.get(pk=user_id)
+ except self.User.DoesNotExist:
return None
def _load_module(self, path):
@@ -125,6 +129,6 @@ def _load_module(self, path):
try:
create_user = getattr(mod, attr)
except AttributeError:
- raise ImproperlyConfigured('Module "%s" does not define a "%s" '
- 'function.' % (module, attr))
+ raise ImproperlyConfigured('Module {0} does not define a {1} '
+ 'function.'.format(module, attr))
return create_user
View
59 lib/python/django_browserid/base.py
@@ -9,7 +9,7 @@
try:
import json
except ImportError:
- import simplejson as json
+ import simplejson as json # NOQA
from django.conf import settings
@@ -21,7 +21,7 @@
DEFAULT_HTTP_TIMEOUT = 5
-DEFAULT_VERIFICATION_URL = 'https://browserid.org/verify'
+DEFAULT_VERIFICATION_URL = 'https://verifier.login.persona.org/verify'
OKAY_RESPONSE = 'okay'
@@ -36,17 +36,6 @@ def get_audience(request):
SITE_URL = 'https://example.com'
SITE_URL = 'http://example.com'
- If you don't have a SITE_URL you can also use these varables:
- PROTOCOL, DOMAIN, and (optionally) PORT.
- Example 1:
- PROTOCOL = 'https://'
- DOMAIN = 'example.com'
-
- Example 2:
- PROTOCOL = 'http://'
- DOMAIN = '127.0.0.1'
- PORT = '8001'
-
If none are set, we trust the request to populate the audience.
This is *not secure*!
"""
@@ -59,32 +48,11 @@ def get_audience(request):
req_proto = 'http://'
req_domain = request.get_host()
- # If we don't define it explicitly
- if not site_url:
- warn('Using DOMAIN and PROTOCOL to specify your BrowserID audience is '
- 'deprecated. Please use the SITE_URL setting instead.',
- DeprecationWarning)
-
- # DOMAIN is example.com req_domain is example.com:8001
- domain = getattr(settings, 'DOMAIN', req_domain.split(':')[0])
- protocol = getattr(settings, 'PROTOCOL', req_proto)
-
- standards = {'https://': 443, 'http://': 80}
- if ':' in req_domain:
- req_port = req_domain.split(':')[1]
- else:
- req_port = None
- port = getattr(settings, 'PORT', req_port or standards[protocol])
- if port == standards[protocol]:
- site_url = ''.join(map(str, (protocol, domain)))
- else:
- site_url = ''.join(map(str, (protocol, domain, ':', port)))
-
req_url = "%s%s" % (req_proto, req_domain)
if site_url != "%s%s" % (req_proto, req_domain):
- log.warning('Misconfigured SITE_URL? settings has [%s], but '
- 'actual request was [%s] BrowserID may fail on '
- 'audience' % (site_url, req_url))
+ log.warning('Misconfigured SITE_URL? settings has {0}, but '
+ 'actual request was {1} BrowserID may fail on '
+ 'audience'.format(site_url, req_url))
return site_url
@@ -94,10 +62,8 @@ def _verify_http_request(url, qs):
'proxies': getattr(settings, 'BROWSERID_PROXY_INFO', None),
'verify': not getattr(settings, 'BROWSERID_DISABLE_CERT_CHECK', False),
'headers': {'Content-type': 'application/x-www-form-urlencoded'},
- 'params': {
- 'timeout': getattr(settings, 'BROWSERID_HTTP_TIMEOUT',
- DEFAULT_HTTP_TIMEOUT)
- }
+ 'timeout': getattr(settings, 'BROWSERID_HTTP_TIMEOUT',
+ DEFAULT_HTTP_TIMEOUT),
}
if parameters['verify']:
@@ -108,8 +74,7 @@ def _verify_http_request(url, qs):
try:
rv = json.loads(r.content)
except ValueError:
- log.debug('Failed to decode JSON. Resp: %s, Content: %s' %
- (r.status_code, r.content))
+ log.debug('Failed to decode JSON. Resp: {0}, Content: {1}'.format(r.status_code, r.content))
return dict(status='failure')
return rv
@@ -120,7 +85,7 @@ def verify(assertion, audience):
verify_url = getattr(settings, 'BROWSERID_VERIFICATION_URL',
DEFAULT_VERIFICATION_URL)
- log.info("Verification URL: %s" % verify_url)
+ log.info("Verification URL: {0}".format(verify_url))
result = _verify_http_request(verify_url, urllib.urlencode({
'assertion': assertion,
@@ -130,7 +95,7 @@ def verify(assertion, audience):
if result['status'] == OKAY_RESPONSE:
return result
- log.error('BrowserID verification failure. Response: %r '
- 'Audience: %r' % (result, audience))
- log.error("BID assert: %r" % assertion)
+ log.error('BrowserID verification failure. Response: {0} '
+ 'Audience: {1}'.format(result, audience))
+ log.error("BID assert: {0}".format(assertion))
return False
View
2 lib/python/django_browserid/forms.py
@@ -10,4 +10,4 @@ class BrowserIDForm(forms.Form):
assertion = forms.CharField(widget=forms.HiddenInput())
class Media:
- js = ('browserid/browserid.js', 'https://browserid.org/include.js')
+ js = ('browserid/browserid.js', 'https://login.persona.org/include.js')
View
70 lib/python/django_browserid/static/browserid/browserid.js
@@ -1,15 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- $(document).ready(function() {
- $('#browserid').bind('click', function(e) {
- e.preventDefault();
- navigator.id.getVerifiedEmail(function(assertion) {
- if (assertion) {
- var $e = $('#id_assertion');
- $e.val(assertion.toString());
- $e.parent().submit();
+
+(function($) {
+ 'use strict';
+
+ $(function() {
+ // State? Ewwwwww.
+ var logoutButton = null;
+ var requestOptions = [
+ 'siteName',
+ 'siteLogo',
+ 'oncancel',
+ 'privacyPolicy',
+ 'returnTo',
+ 'termsOfService'
+ ];
+
+ $(document).delegate('.browserid-login, #browserid', 'click', function(e) {
+ e.preventDefault();
+
+ // Arguments to navigator.id.request can be specified by data-attributes
+ // on the BrowserID link: <a href="#" data-site-name="Site Name">
+ var options = {};
+ var $link = $(e.target);
+ for (var k = 0; k < requestOptions.length; k++) {
+ var name = requestOptions[k];
+ var value = $link.data(name);
+ if (value !== undefined) {
+ options[name] = value;
+ }
+ }
+
+ navigator.id.request(options); // Triggers BrowserID login dialog.
+ });
+
+ $('.browserid-logout').bind('click', function(e) {
+ e.preventDefault();
+ logoutButton = e.target;
+ navigator.id.logout(); // Clears User Agent BrowserID state.
+ });
+
+ navigator.id.watch({
+ onlogin: function(assertion) {
+ // Don't bother if login just failed.
+ if (location.search.indexOf('bid_login_failed=1') !== -1) {
+ navigator.id.logout();
+ } else if (assertion) {
+ var $e = $('#id_assertion');
+ $e.val(assertion.toString());
+ $e.parent().submit();
+ }
+ },
+
+ onlogout: function() {
+ var currentButton = logoutButton;
+ if (currentButton !== null) {
+ logoutButton = null;
+ if (currentButton.href) {
+ window.location = currentButton.href;
+ }
+ }
}
});
});
-});
+})(jQuery);
View
2 lib/python/django_browserid/tests/__init__.py
@@ -31,7 +31,7 @@ def __init__(self, email=None, audience=None):
self.return_value = {
u'audience': audience,
u'email': email,
- u'issuer': u'browserid.org:443',
+ u'issuer': u'login.persona.org:443',
u'status': u'okay' if email is not None else u'failure',
u'valid-until': 1311377222765
}
View
23 lib/python/django_browserid/tests/models.py
@@ -0,0 +1,23 @@
+"""
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+from django.db import models
+
+try:
+ from django.contrib.auth.models import AbstractBaseUser
+except ImportError:
+ AbstractBaseUser = object
+
+
+class CustomUser(AbstractBaseUser):
+ USERNAME_FIELD = 'email'
+
+ email = models.EmailField(unique=True, db_index=True)
+
+ def get_full_name(self):
+ return self.email
+
+ def get_short_name(self):
+ return self.email
View
3 lib/python/django_browserid/tests/settings.py
@@ -5,6 +5,8 @@
"""
TEST_RUNNER = 'django_nose.runner.NoseTestSuiteRunner'
+SECRET_KEY = 'asdf'
+
DATABASES = {
'default': {
'NAME': 'test.db',
@@ -15,6 +17,7 @@
INSTALLED_APPS = (
'django_nose',
'django_browserid',
+ 'django_browserid.tests',
'django.contrib.auth',
'django.contrib.contenttypes',
View
104 lib/python/django_browserid/tests/test_auth.py
@@ -7,11 +7,23 @@
from django.contrib.auth.models import User
from django.test import TestCase
-from mock import patch
+from mock import ANY, patch
from django_browserid.auth import BrowserIDBackend, default_username_algo
from django_browserid.tests import mock_browserid
+# Support Python 2.6 by using unittest2
+try:
+ from unittest import skipIf
+except ImportError:
+ from unittest2 import skipIf
+
+try:
+ from django.contrib.auth import get_user_model
+ from django_browserid.tests.models import CustomUser
+except ImportError:
+ get_user_model = False
+
def new_user(email, username=None):
"""Creates a user with the specified email for testing."""
@@ -25,72 +37,98 @@ def auth(self, verified_email=None):
"""
Attempt to authenticate a user with BrowserIDBackend.
- If verified_email is None, verification will fail, otherwise it will pass
- and return the specified email.
+ If verified_email is None, verification will fail, otherwise it will
+ pass and return the specified email.
"""
with mock_browserid(verified_email):
backend = BrowserIDBackend()
return backend.authenticate(assertion='asdf', audience='asdf')
def test_failed_verification(self):
- """If verification fails, return None."""
- assert self.auth(None) is None
+ # If verification fails, return None.
+ self.assertTrue(self.auth(None) is None)
def test_duplicate_emails(self):
- """If there are two users with the same email address, return None."""
+ # If there are two users with the same email address, return None.
new_user('a@example.com', 'test1')
new_user('a@example.com', 'test2')
- assert self.auth('a@example.com') is None
+ self.assertTrue(self.auth('a@example.com') is None)
def test_auth_success(self):
- """
- If a single user is found with the verified email, return an instance of
- their user object.
- """
+ # If a single user is found with the verified email, return an instance
+ # of their user object.
user = new_user('a@example.com')
- assert self.auth('a@example.com') == user
+ self.assertEqual(self.auth('a@example.com'), user)
@patch.object(settings, 'BROWSERID_CREATE_USER', False)
def test_no_create_user(self):
- """If user creation is disabled and no user is found, return None."""
- assert self.auth('a@example.com') is None
+ # If user creation is disabled and no user is found, return None.
+ self.assertTrue(self.auth('a@example.com') is None)
@patch.object(settings, 'BROWSERID_CREATE_USER', True)
def test_create_user(self):
- """
- If user creation is enabled and no user is found, return a new
- User.
- """
+ # If user creation is enabled and no user is found, return a new
+ # User.
user = self.auth('a@example.com')
- assert user is not None
- assert isinstance(user, User)
- assert user.email == 'a@example.com'
+ self.assertTrue(user is not None)
+ self.assertTrue(isinstance(user, User))
+ self.assertEqual(user.email, 'a@example.com')
@patch.object(settings, 'BROWSERID_CREATE_USER',
'django_browserid.tests.test_auth.new_user')
@patch('django_browserid.tests.test_auth.new_user')
def test_custom_create_user(self, create_user):
- """
- If user creation is enabled with a custom create function and no user
- is found, return the new user created with the custom function.
- """
+ # If user creation is enabled with a custom create function and no user
+ # is found, return the new user created with the custom function.
create_user.return_value = 'test'
- assert self.auth('a@example.com') == 'test'
- assert create_user.called_with('a@example.com')
+ self.assertEqual(self.auth('a@example.com'), 'test')
+ create_user.assert_called_with('a@example.com')
@patch.object(settings, 'BROWSERID_USERNAME_ALGO')
@patch.object(settings, 'BROWSERID_CREATE_USER', True)
def test_custom_username_algorithm(self, username_algo):
- """If a custom username algorithm is specified, use it!"""
+ # If a custom username algorithm is specified, use it!
username_algo.return_value = 'test'
user = self.auth('a@b.com')
- assert user.username == 'test'
+ self.assertEqual(user.username, 'test')
- @patch('django_browserid.signals.user_created')
+ @patch('django_browserid.auth.user_created')
@patch.object(settings, 'BROWSERID_CREATE_USER', True)
def test_user_created_signal(self, user_created):
+ # Test that the user_created signal is called when a new user is
+ # created.
+ user = self.auth('a@b.com')
+ user_created.send.assert_called_with(ANY, user=user)
+
+
+# Only run custom user model tests if we're using a version of Django that
+# supports it.
+@patch.object(settings, 'AUTH_USER_MODEL', 'tests.CustomUser')
+@skipIf(not get_user_model, 'Not supported in Django < 1.5')
+class CustomUserModelTests(TestCase):
+ def _auth(self, backend=None, verified_email=None):
+ if backend is None:
+ backend = BrowserIDBackend()
+
+ with mock_browserid(verified_email):
+ return backend.authenticate(assertion='asdf', audience='asdf')
+
+ def test_existing_user(self):
+ """If a custom user exists with the given email, return them."""
+ user = CustomUser.objects.create(email='a@test.com')
+ authed_user = self._auth(verified_email='a@test.com')
+ self.assertEqual(user, authed_user)
+
+ @patch.object(settings, 'BROWSERID_CREATE_USER', True)
+ def test_create_new_user(self):
"""
- Test that the user_created signal is called when a new user is created.
+ If a custom user does not exist with the given email, create a new
+ user and return them.
"""
- user = self.auth('a@b.com')
- assert user_created.call.called_with(user=user)
+ class CustomUserBrowserIDBackend(BrowserIDBackend):
+ def create_user(self, email):
+ return CustomUser.objects.create(email=email)
+ user = self._auth(backend=CustomUserBrowserIDBackend(),
+ verified_email='b@test.com')
+ self.assertTrue(isinstance(user, CustomUser))
+ self.assertEqual(user.email, 'b@test.com')
View
46 lib/python/django_browserid/tests/test_verification.py
@@ -9,7 +9,7 @@
from django.contrib import auth
from django.contrib.auth.models import User
-from mock import patch
+from mock import ANY, patch
from django_browserid.base import verify
from django_browserid.tests import mock_browserid
@@ -38,35 +38,37 @@ def negative_assertion(fake_http_request, **kwargs):
@patch('django_browserid.auth.BrowserIDBackend.authenticate')
def test_backend_authenticate(fake):
- """Test that the authentication backend is set up correctly."""
+ # Test that the authentication backend is set up correctly.
fake.return_value = None
auth.authenticate(**authenticate_kwargs)
fake.assert_called_with(**authenticate_kwargs)
@patch('django_browserid.auth.verify')
def test_backend_verify(fake):
- """Test that authenticate() calls verify()."""
+ # Test that authenticate() calls verify().
fake.return_value = False
auth.authenticate(**authenticate_kwargs)
fake.assert_called_with(assertion, audience)
@mock_browserid(None)
def test_backend_verify_invalid_assertion():
- """Test that authenticate() returns None when credentials are bad."""
+ # Test that authenticate() returns None when credentials are bad.
user = auth.authenticate(**authenticate_kwargs)
assert user is None
+
@patch('django_browserid.auth.verify')
def test_auth_copes_with_false(verify):
- """Test that authenticate copes with False."""
+ # Test that authenticate copes with False.
verify.return_value = False
assert BrowserIDBackend().authenticate(**authenticate_kwargs) is None
+
@mock_browserid('myemail@example.com')
def test_verify_correct_credentials():
- """Test that verify() returns assertion details when assertion is valid."""
+ # Test that verify() returns assertion details when assertion is valid.
verification = verify(assertion, audience)
assert verification['status'] == 'okay'
assert verification['email'] == 'myemail@example.com'
@@ -76,11 +78,14 @@ def test_verify_correct_credentials():
@patch.object(settings, 'BROWSERID_USERNAME_ALGO', None, create=True)
@mock_browserid('bid_create@example.com')
def test_authenticate_create_user():
- """Test that automatic user creation works when enabled."""
+ # Test that automatic user creation works when enabled.
User.objects.filter(email='bid_create@example.com').delete()
- assert User.objects.filter(email='bid_create@example.com').exists() == False
+ ob = User.objects.filter(email='bid_create@example.com')
+ assert ob.exists() is False
auth.authenticate(**authenticate_kwargs)
- assert User.objects.filter(email='bid_create@example.com').exists() == True
+
+ ob = User.objects.filter(email='bid_create@example.com')
+ assert ob.exists() is True
def username_algo(email):
@@ -91,8 +96,8 @@ def username_algo(email):
@patch.object(settings, 'BROWSERID_USERNAME_ALGO', username_algo, create=True)
@mock_browserid('bid_alt_username@example.com')
def test_authenticate_create_user_with_alternate_username_algo():
- """Test that automatic user creation with an alternate username algo
- works."""
+ # Test that automatic user creation with an alternate username algo
+ # works.
user = auth.authenticate(**authenticate_kwargs)
assert user.username == 'bid_alt_username'
@@ -102,7 +107,7 @@ def test_authenticate_create_user_with_alternate_username_algo():
@patch('django_browserid.tests.fake_create_user')
@mock_browserid('does.not.exist@example.org')
def test_authenticate_create_user_with_callable(fake):
- """Test that automatic user creation with a callable function name works"""
+ # Test that automatic user creation with a callable function name works
fake.return_value = None
auth.authenticate(**authenticate_kwargs)
fake.assert_called_with('does.not.exist@example.org')
@@ -111,6 +116,21 @@ def test_authenticate_create_user_with_callable(fake):
@patch.object(settings, 'BROWSERID_CREATE_USER', False, create=True)
@mock_browserid('someotheremail@example.com')
def test_authenticate_missing_user():
- """Test that authenticate() returns None when user creation disabled."""
+ # Test that authenticate() returns None when user creation disabled.
user = auth.authenticate(**authenticate_kwargs)
assert user is None
+
+
+@patch.object(settings, 'BROWSERID_HTTP_TIMEOUT', 1, create=True)
+@patch.object(settings, 'BROWSERID_VERIFICATION_URL',
+ 'https://custom.org/verify', create=True)
+@patch('django_browserid.base.requests.post')
+def test_verify_post_uses_custom_settings(post):
+ post.return_value.content = '{"status": "okay"}'
+ verify(assertion, audience)
+ post.assert_called_with('https://custom.org/verify',
+ verify=True,
+ proxies=ANY,
+ data=ANY,
+ timeout=1,
+ headers=ANY)
View
42 lib/python/django_browserid/tests/test_views.py
@@ -59,40 +59,57 @@ def verify(request_type, redirect_field_name=None, success_url=None,
def test_get_redirect_failure():
- """Issuing a GET to the verify view redirects to the failure URL."""
+ # Issuing a GET to the verify view redirects to the failure URL.
response = verify('get', failure_url='/fail')
assert response.status_code == 302
- assert response['Location'].endswith('/fail')
+ assert response['Location'].endswith('/fail?bid_login_failed=1')
def test_invalid_redirect_failure():
- """Invalid form arguments redirect to the failure URL."""
+ # Invalid form arguments redirect to the failure URL.
response = verify('post', failure_url='/fail', blah='asdf')
assert response.status_code == 302
- assert response['Location'].endswith('/fail')
+ assert response['Location'].endswith('/fail?bid_login_failed=1')
@mock_browserid(None)
def test_auth_fail_redirect_failure():
- """If authentication fails, redirect to the failure URL."""
+ # If authentication fails, redirect to the failure URL.
response = verify('post', failure_url='/fail', assertion='asdf')
assert response.status_code == 302
- assert response['Location'].endswith('/fail')
+ assert response['Location'].endswith('/fail?bid_login_failed=1')
+
+
+@mock_browserid(None)
+def test_auth_fail_url_parameters():
+ # Ensure that bid_login_failed=1 is appended to the failure url.
+ response = verify('post', failure_url='/fail?', assertion='asdf')
+ assert response['Location'].endswith('/fail?bid_login_failed=1')
+
+ response = verify('post', failure_url='/fail?asdf', assertion='asdf')
+ assert response['Location'].endswith('/fail?asdf&bid_login_failed=1')
+
+ response = verify('post', failure_url='/fail?asdf=4', assertion='asdf')
+ assert response['Location'].endswith('/fail?asdf=4&bid_login_failed=1')
+
+ response = verify('post', failure_url='/fail?asdf=4&bid_login_failed=1',
+ assertion='asdf')
+ assert response['Location'].endswith('/fail?asdf=4&bid_login_failed=1'
+ '&bid_login_failed=1')
@mock_browserid('test@example.com')
def test_auth_success_redirect_success():
- """If authentication succeeds, redirect to the success URL."""
+ # If authentication succeeds, redirect to the success URL.
response = verify('post', success_url='/success', assertion='asdf')
assert response.status_code == 302
assert response['Location'].endswith('/success')
@mock_browserid('test@example.com')
def test_default_redirect_field():
- """If a redirect is passed as an argument to the request, redirect to that
- instead of the success URL.
- """
+ # If a redirect is passed as an argument to the request, redirect to that
+ # instead of the success URL.
kwargs = {auth.REDIRECT_FIELD_NAME: '/field_success', 'assertion': 'asdf'}
response = verify('post', success_url='/success', **kwargs)
assert response.status_code == 302
@@ -101,9 +118,8 @@ def test_default_redirect_field():
@mock_browserid('test@example.com')
def test_redirect_field_name():
- """If a redirect field name is specified, use the request argument matching
- that name as the path to redirect to.
- """
+ # If a redirect field name is specified, use the request argument matching
+ # that name as the path to redirect to.
kwargs = {'my_redirect': '/field_success', 'assertion': 'asdf'}
response = verify('post', success_url='/success',
redirect_field_name='my_redirect', **kwargs)
View
14 lib/python/django_browserid/views.py
@@ -36,12 +36,20 @@ def login_failure(self):
"""Handle a failed login. Use this to perform complex redirects
post-login.
"""
- return redirect(self.get_failure_url())
+ # Append "?bid_login_failed=1" to the URL to notify the JavaScript that
+ # login failed.
+ failure_url = self.get_failure_url()
+
+ if not failure_url.endswith('?'):
+ failure_url += '?' if not '?' in failure_url else '&'
+ failure_url += 'bid_login_failed=1'
+
+ return redirect(failure_url)
def form_valid(self, form):
"""Handles the return post request from the browserID form and puts
interesting variables into the class. If everything checks out, then
- we call handle_user to decide how to handle a valid user
+ we call login_success to decide how to handle a valid user
"""
self.assertion = form.cleaned_data['assertion']
self.audience = get_audience(self.request)
@@ -58,7 +66,7 @@ def form_invalid(self, *args, **kwargs):
return self.login_failure()
def get(self, *args, **kwargs):
- return redirect(self.get_failure_url())
+ return self.login_failure()
def get_failure_url(self):
"""
View
35 lib/python/requests/__init__.py
@@ -6,17 +6,44 @@
# /
"""
-requests
-~~~~~~~~
+requests HTTP library
+~~~~~~~~~~~~~~~~~~~~~
+
+Requests is an HTTP library, written in Python, for human beings. Basic GET
+usage:
+
+ >>> import requests
+ >>> r = requests.get('http://python.org')
+ >>> r.status_code
+ 200
+ >>> 'Python is a programming language' in r.content
+ True
+
+... or POST:
+
+ >>> payload = dict(key1='value1', key2='value2')
+ >>> r = requests.post("http://httpbin.org/post", data=payload)
+ >>> print r.text
+ {
+ ...
+ "form": {
+ "key2": "value2",
+ "key1": "value1"
+ },
+ ...
+ }
+
+The other HTTP methods are supported - see `requests.api`. Full documentation
+is at <http://python-requests.org>.
:copyright: (c) 2012 by Kenneth Reitz.
:license: ISC, see LICENSE for more details.
"""
__title__ = 'requests'
-__version__ = '0.13.0'
-__build__ = 0x001300
+__version__ = '0.14.2'
+__build__ = 0x001402
__author__ = 'Kenneth Reitz'
__license__ = 'ISC'
__copyright__ = 'Copyright 2012 Kenneth Reitz'
View
23 lib/python/requests/_oauth.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+"""
+requests._oauth
+~~~~~~~~~~~~~~~
+
+This module contains the path hack necessary for oauthlib to be vendored into
+requests while allowing upstream changes.
+"""
+
+import os
+import sys
+
+try:
+ from oauthlib.oauth1 import rfc5849
+ from oauthlib.common import extract_params
+ from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
+except ImportError:
+ from .packages import oauthlib
+ sys.modules['oauthlib'] = oauthlib
+ from oauthlib.oauth1 import rfc5849
+ from oauthlib.common import extract_params
+ from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
View
58 lib/python/requests/adapters.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+"""
+requests.adapters
+~~~~~~~~~~~~~~~~~
+
+This module contains the transport adapters that Requests uses to define
+and maintain connections.
+"""
+
+from .packages.urllib3.poolmanager import PoolManager
+
+class BaseAdapter(object):
+ """The Base Transport Adapter"""
+
+ def __init__(self, config=None):
+ super(BaseAdapter, self).__init__()
+ self.config = config or {}
+ self.session = None
+
+ @property
+ def configure(self, config):
+ self.config.update(config)
+
+ def send(self):
+ raise NotImplementedError
+
+ def close(self):
+ raise NotImplementedError
+
+
+class HTTPAdapter(BaseAdapter):
+ """Built-In HTTP Adapter for Urllib3."""
+ def __init__(self):
+ super(HTTPAdapter, self).__init__()
+
+ self.init_poolmanager()
+
+ def init_poolmanager(self):
+ self.poolmanager = PoolManager(
+ num_pools=self.config.get('pool_connections'),
+ maxsize=self.config.get('pool_maxsize')
+ )
+
+ def close(self):
+ """Dispose of any internal state.
+
+ Currently, this just closes the PoolManager, which closes pooled
+ connections.
+ """
+ self.poolmanager.clear()
+
+ def send(self, request):
+ """Sends request object. Returns Response object."""
+ pass
+
+
+
View
17 lib/python/requests/api.py
@@ -14,6 +14,7 @@
from . import sessions
from .safe_mode import catch_exceptions_if_in_safe_mode
+
@catch_exceptions_if_in_safe_mode
def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request <Request>`.
@@ -38,9 +39,19 @@ def request(method, url, **kwargs):
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
"""
- s = kwargs.pop('session') if 'session' in kwargs else sessions.session()
- return s.request(method=method, url=url, **kwargs)
-
+ # if this session was passed in, leave it open (and retain pooled connections);
+ # if we're making it just for this call, then close it when we're done.
+ adhoc_session = False
+ session = kwargs.pop('session', None)
+ if session is None:
+ session = sessions.session()
+ adhoc_session = True
+
+ try:
+ return session.request(method=method, url=url, **kwargs)
+ finally:
+ if adhoc_session:
+ session.close()
def get(url, **kwargs):
View
361 lib/python/requests/auth.py
@@ -8,24 +8,33 @@
"""
import os
+import re
import time
import hashlib
+import logging
from base64 import b64encode
from .compat import urlparse, str
from .utils import parse_dict_header
try:
- from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
- from oauthlib.common import extract_params
- # hush pyflakes:
- SIGNATURE_HMAC; SIGNATURE_TYPE_AUTH_HEADER
+ from ._oauth import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER, extract_params)
+
except (ImportError, SyntaxError):
SIGNATURE_HMAC = None
SIGNATURE_TYPE_AUTH_HEADER = None
+try:
+ import kerberos as k
+except ImportError as exc:
+ k = None
+
+log = logging.getLogger(__name__)
+
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
def _basic_auth_str(username, password):
"""Returns a Basic Auth string."""
@@ -66,29 +75,37 @@ def __call__(self, r):
Parameters may be included from the body if the content-type is
urlencoded, if no content type is set an educated guess is made.
"""
- contenttype = r.headers.get('Content-Type', None)
+ # split(";") because Content-Type may be "multipart/form-data; boundary=xxxxx"
+ contenttype = r.headers.get('Content-Type', '').split(";")[0].lower()
# extract_params will not give params unless the body is a properly
# formatted string, a dictionary or a list of 2-tuples.
- decoded_body = extract_params(r.data)
- if contenttype == None and decoded_body != None:
- # extract_params can only check the present r.data and does not know
- # of r.files, thus an extra check is performed. We know that
- # if files are present the request will not have
- # Content-type: x-www-form-urlencoded. We guess it will have
- # a mimetype of multipart/form-encoded and if this is not the case
- # we assume the correct header will be set later.
- if r.files:
- # Omit body data in the signing and since it will always
- # be empty (cant add paras to body if multipart) and we wish
- # to preserve body.
- r.headers['Content-Type'] = 'multipart/form-encoded'
- r.url, r.headers, _ = self.client.sign(
- unicode(r.url), unicode(r.method), None, r.headers)
- else:
- # Normal signing
- r.headers['Content-Type'] = 'application/x-www-form-urlencoded'
- r.url, r.headers, r.data = self.client.sign(
- unicode(r.url), unicode(r.method), r.data, r.headers)
+ decoded_body = extract_params(r.data)
+
+ # extract_params can only check the present r.data and does not know
+ # of r.files, thus an extra check is performed. We know that
+ # if files are present the request will not have
+ # Content-type: x-www-form-urlencoded. We guess it will have
+ # a mimetype of multipart/form-data and if this is not the case
+ # we assume the correct header will be set later.
+ _oauth_signed = True
+ if r.files and contenttype == CONTENT_TYPE_MULTI_PART:
+ # Omit body data in the signing and since it will always
+ # be empty (cant add paras to body if multipart) and we wish
+ # to preserve body.
+ r.url, r.headers, _ = self.client.sign(
+ unicode(r.full_url), unicode(r.method), None, r.headers)
+ elif decoded_body is not None and contenttype in (CONTENT_TYPE_FORM_URLENCODED, ''):
+ # Normal signing
+ if not contenttype:
+ r.headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
+ r.url, r.headers, r.data = self.client.sign(
+ unicode(r.full_url), unicode(r.method), r.data, r.headers)
+ else:
+ _oauth_signed = False
+ if _oauth_signed:
+ # Both flows add params to the URL by using r.full_url,
+ # so this prevents adding it again later
+ r.params = {}
# Having the authorization header, key or value, in unicode will
# result in UnicodeDecodeErrors when the request is concatenated
@@ -100,12 +117,12 @@ def __call__(self, r):
# >>> d
# { u'a' : 'foo' }
u_header = unicode('Authorization')
- if u_header in r.headers:
+ if u_header in r.headers:
auth_header = r.headers[u_header].encode('utf-8')
del r.headers[u_header]
r.headers['Authorization'] = auth_header
- return r
+ return r
class HTTPBasicAuth(AuthBase):
@@ -131,91 +148,101 @@ class HTTPDigestAuth(AuthBase):
def __init__(self, username, password):
self.username = username
self.password = password
+ self.last_nonce = ''
+ self.nonce_count = 0
+ self.chal = {}
+
+ def build_digest_header(self, method, url):
+
+ realm = self.chal['realm']
+ nonce = self.chal['nonce']
+ qop = self.chal.get('qop')
+ algorithm = self.chal.get('algorithm', 'MD5')
+ opaque = self.chal.get('opaque', None)
+
+ algorithm = algorithm.upper()
+ # lambdas assume digest modules are imported at the top level
+ if algorithm == 'MD5':
+ def md5_utf8(x):
+ if isinstance(x, str):
+ x = x.encode('utf-8')
+ return hashlib.md5(x).hexdigest()
+ hash_utf8 = md5_utf8
+ elif algorithm == 'SHA':
+ def sha_utf8(x):
+ if isinstance(x, str):
+ x = x.encode('utf-8')
+ return hashlib.sha1(x).hexdigest()
+ hash_utf8 = sha_utf8
+ # XXX MD5-sess
+ KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
+
+ if hash_utf8 is None:
+ return None
+
+ # XXX not implemented yet
+ entdig = None
+ p_parsed = urlparse(url)
+ path = p_parsed.path
+ if p_parsed.query:
+ path += '?' + p_parsed.query
+
+ A1 = '%s:%s:%s' % (self.username, realm, self.password)
+ A2 = '%s:%s' % (method, path)
+
+ if qop == 'auth':
+ if nonce == self.last_nonce:
+ self.nonce_count += 1
+ else:
+ self.nonce_count = 1
+
+ ncvalue = '%08x' % self.nonce_count
+ s = str(self.nonce_count).encode('utf-8')
+ s += nonce.encode('utf-8')
+ s += time.ctime().encode('utf-8')
+ s += os.urandom(8)
+
+ cnonce = (hashlib.sha1(s).hexdigest()[:16])
+ noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, hash_utf8(A2))
+ respdig = KD(hash_utf8(A1), noncebit)
+ elif qop is None:
+ respdig = KD(hash_utf8(A1), "%s:%s" % (nonce, hash_utf8(A2)))
+ else:
+ # XXX handle auth-int.
+ return None
+
+ self.last_nonce = nonce
+
+ # XXX should the partial digests be encoded too?
+ base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
+ 'response="%s"' % (self.username, realm, nonce, path, respdig)
+ if opaque:
+ base += ', opaque="%s"' % opaque
+ if entdig:
+ base += ', digest="%s"' % entdig
+ base += ', algorithm="%s"' % algorithm
+ if qop:
+ base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
+
+ return 'Digest %s' % (base)
def handle_401(self, r):
"""Takes the given response and tries digest-auth, if needed."""
- r.request.deregister_hook('response', self.handle_401)
+ num_401_calls = r.request.hooks['response'].count(self.handle_401)
s_auth = r.headers.get('www-authenticate', '')
- if 'digest' in s_auth.lower():
-
- last_nonce = ''
- nonce_count = 0
-
- chal = parse_dict_header(s_auth.replace('Digest ', ''))
-
- realm = chal['realm']
- nonce = chal['nonce']
- qop = chal.get('qop')
- algorithm = chal.get('algorithm', 'MD5')
- opaque = chal.get('opaque', None)
-
- algorithm = algorithm.upper()
- # lambdas assume digest modules are imported at the top level
- if algorithm == 'MD5':
- def md5_utf8(x):
- if isinstance(x, str):
- x = x.encode('utf-8')
- return hashlib.md5(x).hexdigest()
- hash_utf8 = md5_utf8
- elif algorithm == 'SHA':
- def sha_utf8(x):
- if isinstance(x, str):
- x = x.encode('utf-8')
- return hashlib.sha1(x).hexdigest()
- hash_utf8 = sha_utf8
- # XXX MD5-sess
- KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
-
- if hash_utf8 is None:
- return None
-
- # XXX not implemented yet
- entdig = None
- p_parsed = urlparse(r.request.url)
- path = p_parsed.path
- if p_parsed.query:
- path += '?' + p_parsed.query
-
- A1 = '%s:%s:%s' % (self.username, realm, self.password)
- A2 = '%s:%s' % (r.request.method, path)
-
- if qop == 'auth':
- if nonce == last_nonce:
- nonce_count += 1
- else:
- nonce_count = 1
- last_nonce = nonce
-
- ncvalue = '%08x' % nonce_count
- s = str(nonce_count).encode('utf-8')
- s += nonce.encode('utf-8')
- s += time.ctime().encode('utf-8')
- s += os.urandom(8)
-
- cnonce = (hashlib.sha1(s).hexdigest()[:16])
- noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, hash_utf8(A2))
- respdig = KD(hash_utf8(A1), noncebit)
- elif qop is None:
- respdig = KD(hash_utf8(A1), "%s:%s" % (nonce, hash_utf8(A2)))
- else:
- # XXX handle auth-int.
- return None
-
- # XXX should the partial digests be encoded too?
- base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
- 'response="%s"' % (self.username, realm, nonce, path, respdig)
- if opaque:
- base += ', opaque="%s"' % opaque
- if entdig:
- base += ', digest="%s"' % entdig
- base += ', algorithm="%s"' % algorithm
- if qop:
- base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
+ if 'digest' in s_auth.lower() and num_401_calls < 2:
+
+ self.chal = parse_dict_header(s_auth.replace('Digest ', ''))
- r.request.headers['Authorization'] = 'Digest %s' % (base)
+ # Consume content and release the original connection
+ # to allow our new request to reuse the same one.
+ r.content
+ r.raw.release_conn()
+
+ r.request.headers['Authorization'] = self.build_digest_header(r.request.method, r.request.url)
r.request.send(anyway=True)
_r = r.request.response
_r.history.append(r)
@@ -225,5 +252,131 @@ def sha_utf8(x):
return r
def __call__(self, r):
+ # If we have a saved nonce, skip the 401
+ if self.last_nonce:
+ r.headers['Authorization'] = self.build_digest_header(r.method, r.url)
r.register_hook('response', self.handle_401)
return r
+
+
+def _negotiate_value(r):
+ """Extracts the gssapi authentication token from the appropriate header"""
+
+ authreq = r.headers.get('www-authenticate', None)
+
+ if authreq:
+ rx = re.compile('(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I)
+ mo = rx.search(authreq)
+ if mo:
+ return mo.group(1)
+
+ return None
+
+
+class HTTPKerberosAuth(AuthBase):
+ """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request object."""
+ def __init__(self, require_mutual_auth=True):
+ if k is None:
+ raise Exception("Kerberos libraries unavailable")
+ self.context = None
+ self.require_mutual_auth = require_mutual_auth
+
+ def generate_request_header(self, r):
+ """Generates the gssapi authentication token with kerberos"""
+
+ host = urlparse(r.url).netloc
+ tail, _, head = host.rpartition(':')
+ domain = tail if tail else head
+
+ result, self.context = k.authGSSClientInit("HTTP@%s" % domain)
+
+ if result < 1:
+ raise Exception("authGSSClientInit failed")
+
+ result = k.authGSSClientStep(self.context, _negotiate_value(r))
+
+ if result < 0:
+ raise Exception("authGSSClientStep failed")
+
+ response = k.authGSSClientResponse(self.context)
+
+ return "Negotiate %s" % response
+
+ def authenticate_user(self, r):
+ """Handles user authentication with gssapi/kerberos"""
+
+ auth_header = self.generate_request_header(r)
+ log.debug("authenticate_user(): Authorization header: %s" % auth_header)
+ r.request.headers['Authorization'] = auth_header
+ r.request.send(anyway=True)
+ _r = r.request.response
+ _r.history.append(r)
+ log.debug("authenticate_user(): returning %s" % _r)
+ return _r
+
+ def handle_401(self, r):
+ """Handles 401's, attempts to use gssapi/kerberos authentication"""
+
+ log.debug("handle_401(): Handling: 401")
+ if _negotiate_value(r) is not None:
+ _r = self.authenticate_user(r)
+ log.debug("handle_401(): returning %s" % _r)
+ return _r
+ else:
+ log.debug("handle_401(): Kerberos is not supported")
+ log.debug("handle_401(): returning %s" % r)
+ return r
+
+ def handle_other(self, r):
+ """Handles all responses with the exception of 401s.
+
+ This is necessary so that we can authenticate responses if requested"""
+
+ log.debug("handle_other(): Handling: %d" % r.status_code)
+ self.deregister(r)
+ if self.require_mutual_auth:
+ if _negotiate_value(r) is not None:
+ log.debug("handle_other(): Authenticating the server")
+ _r = self.authenticate_server(r)
+ log.debug("handle_other(): returning %s" % _r)
+ return _r
+ else:
+ log.error("handle_other(): Mutual authentication failed")
+ raise Exception("Mutual authentication failed")
+ else:
+ log.debug("handle_other(): returning %s" % r)
+ return r
+
+ def authenticate_server(self, r):
+ """Uses GSSAPI to authenticate the server"""
+
+ log.debug("authenticate_server(): Authenticate header: %s" % _negotiate_value(r))
+ result = k.authGSSClientStep(self.context, _negotiate_value(r))
+ if result < 1:
+ raise Exception("authGSSClientStep failed")
+ _r = r.request.response
+ log.debug("authenticate_server(): returning %s" % _r)
+ return _r
+
+ def handle_response(self, r):
+ """Takes the given response and tries kerberos-auth, as needed."""
+
+ if r.status_code == 401:
+ _r = self.handle_401(r)
+ log.debug("handle_response returning %s" % _r)
+ return _r
+ else:
+ _r = self.handle_other(r)
+ log.debug("handle_response returning %s" % _r)
+ return _r
+
+ log.debug("handle_response returning %s" % r)
+ return r
+
+ def deregister(self, r):
+ """Deregisters the response handler"""
+ r.request.deregister_hook('response', self.handle_response)
+
+ def __call__(self, r):
+ r.register_hook('response', self.handle_response)
+ return r
View
3,338 lib/python/requests/cacert.pem
3,338 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
27 lib/python/requests/certs.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+ceritfi.py
+~~~~~~~~~~
+
+This module returns the installation location of cacert.pem.
+"""
+
+import os
+try:
+ import certifi
+except ImportError:
+ certifi = None
+
+
+def where():
+
+ if certifi:
+ return certifi.where()
+ else:
+ f = os.path.split(__file__)[0]
+ return os.path.join(f, 'cacert.pem')
+
+if __name__ == '__main__':
+ print(where())
View
24 lib/python/requests/compat.py
@@ -72,34 +72,48 @@
is_hpux = ('hpux' in str(sys.platform).lower()) # Complete guess.
is_solaris = ('solar==' in str(sys.platform).lower()) # Complete guess.
+try:
+ import simplejson as json
+except ImportError:
+ import json
# ---------
# Specifics
# ---------
if is_py2:
- from urllib import quote, unquote, urlencode
- from urlparse import urlparse, urlunparse, urljoin, urlsplit
+ from urllib import quote, unquote, quote_plus, unquote_plus, urlencode
+ from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag
from urllib2 import parse_http_list
import cookielib
from Cookie import Morsel
from StringIO import StringIO
+ try:
+ import cchardet as chardet
+ except ImportError:
+ from .packages import chardet
+ from .packages.urllib3.packages.ordered_dict import OrderedDict
+ builtin_str = str
bytes = str
str = unicode
basestring = basestring
+ numeric_types = (int, long, float)
elif is_py3:
- from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote
+ from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
from urllib.request import parse_http_list
from http import cookiejar as cookielib
from http.cookies import Morsel
from io import StringIO
+ from .packages import chardet2 as chardet
+ from collections import OrderedDict
+ builtin_str = str
str = str
bytes = bytes
- basestring = (str,bytes)
-
+ basestring = (str, bytes)
+ numeric_types = (int, float)
View
44 lib/python/requests/cookies.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
"""
Compatibility code to be able to use `cookielib.CookieJar` with requests.
@@ -14,6 +16,7 @@
except ImportError:
import dummy_threading as threading
+
class MockRequest(object):
"""Wraps a `requests.Request` to mimic a `urllib2.Request`.
@@ -39,7 +42,7 @@ def get_host(self):
def get_origin_req_host(self):
if self._r.response.history:
r = self._r.response.history[0]
- return urlparse(r).netloc
+ return urlparse(r.url).netloc
else:
return self.get_host()
@@ -66,6 +69,11 @@ def add_unredirected_header(self, name, value):
def get_new_headers(self):
return self._new_headers
+ @property
+ def unverifiable(self):
+ return self.is_unverifiable()
+
+
class MockResponse(object):
"""Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
@@ -86,6 +94,7 @@ def info(self):
def getheaders(self, name):
self._headers.getheaders(name)
+
def extract_cookies_to_jar(jar, request, response):
"""Extract the cookies from the response into a CookieJar.
@@ -99,12 +108,14 @@ def extract_cookies_to_jar(jar, request, response):
res = MockResponse(response._original_response.msg)
jar.extract_cookies(res, req)
+
def get_cookie_header(jar, request):
"""Produce an appropriate Cookie header string to be sent with `request`, or None."""
r = MockRequest(request)
jar.add_cookie_header(r)
return r.get_new_headers().get('Cookie')
+
def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
"""Unsets a cookie by name, by default over all domains and paths.
@@ -120,10 +131,12 @@ def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
for domain, path, name in clearables:
cookiejar.clear(domain, path, name)
+
class CookieConflictError(RuntimeError):
- """There are two cookies that meet the criteria specified in the cookie jar.
+ """There are two cookies that meet the criteria specified in the cookie jar.
Use .get and .set and include domain and path args in order to be more specific."""
+
class RequestsCookieJar(cookielib.CookieJar, collections.MutableMapping):
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict interface.
@@ -181,7 +194,7 @@ def values(self):
for cookie in iter(self):
values.append(cookie.value)
return values
-
+
def items(self):
"""Dict-like items() that returns a list of name-value tuples from the jar.
See keys() and values(). Allows client-code to call "dict(RequestsCookieJar)
@@ -215,14 +228,14 @@ def multiple_domains(self):
if cookie.domain is not None and cookie.domain in domains:
return True
domains.append(cookie.domain)
- return False # there is only one domain in jar
+ return False # there is only one domain in jar
def get_dict(self, domain=None, path=None):
"""Takes as an argument an optional domain and path and returns a plain old
Python dict of name-value pairs of cookies that meet the requirements."""
dictionary = {}
for cookie in iter(self):
- if (domain == None or cookie.domain == domain) and (path == None
+ if (domain is None or cookie.domain == domain) and (path is None
or cookie.path == path):
dictionary[cookie.name] = cookie.value
return dictionary
@@ -244,7 +257,7 @@ def __delitem__(self, name):
remove_cookie_by_name(self, name)
def _find(self, name, domain=None, path=None):
- """Requests uses this method internally to get cookie values. Takes as args name
+ """Requests uses this method internally to get cookie values. Takes as args name
and optional domain and path. Returns a cookie.value. If there are conflicting cookies,
_find arbitrarily chooses one. See _find_no_duplicates if you want an exception thrown
if there are conflicting cookies."""
@@ -257,18 +270,18 @@ def _find(self, name, domain=None, path=None):
raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
def _find_no_duplicates(self, name, domain=None, path=None):
- """__get_item__ and get call _find_no_duplicates -- never used in Requests internally.
- Takes as args name and optional domain and path. Returns a cookie.value.
- Throws KeyError if cookie is not found and CookieConflictError if there are
+ """__get_item__ and get call _find_no_duplicates -- never used in Requests internally.
+ Takes as args name and optional domain and path. Returns a cookie.value.
+ Throws KeyError if cookie is not found and CookieConflictError if there are
multiple cookies that match name and optionally domain and path."""
toReturn = None
for cookie in iter(self):
if cookie.name == name:
if domain is None or cookie.domain == domain:
if path is None or cookie.path == path:
- if toReturn != None: # if there are multiple cookies that meet passed in criteria
+ if toReturn is not None: # if there are multiple cookies that meet passed in criteria
raise CookieConflictError('There are multiple cookies with name, %r' % (name))
- toReturn = cookie.value # we will eventually return this as long as no cookie conflict
+ toReturn = cookie.value # we will eventually return this as long as no cookie conflict
if toReturn:
return toReturn
@@ -291,6 +304,7 @@ def copy(self):
"""This is not implemented. Calling this will throw an exception."""
raise NotImplementedError
+
def create_cookie(name, value, **kwargs):
"""Make a cookie from underspecified parameters.
@@ -310,8 +324,7 @@ def create_cookie(name, value, **kwargs):
comment=None,
comment_url=None,
rest={'HttpOnly': None},
- rfc2109=False,
- )
+ rfc2109=False,)
badargs = set(kwargs) - set(result)
if badargs:
@@ -326,6 +339,7 @@ def create_cookie(name, value, **kwargs):
return cookielib.Cookie(**result)
+
def morsel_to_cookie(morsel):
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
c = create_cookie(
@@ -345,10 +359,10 @@ def morsel_to_cookie(morsel):
comment=morsel['comment'],
comment_url=bool(morsel['comment']),
rest={'HttpOnly': morsel['httponly']},
- rfc2109=False,
- )
+ rfc2109=False,)
return c
+
def cookiejar_from_dict(cookie_dict, cookiejar=None):
"""Returns a CookieJar from a key/value dictionary.
View
11 lib/python/requests/defaults.py
@@ -20,20 +20,19 @@
:pool_connections: The number of active HTTP connection pools to use.
:encode_uri: If true, URIs will automatically be percent-encoded.
:trust_env: If true, the surrouding environment will be trusted (environ, netrc).
-:param store_cookies: If false, the received cookies as part of the HTTP response would be ignored.
+:store_cookies: If false, the received cookies as part of the HTTP response would be ignored.
"""
SCHEMAS = ['http', 'https']
-from . import __version__
+from .utils import default_user_agent
defaults = dict()
-
defaults['base_headers'] = {
- 'User-Agent': 'python-requests/%s' % __version__,
- 'Accept-Encoding': ', '.join(('identity', 'deflate', 'compress', 'gzip')),
+ 'User-Agent': default_user_agent(),
+ 'Accept-Encoding': ', '.join(('gzip', 'deflate', 'compress')),
'Accept': '*/*'
}
@@ -49,5 +48,3 @@
defaults['encode_uri'] = True
defaults['trust_env'] = True
defaults['store_cookies'] = True
-
-
View
10 lib/python/requests/exceptions.py
@@ -8,34 +8,44 @@
"""
+
class RequestException(RuntimeError):
"""There was an ambiguous exception that occurred while handling your
request."""
+
class HTTPError(RequestException):
"""An HTTP error occurred."""
response = None
+
class ConnectionError(RequestException):
"""A Connection error occurred."""
+
class SSLError(ConnectionError):
"""An SSL error occurred."""
+
class Timeout(RequestException):
"""The request timed out."""
+
class URLRequired(RequestException):
"""A valid URL is required to make a request."""
+
class TooManyRedirects(RequestException):
"""Too many redirects."""
+
class MissingSchema(RequestException, ValueError):
"""The URL schema (e.g. http or https) is missing."""
+
class InvalidSchema(RequestException, ValueError):
"""See defaults.py for valid schemas."""
+
class InvalidURL(RequestException, ValueError):
""" The URL provided was somehow invalid. """
View
9 lib/python/requests/hooks.py
@@ -25,8 +25,6 @@
"""
-import traceback
-
HOOKS = ('args', 'pre_request', 'pre_send', 'post_request', 'response')
@@ -43,9 +41,8 @@ def dispatch_hook(key, hooks, hook_data):
hooks = [hooks]
for hook in hooks:
- try:
- hook_data = hook(hook_data) or hook_data
- except Exception:
- traceback.print_exc()
+ _hook_data = hook(hook_data)
+ if _hook_data is not None:
+ hook_data = _hook_data
return hook_data
View
229 lib/python/requests/models.py
@@ -7,9 +7,10 @@
This module contains the primary objects that power Requests.
"""
-import json
import os
+import socket
from datetime import datetime
+from io import BytesIO
from .hooks import dispatch_hook, HOOKS
from .structures import CaseInsensitiveDict
@@ -18,6 +19,7 @@
from .auth import HTTPBasicAuth, HTTPProxyAuth
from .cookies import cookiejar_from_dict, extract_cookies_to_jar, get_cookie_header
from .packages.urllib3.exceptions import MaxRetryError, LocationParseError
+from .packages.urllib3.exceptions import TimeoutError
from .packages.urllib3.exceptions import SSLError as _SSLError
from .packages.urllib3.exceptions import HTTPError as _HTTPError
from .packages.urllib3 import connectionpool, poolmanager
@@ -29,25 +31,19 @@
from .utils import (
get_encoding_from_headers, stream_untransfer, guess_filename, requote_uri,
stream_decode_response_unicode, get_netrc_auth, get_environ_proxies,
- DEFAULT_CA_BUNDLE_PATH)
+ to_key_val_list, DEFAULT_CA_BUNDLE_PATH, parse_header_links, iter_slices,
+ guess_json_utf)
from .compat import (
cookielib, urlparse, urlunparse, urljoin, urlsplit, urlencode, str, bytes,
- StringIO, is_py2)
-
-# Import chardet if it is available.
-try:
- import chardet
- # hush pyflakes
- chardet
-except ImportError:
- chardet = None
+ StringIO, is_py2, chardet, json, builtin_str, urldefrag)
REDIRECT_STATI = (codes.moved, codes.found, codes.other, codes.temporary_moved)
CONTENT_CHUNK_SIZE = 10 * 1024
+
class Request(object):
- """The :class:`Request <Request>` object. It carries out all functionality of
- Requests. Recommended interface is with the Requests functions.
+ """The :class:`Request <Request>` object. It carries out all functionality
+ of Requests. Recommended interface is with the Requests functions.
"""
def __init__(self,
@@ -65,7 +61,7 @@ def __init__(self,
proxies=None,
hooks=None,
config=None,
- prefetch=False,
+ prefetch=True,
_poolmanager=None,
verify=None,
session=None,
@@ -79,7 +75,14 @@ def __init__(self,
self.timeout = timeout
#: Request URL.
- self.url = url
+ #: Accept objects that have string representations.
+ try:
+ self.url = unicode(url)
+ except NameError:
+ # We're on Python 3.
+ self.url = str(url)
+ except UnicodeDecodeError:
+ self.url = url
#: Dictionary of HTTP Headers to attach to the :class:`Request <Request>`.
self.headers = dict(headers or [])
@@ -90,7 +93,7 @@ def __init__(self,
#: HTTP Method to use.
self.method = method
- #: Dictionary or byte of request body data to attach to the
+ #: Dictionary, bytes or file stream of request body data to attach to the
#: :class:`Request <Request>`.
self.data = None
@@ -109,6 +112,10 @@ def __init__(self,
# Dictionary mapping protocol to the URL of the proxy (e.g. {'http': 'foo.bar:3128'})
self.proxies = dict(proxies or [])
+ for proxy_type, uri_ref in list(self.proxies.items()):
+ if not uri_ref:
+ del self.proxies[proxy_type]
+
# If no proxies are given, allow configuration by environment variables
# HTTP_PROXY and HTTPS_PROXY.
if not self.proxies and self.config.get('trust_env'):
@@ -191,7 +198,7 @@ def build(resp):
response.status_code = getattr(resp, 'status', None)
# Make headers case-insensitive.
- response.headers = CaseInsensitiveDict(getattr(resp, 'headers', None))
+ response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
# Set encoding.
response.encoding = get_encoding_from_headers(response.headers)
@@ -299,7 +306,8 @@ def build(resp):
proxies=self.proxies,
verify=self.verify,
session=self.session,
- cert=self.cert
+ cert=self.cert,
+ prefetch=self.prefetch,
)
request.send()
@@ -319,51 +327,60 @@ def _encode_params(data):
if parameters are supplied as a dict.
"""
- if isinstance(data, bytes):
+ if isinstance(data, (str, bytes)):
return data
- if isinstance(data, str):
+ elif hasattr(data, 'read'):
return data
elif hasattr(data, '__iter__'):
- try:
- dict(data)
- except ValueError:
- raise ValueError('Unable to encode lists with elements that are not 2-tuples.')
-
- params = list(data.items() if isinstance(data, dict) else data)
result = []
- for k, vs in params:
+ for k, vs in to_key_val_list(data):
for v in isinstance(vs, list) and vs or [vs]:
- result.append(
- (k.encode('utf-8') if isinstance(k, str) else k,
- v.encode('utf-8') if isinstance(v, str) else v))
+ if v is not None:
+ result.append(
+ (k.encode('utf-8') if isinstance(k, str) else k,
+ v.encode('utf-8') if isinstance(v, str) else v))
return urlencode(result, doseq=True)
else:
return data
def _encode_files(self, files):
+ """Build the body for a multipart/form-data request.
+ Will successfully encode files when passed as a dict or a list of
+ 2-tuples. Order is retained if data is a list of 2-tuples but abritrary
+ if parameters are supplied as a dict.