aboutsummaryrefslogtreecommitdiff
path: root/requests/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'requests/auth.py')
-rw-r--r--requests/auth.py219
1 files changed, 98 insertions, 121 deletions
diff --git a/requests/auth.py b/requests/auth.py
index a20c545..277e601 100644
--- a/requests/auth.py
+++ b/requests/auth.py
@@ -7,24 +7,22 @@ requests.auth
This module contains the authentication handlers for Requests.
"""
+import os
import time
import hashlib
+import logging
from base64 import b64encode
from .compat import urlparse, str
-from .utils import randombytes, parse_dict_header
+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
-except (ImportError, SyntaxError):
- SIGNATURE_HMAC = None
- SIGNATURE_TYPE_AUTH_HEADER = 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."""
@@ -38,38 +36,6 @@ class AuthBase(object):
def __call__(self, r):
raise NotImplementedError('Auth hooks must be callable.')
-
-class OAuth1(AuthBase):
- """Signs the request using OAuth 1 (RFC5849)"""
- def __init__(self, client_key,
- client_secret=None,
- resource_owner_key=None,
- resource_owner_secret=None,
- callback_uri=None,
- signature_method=SIGNATURE_HMAC,
- signature_type=SIGNATURE_TYPE_AUTH_HEADER,
- rsa_key=None, verifier=None):
-
- try:
- signature_type = signature_type.upper()
- except AttributeError:
- pass
-
- self.client = Client(client_key, client_secret, resource_owner_key,
- resource_owner_secret, callback_uri, signature_method,
- signature_type, rsa_key, verifier)
-
- def __call__(self, r):
- contenttype = r.headers.get('Content-Type', None)
- decoded_body = extract_params(r.data)
- if contenttype == None and decoded_body != None:
- 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)
- return r
-
-
class HTTPBasicAuth(AuthBase):
"""Attaches HTTP Basic Authentication to the given Request object."""
def __init__(self, username, password):
@@ -93,93 +59,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 += randombytes(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 ', ''))
+
+ # 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'] = 'Digest %s' % (base)
- r.request.send(anyway=True)
- _r = r.request.response
+ r.request.headers['Authorization'] = self.build_digest_header(r.request.method, r.request.url)
+ _r = r.connection.send(r.request)
_r.history.append(r)
return _r
@@ -187,5 +161,8 @@ class HTTPDigestAuth(AuthBase):
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