Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2 

3""" 

4requests.auth 

5~~~~~~~~~~~~~ 

6 

7This module contains the authentication handlers for Requests. 

8""" 

9 

10import os 

11import re 

12import time 

13import hashlib 

14import threading 

15import warnings 

16 

17from base64 import b64encode 

18 

19from .compat import urlparse, str, basestring 

20from .cookies import extract_cookies_to_jar 

21from ._internal_utils import to_native_string 

22from .utils import parse_dict_header 

23 

24CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' 

25CONTENT_TYPE_MULTI_PART = 'multipart/form-data' 

26 

27 

28def _basic_auth_str(username, password): 

29 """Returns a Basic Auth string.""" 

30 

31 # "I want us to put a big-ol' comment on top of it that 

32 # says that this behaviour is dumb but we need to preserve 

33 # it because people are relying on it." 

34 # - Lukasa 

35 # 

36 # These are here solely to maintain backwards compatibility 

37 # for things like ints. This will be removed in 3.0.0. 

38 if not isinstance(username, basestring): 

39 warnings.warn( 

40 "Non-string usernames will no longer be supported in Requests " 

41 "3.0.0. Please convert the object you've passed in ({!r}) to " 

42 "a string or bytes object in the near future to avoid " 

43 "problems.".format(username), 

44 category=DeprecationWarning, 

45 ) 

46 username = str(username) 

47 

48 if not isinstance(password, basestring): 

49 warnings.warn( 

50 "Non-string passwords will no longer be supported in Requests " 

51 "3.0.0. Please convert the object you've passed in ({!r}) to " 

52 "a string or bytes object in the near future to avoid " 

53 "problems.".format(type(password)), 

54 category=DeprecationWarning, 

55 ) 

56 password = str(password) 

57 # -- End Removal -- 

58 

59 if isinstance(username, str): 

60 username = username.encode('latin1') 

61 

62 if isinstance(password, str): 

63 password = password.encode('latin1') 

64 

65 authstr = 'Basic ' + to_native_string( 

66 b64encode(b':'.join((username, password))).strip() 

67 ) 

68 

69 return authstr 

70 

71 

72class AuthBase(object): 

73 """Base class that all auth implementations derive from""" 

74 

75 def __call__(self, r): 

76 raise NotImplementedError('Auth hooks must be callable.') 

77 

78 

79class HTTPBasicAuth(AuthBase): 

80 """Attaches HTTP Basic Authentication to the given Request object.""" 

81 

82 def __init__(self, username, password): 

83 self.username = username 

84 self.password = password 

85 

86 def __eq__(self, other): 

87 return all([ 

88 self.username == getattr(other, 'username', None), 

89 self.password == getattr(other, 'password', None) 

90 ]) 

91 

92 def __ne__(self, other): 

93 return not self == other 

94 

95 def __call__(self, r): 

96 r.headers['Authorization'] = _basic_auth_str(self.username, self.password) 

97 return r 

98 

99 

100class HTTPProxyAuth(HTTPBasicAuth): 

101 """Attaches HTTP Proxy Authentication to a given Request object.""" 

102 

103 def __call__(self, r): 

104 r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password) 

105 return r 

106 

107 

108class HTTPDigestAuth(AuthBase): 

109 """Attaches HTTP Digest Authentication to the given Request object.""" 

110 

111 def __init__(self, username, password): 

112 self.username = username 

113 self.password = password 

114 # Keep state in per-thread local storage 

115 self._thread_local = threading.local() 

116 

117 def init_per_thread_state(self): 

118 # Ensure state is initialized just once per-thread 

119 if not hasattr(self._thread_local, 'init'): 

120 self._thread_local.init = True 

121 self._thread_local.last_nonce = '' 

122 self._thread_local.nonce_count = 0 

123 self._thread_local.chal = {} 

124 self._thread_local.pos = None 

125 self._thread_local.num_401_calls = None 

126 

127 def build_digest_header(self, method, url): 

128 """ 

129 :rtype: str 

130 """ 

131 

132 realm = self._thread_local.chal['realm'] 

133 nonce = self._thread_local.chal['nonce'] 

134 qop = self._thread_local.chal.get('qop') 

135 algorithm = self._thread_local.chal.get('algorithm') 

136 opaque = self._thread_local.chal.get('opaque') 

137 hash_utf8 = None 

138 

139 if algorithm is None: 

140 _algorithm = 'MD5' 

141 else: 

142 _algorithm = algorithm.upper() 

143 # lambdas assume digest modules are imported at the top level 

144 if _algorithm == 'MD5' or _algorithm == 'MD5-SESS': 

145 def md5_utf8(x): 

146 if isinstance(x, str): 

147 x = x.encode('utf-8') 

148 return hashlib.md5(x).hexdigest() 

149 hash_utf8 = md5_utf8 

150 elif _algorithm == 'SHA': 

151 def sha_utf8(x): 

152 if isinstance(x, str): 

153 x = x.encode('utf-8') 

154 return hashlib.sha1(x).hexdigest() 

155 hash_utf8 = sha_utf8 

156 elif _algorithm == 'SHA-256': 

157 def sha256_utf8(x): 

158 if isinstance(x, str): 

159 x = x.encode('utf-8') 

160 return hashlib.sha256(x).hexdigest() 

