auth.py 5.5 KB

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