Path: blob/master/venv/Lib/site-packages/requests/auth.py
811 views
# -*- coding: utf-8 -*-12"""3requests.auth4~~~~~~~~~~~~~56This module contains the authentication handlers for Requests.7"""89import os10import re11import time12import hashlib13import threading14import warnings1516from base64 import b64encode1718from .compat import urlparse, str, basestring19from .cookies import extract_cookies_to_jar20from ._internal_utils import to_native_string21from .utils import parse_dict_header2223CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'24CONTENT_TYPE_MULTI_PART = 'multipart/form-data'252627def _basic_auth_str(username, password):28"""Returns a Basic Auth string."""2930# "I want us to put a big-ol' comment on top of it that31# says that this behaviour is dumb but we need to preserve32# it because people are relying on it."33# - Lukasa34#35# These are here solely to maintain backwards compatibility36# for things like ints. This will be removed in 3.0.0.37if not isinstance(username, basestring):38warnings.warn(39"Non-string usernames will no longer be supported in Requests "40"3.0.0. Please convert the object you've passed in ({!r}) to "41"a string or bytes object in the near future to avoid "42"problems.".format(username),43category=DeprecationWarning,44)45username = str(username)4647if not isinstance(password, basestring):48warnings.warn(49"Non-string passwords will no longer be supported in Requests "50"3.0.0. Please convert the object you've passed in ({!r}) to "51"a string or bytes object in the near future to avoid "52"problems.".format(type(password)),53category=DeprecationWarning,54)55password = str(password)56# -- End Removal --5758if isinstance(username, str):59username = username.encode('latin1')6061if isinstance(password, str):62password = password.encode('latin1')6364authstr = 'Basic ' + to_native_string(65b64encode(b':'.join((username, password))).strip()66)6768return authstr697071class AuthBase(object):72"""Base class that all auth implementations derive from"""7374def __call__(self, r):75raise NotImplementedError('Auth hooks must be callable.')767778class HTTPBasicAuth(AuthBase):79"""Attaches HTTP Basic Authentication to the given Request object."""8081def __init__(self, username, password):82self.username = username83self.password = password8485def __eq__(self, other):86return all([87self.username == getattr(other, 'username', None),88self.password == getattr(other, 'password', None)89])9091def __ne__(self, other):92return not self == other9394def __call__(self, r):95r.headers['Authorization'] = _basic_auth_str(self.username, self.password)96return r979899class HTTPProxyAuth(HTTPBasicAuth):100"""Attaches HTTP Proxy Authentication to a given Request object."""101102def __call__(self, r):103r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)104return r105106107class HTTPDigestAuth(AuthBase):108"""Attaches HTTP Digest Authentication to the given Request object."""109110def __init__(self, username, password):111self.username = username112self.password = password113# Keep state in per-thread local storage114self._thread_local = threading.local()115116def init_per_thread_state(self):117# Ensure state is initialized just once per-thread118if not hasattr(self._thread_local, 'init'):119self._thread_local.init = True120self._thread_local.last_nonce = ''121self._thread_local.nonce_count = 0122self._thread_local.chal = {}123self._thread_local.pos = None124self._thread_local.num_401_calls = None125126def build_digest_header(self, method, url):127"""128:rtype: str129"""130131realm = self._thread_local.chal['realm']132nonce = self._thread_local.chal['nonce']133qop = self._thread_local.chal.get('qop')134algorithm = self._thread_local.chal.get('algorithm')135opaque = self._thread_local.chal.get('opaque')136hash_utf8 = None137138if algorithm is None:139_algorithm = 'MD5'140else:141_algorithm = algorithm.upper()142# lambdas assume digest modules are imported at the top level143if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':144def md5_utf8(x):145if isinstance(x, str):146x = x.encode('utf-8')147return hashlib.md5(x).hexdigest()148hash_utf8 = md5_utf8149elif _algorithm == 'SHA':150def sha_utf8(x):151if isinstance(x, str):152x = x.encode('utf-8')153return hashlib.sha1(x).hexdigest()154hash_utf8 = sha_utf8155elif _algorithm == 'SHA-256':156def sha256_utf8(x):157if isinstance(x, str):158x = x.encode('utf-8')159return hashlib.sha256(x).hexdigest()160hash_utf8 = sha256_utf8161elif _algorithm == 'SHA-512':162def sha512_utf8(x):163if isinstance(x, str):164x = x.encode('utf-8')165return hashlib.sha512(x).hexdigest()166hash_utf8 = sha512_utf8167168KD = lambda s, d: hash_utf8("%s:%s" % (s, d))169170if hash_utf8 is None:171return None172173# XXX not implemented yet174entdig = None175p_parsed = urlparse(url)176#: path is request-uri defined in RFC 2616 which should not be empty177path = p_parsed.path or "/"178if p_parsed.query:179path += '?' + p_parsed.query180181A1 = '%s:%s:%s' % (self.username, realm, self.password)182A2 = '%s:%s' % (method, path)183184HA1 = hash_utf8(A1)185HA2 = hash_utf8(A2)186187if nonce == self._thread_local.last_nonce:188self._thread_local.nonce_count += 1189else:190self._thread_local.nonce_count = 1191ncvalue = '%08x' % self._thread_local.nonce_count192s = str(self._thread_local.nonce_count).encode('utf-8')193s += nonce.encode('utf-8')194s += time.ctime().encode('utf-8')195s += os.urandom(8)196197cnonce = (hashlib.sha1(s).hexdigest()[:16])198if _algorithm == 'MD5-SESS':199HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))200201if not qop:202respdig = KD(HA1, "%s:%s" % (nonce, HA2))203elif qop == 'auth' or 'auth' in qop.split(','):204noncebit = "%s:%s:%s:%s:%s" % (205nonce, ncvalue, cnonce, 'auth', HA2206)207respdig = KD(HA1, noncebit)208else:209# XXX handle auth-int.210return None211212self._thread_local.last_nonce = nonce213214# XXX should the partial digests be encoded too?215base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \216'response="%s"' % (self.username, realm, nonce, path, respdig)217if opaque:218base += ', opaque="%s"' % opaque219if algorithm:220base += ', algorithm="%s"' % algorithm221if entdig:222base += ', digest="%s"' % entdig223if qop:224base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)225226return 'Digest %s' % (base)227228def handle_redirect(self, r, **kwargs):229"""Reset num_401_calls counter on redirects."""230if r.is_redirect:231self._thread_local.num_401_calls = 1232233def handle_401(self, r, **kwargs):234"""235Takes the given response and tries digest-auth, if needed.236237:rtype: requests.Response238"""239240# If response is not 4xx, do not auth241# See https://github.com/psf/requests/issues/3772242if not 400 <= r.status_code < 500:243self._thread_local.num_401_calls = 1244return r245246if self._thread_local.pos is not None:247# Rewind the file position indicator of the body to where248# it was to resend the request.249r.request.body.seek(self._thread_local.pos)250s_auth = r.headers.get('www-authenticate', '')251252if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2:253254self._thread_local.num_401_calls += 1255pat = re.compile(r'digest ', flags=re.IGNORECASE)256self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1))257258# Consume content and release the original connection259# to allow our new request to reuse the same one.260r.content261r.close()262prep = r.request.copy()263extract_cookies_to_jar(prep._cookies, r.request, r.raw)264prep.prepare_cookies(prep._cookies)265266prep.headers['Authorization'] = self.build_digest_header(267prep.method, prep.url)268_r = r.connection.send(prep, **kwargs)269_r.history.append(r)270_r.request = prep271272return _r273274self._thread_local.num_401_calls = 1275return r276277def __call__(self, r):278# Initialize per-thread state, if needed279self.init_per_thread_state()280# If we have a saved nonce, skip the 401281if self._thread_local.last_nonce:282r.headers['Authorization'] = self.build_digest_header(r.method, r.url)283try:284self._thread_local.pos = r.body.tell()285except AttributeError:286# In the case of HTTPDigestAuth being reused and the body of287# the previous request was a file-like object, pos has the288# file position of the previous body. Ensure it's set to289# None.290self._thread_local.pos = None291r.register_hook('response', self.handle_401)292r.register_hook('response', self.handle_redirect)293self._thread_local.num_401_calls = 1294295return r296297def __eq__(self, other):298return all([299self.username == getattr(other, 'username', None),300self.password == getattr(other, 'password', None)301])302303def __ne__(self, other):304return not self == other305306307