| OLD | NEW | 
|---|
|  | (Empty) | 
| 1 # -*- coding: utf-8 -*- |  | 
| 2 |  | 
| 3 """ |  | 
| 4 requests.auth |  | 
| 5 ~~~~~~~~~~~~~ |  | 
| 6 |  | 
| 7 This module contains the authentication handlers for Requests. |  | 
| 8 """ |  | 
| 9 |  | 
| 10 import os |  | 
| 11 import re |  | 
| 12 import time |  | 
| 13 import hashlib |  | 
| 14 import logging |  | 
| 15 |  | 
| 16 from base64 import b64encode |  | 
| 17 |  | 
| 18 from .compat import urlparse, str |  | 
| 19 from .utils import parse_dict_header |  | 
| 20 |  | 
| 21 log = logging.getLogger(__name__) |  | 
| 22 |  | 
| 23 CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' |  | 
| 24 CONTENT_TYPE_MULTI_PART = 'multipart/form-data' |  | 
| 25 |  | 
| 26 |  | 
| 27 def _basic_auth_str(username, password): |  | 
| 28     """Returns a Basic Auth string.""" |  | 
| 29 |  | 
| 30     return 'Basic ' + b64encode(('%s:%s' % (username, password)).encode('latin1'
     )).strip().decode('latin1') |  | 
| 31 |  | 
| 32 |  | 
| 33 class AuthBase(object): |  | 
| 34     """Base class that all auth implementations derive from""" |  | 
| 35 |  | 
| 36     def __call__(self, r): |  | 
| 37         raise NotImplementedError('Auth hooks must be callable.') |  | 
| 38 |  | 
| 39 |  | 
| 40 class HTTPBasicAuth(AuthBase): |  | 
| 41     """Attaches HTTP Basic Authentication to the given Request object.""" |  | 
| 42     def __init__(self, username, password): |  | 
| 43         self.username = username |  | 
| 44         self.password = password |  | 
| 45 |  | 
| 46     def __call__(self, r): |  | 
| 47         r.headers['Authorization'] = _basic_auth_str(self.username, self.passwor
     d) |  | 
| 48         return r |  | 
| 49 |  | 
| 50 |  | 
| 51 class HTTPProxyAuth(HTTPBasicAuth): |  | 
| 52     """Attaches HTTP Proxy Authentication to a given Request object.""" |  | 
| 53     def __call__(self, r): |  | 
| 54         r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.p
     assword) |  | 
| 55         return r |  | 
| 56 |  | 
| 57 |  | 
| 58 class HTTPDigestAuth(AuthBase): |  | 
| 59     """Attaches HTTP Digest Authentication to the given Request object.""" |  | 
| 60     def __init__(self, username, password): |  | 
| 61         self.username = username |  | 
| 62         self.password = password |  | 
| 63         self.last_nonce = '' |  | 
| 64         self.nonce_count = 0 |  | 
| 65         self.chal = {} |  | 
| 66 |  | 
| 67     def build_digest_header(self, method, url): |  | 
| 68 |  | 
| 69         realm = self.chal['realm'] |  | 
| 70         nonce = self.chal['nonce'] |  | 
| 71         qop = self.chal.get('qop') |  | 
| 72         algorithm = self.chal.get('algorithm') |  | 
| 73         opaque = self.chal.get('opaque') |  | 
| 74 |  | 
| 75         if algorithm is None: |  | 
| 76             _algorithm = 'MD5' |  | 
| 77         else: |  | 
| 78             _algorithm = algorithm.upper() |  | 
| 79         # lambdas assume digest modules are imported at the top level |  | 
| 80         if _algorithm == 'MD5': |  | 
| 81             def md5_utf8(x): |  | 
| 82                 if isinstance(x, str): |  | 
| 83                     x = x.encode('utf-8') |  | 
| 84                 return hashlib.md5(x).hexdigest() |  | 
| 85             hash_utf8 = md5_utf8 |  | 
| 86         elif _algorithm == 'SHA': |  | 
| 87             def sha_utf8(x): |  | 
| 88                 if isinstance(x, str): |  | 
| 89                     x = x.encode('utf-8') |  | 
| 90                 return hashlib.sha1(x).hexdigest() |  | 
| 91             hash_utf8 = sha_utf8 |  | 
| 92         # XXX MD5-sess |  | 
| 93         KD = lambda s, d: hash_utf8("%s:%s" % (s, d)) |  | 
| 94 |  | 
| 95         if hash_utf8 is None: |  | 
| 96             return None |  | 
| 97 |  | 
| 98         # XXX not implemented yet |  | 
| 99         entdig = None |  | 
| 100         p_parsed = urlparse(url) |  | 
| 101         path = p_parsed.path |  | 
| 102         if p_parsed.query: |  | 
| 103             path += '?' + p_parsed.query |  | 
| 104 |  | 
| 105         A1 = '%s:%s:%s' % (self.username, realm, self.password) |  | 
| 106         A2 = '%s:%s' % (method, path) |  | 
| 107 |  | 
| 108         if qop is None: |  | 
| 109             respdig = KD(hash_utf8(A1), "%s:%s" % (nonce, hash_utf8(A2))) |  | 
| 110         elif qop == 'auth' or 'auth' in qop.split(','): |  | 
| 111             if nonce == self.last_nonce: |  | 
| 112                 self.nonce_count += 1 |  | 
| 113             else: |  | 
| 114                 self.nonce_count = 1 |  | 
| 115 |  | 
| 116             ncvalue = '%08x' % self.nonce_count |  | 
| 117             s = str(self.nonce_count).encode('utf-8') |  | 
| 118             s += nonce.encode('utf-8') |  | 
| 119             s += time.ctime().encode('utf-8') |  | 
| 120             s += os.urandom(8) |  | 
| 121 |  | 
| 122             cnonce = (hashlib.sha1(s).hexdigest()[:16]) |  | 
| 123             noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, hash_utf
     8(A2)) |  | 
