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