comparison env/lib/python3.9/site-packages/requests/auth.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
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 threading
15 import warnings
16
17 from base64 import b64encode
18
19 from .compat import urlparse, str, basestring
20 from .cookies import extract_cookies_to_jar
21 from ._internal_utils import to_native_string
22 from .utils import parse_dict_header
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 # "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
72 class 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
79 class 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
100 class 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
108 class 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