| 124             respdig = KD(hash_utf8(A1), noncebit) |  | 
| 125         else: |  | 
| 126             # XXX handle auth-int. |  | 
| 127             return None |  | 
| 128 |  | 
| 129         self.last_nonce = nonce |  | 
| 130 |  | 
| 131         # XXX should the partial digests be encoded too? |  | 
| 132         base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ |  | 
| 133                'response="%s"' % (self.username, realm, nonce, path, respdig) |  | 
| 134         if opaque: |  | 
| 135             base += ', opaque="%s"' % opaque |  | 
| 136         if algorithm: |  | 
| 137             base += ', algorithm="%s"' % algorithm |  | 
| 138         if entdig: |  | 
| 139             base += ', digest="%s"' % entdig |  | 
| 140         if qop: |  | 
| 141             base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce) |  | 
| 142 |  | 
| 143         return 'Digest %s' % (base) |  | 
| 144 |  | 
| 145     def handle_401(self, r, **kwargs): |  | 
| 146         """Takes the given response and tries digest-auth, if needed.""" |  | 
| 147 |  | 
| 148         num_401_calls = getattr(self, 'num_401_calls', 1) |  | 
| 149         s_auth = r.headers.get('www-authenticate', '') |  | 
| 150 |  | 
| 151         if 'digest' in s_auth.lower() and num_401_calls < 2: |  | 
| 152 |  | 
| 153             setattr(self, 'num_401_calls', num_401_calls + 1) |  | 
| 154             pat = re.compile(r'digest ', flags=re.IGNORECASE) |  | 
| 155             self.chal = parse_dict_header(pat.sub('', s_auth, count=1)) |  | 
| 156 |  | 
| 157             # Consume content and release the original connection |  | 
| 158             # to allow our new request to reuse the same one. |  | 
| 159             r.content |  | 
| 160             r.raw.release_conn() |  | 
| 161             prep = r.request.copy() |  | 
| 162             prep.prepare_cookies(r.cookies) |  | 
| 163 |  | 
| 164             prep.headers['Authorization'] = self.build_digest_header( |  | 
| 165                 prep.method, prep.url) |  | 
| 166             _r = r.connection.send(prep, **kwargs) |  | 
| 167             _r.history.append(r) |  | 
| 168             _r.request = prep |  | 
| 169 |  | 
| 170             return _r |  | 
| 171 |  | 
| 172         setattr(self, 'num_401_calls', 1) |  | 
| 173         return r |  | 
| 174 |  | 
| 175     def __call__(self, r): |  | 
| 176         # If we have a saved nonce, skip the 401 |  | 
| 177         if self.last_nonce: |  | 
| 178             r.headers['Authorization'] = self.build_digest_header(r.method, r.ur
     l) |  | 
| 179         r.register_hook('response', self.handle_401) |  | 
| 180         return r |  | 
| OLD | NEW | 
|---|