161 hash_utf8 = sha256_utf8 

162 elif _algorithm == 'SHA-512': 

163 def sha512_utf8(x): 

164 if isinstance(x, str): 

165 x = x.encode('utf-8') 

166 return hashlib.sha512(x).hexdigest() 

167 hash_utf8 = sha512_utf8 

168 

169 KD = lambda s, d: hash_utf8("%s:%s" % (s, d)) 

170 

171 if hash_utf8 is None: 

172 return None 

173 

174 # XXX not implemented yet 

175 entdig = None 

176 p_parsed = urlparse(url) 

177 #: path is request-uri defined in RFC 2616 which should not be empty 

178 path = p_parsed.path or "/" 

179 if p_parsed.query: 

180 path += '?' + p_parsed.query 

181 

182 A1 = '%s:%s:%s' % (self.username, realm, self.password) 

183 A2 = '%s:%s' % (method, path) 

184 

185 HA1 = hash_utf8(A1) 

186 HA2 = hash_utf8(A2) 

187 

188 if nonce == self._thread_local.last_nonce: 

189 self._thread_local.nonce_count += 1 

190 else: 

191 self._thread_local.nonce_count = 1 

192 ncvalue = '%08x' % self._thread_local.nonce_count 

193 s = str(self._thread_local.nonce_count).encode('utf-8') 

194 s += nonce.encode('utf-8') 

195 s += time.ctime().encode('utf-8') 

196 s += os.urandom(8) 

197 

198 cnonce = (hashlib.sha1(s).hexdigest()[:16]) 

199 if _algorithm == 'MD5-SESS': 

200 HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce)) 

201 

202 if not qop: 

203 respdig = KD(HA1, "%s:%s" % (nonce, HA2)) 

204 elif qop == 'auth' or 'auth' in qop.split(','): 

205 noncebit = "%s:%s:%s:%s:%s" % ( 

206 nonce, ncvalue, cnonce, 'auth', HA2 

207 ) 

208 respdig = KD(HA1, noncebit) 

209 else: 

210 # XXX handle auth-int. 

211 return None 

212 

213 self._thread_local.last_nonce = nonce 

214 

215 # XXX should the partial digests be encoded too? 

216 base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ 

217 'response="%s"' % (self.username, realm, nonce, path, respdig) 

218 if opaque: 

219 base += ', opaque="%s"' % opaque 

220 if algorithm: 

221 base += ', algorithm="%s"' % algorithm 

222 if entdig: 

223 base += ', digest="%s"' % entdig 

224 if qop: 

225 base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce) 

226 

227 return 'Digest %s' % (base) 

228 

229 def handle_redirect(self, r, **kwargs): 

230 """Reset num_401_calls counter on redirects.""" 

231 if r.is_redirect: 

232 self._thread_local.num_401_calls = 1 

233 

234 def handle_401(self, r, **kwargs): 

235 """ 

236 Takes the given response and tries digest-auth, if needed. 

237 

238 :rtype: requests.Response 

239 """ 

240 

241 # If response is not 4xx, do not auth 

242 # See https://github.com/psf/requests/issues/3772 

243 if not 400 <= r.status_code < 500: 

244 self._thread_local.num_401_calls = 1 

245 return r 

246 

247 if self._thread_local.pos is not None: 

248 # Rewind the file position indicator of the body to where 

249 # it was to resend the request. 

250 r.request.body.seek(self._thread_local.pos) 

251 s_auth = r.headers.get('www-authenticate', '') 

252 

253 if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2: 

254 

255 self._thread_local.num_401_calls += 1 

256 pat = re.compile(r'digest ', flags=re.IGNORECASE) 

257 self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1)) 

258 

259 # Consume content and release the original connection 

260 # to allow our new request to reuse the same one. 

261 r.content 

262 r.close() 

263 prep = r.request.copy() 

264 extract_cookies_to_jar(prep._cookies, r.request, r.raw) 

265 prep.prepare_cookies(prep._cookies) 

266 

267 prep.headers['Authorization'] = self.build_digest_header( 

268 prep.method, prep.url) 

269 _r = r.connection.send(prep, **kwargs) 

270 _r.history.append(r) 

271 _r.request = prep 

272 

273 return _r 

274 

275 self._thread_local.num_401_calls = 1 

276 return r 

277 

278 def __call__(self, r): 

279 # Initialize per-thread state, if needed 

280 self.init_per_thread_state() 

281 # If we have a saved nonce, skip the 401 

282 if self._thread_local.last_nonce: 

283 r.headers['Authorization'] = self.build_digest_header(r.method, r.url) 

284 try: 

285 self._thread_local.pos = r.body.tell() 

286 except AttributeError: 

287 # In the case of HTTPDigestAuth being reused and the body of 

288 # the previous request was a file-like object, pos has the 

289 # file position of the previous body. Ensure it's set to 

290 # None. 

291 self._thread_local.pos = None 

292 r.register_hook('response', self.handle_401) 

293 r.register_hook('response', self.handle_redirect) 

294 self._thread_local.num_401_calls = 1 

295 

296 return r 

297 

298 def __eq__(self, other): 

299 return all([ 

300 self.username == getattr(other, 'username', None), 

301 self.password == getattr(other, 'password', None) 

302 ]) 

303 

304 def __ne__(self, other): 

305 return not self == other