comparison requests/auth.py @ 7:5eb2d5e3bf22

planemo upload for repository https://toolrepo.galaxytrakr.org/view/jpayne/bioproject_to_srr_2/556cac4fb538
author jpayne
date Sun, 05 May 2024 23:32:17 -0400
parents
children
comparison
equal deleted inserted replaced
6:b2745907b1eb 7:5eb2d5e3bf22
1 """
2 requests.auth
3 ~~~~~~~~~~~~~
4
5 This module contains the authentication handlers for Requests.
6 """
7
8 import hashlib
9 import os
10 import re
11 import threading
12 import time
13 import warnings
14 from base64 import b64encode
15
16 from ._internal_utils import to_native_string
17 from .compat import basestring, str, urlparse
18 from .cookies import extract_cookies_to_jar
19 from .utils import parse_dict_header
20
21 CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
22 CONTENT_TYPE_MULTI_PART = "multipart/form-data"
23
24
25 def _basic_auth_str(username, password):
26 """Returns a Basic Auth string."""
27
28 # "I want us to put a big-ol' comment on top of it that
29 # says that this behaviour is dumb but we need to preserve
30 # it because people are relying on it."
31 # - Lukasa
32 #
33 # These are here solely to maintain backwards compatibility
34 # for things like ints. This will be removed in 3.0.0.
35 if not isinstance(username, basestring):
36 warnings.warn(
37 "Non-string usernames will no longer be supported in Requests "
38 "3.0.0. Please convert the object you've passed in ({!r}) to "
39 "a string or bytes object in the near future to avoid "
40 "problems.".format(username),
41 category=DeprecationWarning,
42 )
43 username = str(username)
44
45 if not isinstance(password, basestring):
46 warnings.warn(
47 "Non-string passwords will no longer be supported in Requests "
48 "3.0.0. Please convert the object you've passed in ({!r}) to "
49 "a string or bytes object in the near future to avoid "
50 "problems.".format(type(password)),
51 category=DeprecationWarning,
52 )
53 password = str(password)
54 # -- End Removal --
55
56 if isinstance(username, str):
57 username = username.encode("latin1")
58
59 if isinstance(password, str):
60 password = password.encode("latin1")
61
62 authstr = "Basic " + to_native_string(
63 b64encode(b":".join((username, password))).strip()
64 )
65
66 return authstr
67
68
69 class AuthBase:
70 """Base class that all auth implementations derive from"""
71
72 def __call__(self, r):
73 raise NotImplementedError("Auth hooks must be callable.")
74
75
76 class HTTPBasicAuth(AuthBase):
77 """Attaches HTTP Basic Authentication to the given Request object."""
78
79 def __init__(self, username, password):
80 self.username = username
81 self.password = password
82
83 def __eq__(self, other):
84 return all(
85 [
86 self.username == getattr(other, "username", None),
87 self.password == getattr(other, "password", None),
88 ]
89 )
90
91 def __ne__(self, other):
92 return not self == other
93
94 def __call__(self, r):
95 r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
96 return r
97
98
99 class HTTPProxyAuth(HTTPBasicAuth):
100 """Attaches HTTP Proxy Authentication to a given Request object."""
101
102 def __call__(self, r):
103 r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
104 return r
105
106
107 class HTTPDigestAuth(AuthBase):
108 """Attaches HTTP Digest Authentication to the given Request object."""
109
110 def __init__(self, username, password):
111 self.username = username
112 self.password = password
113 # Keep state in per-thread local storage
114 self._thread_local = threading.local()
115
116 def init_per_thread_state(self):
117 # Ensure state is initialized just once per-thread
118 if not hasattr(self._thread_local, "init"):
119 self._thread_local.init = True
120 self._thread_local.last_nonce = ""
121 self._thread_local.nonce_count = 0
122 self._thread_local.chal = {}
123 self._thread_local.pos = None
124 self._thread_local.num_401_calls = None
125
126 def build_digest_header(self, method, url):
127 """
128 :rtype: str
129 """
130
131 realm = self._thread_local.chal["realm"]
132 nonce = self._thread_local.chal["nonce"]
133 qop = self._thread_local.chal.get("qop")
134 algorithm = self._thread_local.chal.get("algorithm")
135 opaque = self._thread_local.chal.get("opaque")
136 hash_utf8 = None
137
138 if algorithm is None:
139 _algorithm = "MD5"
140 else:
141 _algorithm = algorithm.upper()
142 # lambdas assume digest modules are imported at the top level
143 if _algorithm == "MD5" or _algorithm == "MD5-SESS":
144
145 def md5_utf8(x):
146 if isinstance(x, str):
147 x = x.encode("utf-8")
148 return hashlib.md5(x).hexdigest()
149
150 hash_utf8 = md5_utf8
151 elif _algorithm == "SHA":
152
153 def sha_utf8(x):
154 if isinstance(x, str):
155 x = x.encode("utf-8")
156 return hashlib.sha1(x).hexdigest()
157
158 hash_utf8 = sha_utf8
159 elif _algorithm == "SHA-256":
160
161 def sha256_utf8(x):
162 if isinstance(x, str):
163 x = x.encode("utf-8")
164 return hashlib.sha256(x).hexdigest()
165
166 hash_utf8 = sha256_utf8
167 elif _algorithm == "SHA-512":
168
169 def sha512_utf8(x):
170 if isinstance(x, str):
171 x = x.encode("utf-8")
172 return hashlib.sha512(x).hexdigest()
173
174 hash_utf8 = sha512_utf8
175
176 KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731
177
178 if hash_utf8 is None:
179 return None
180
181 # XXX not implemented yet
182 entdig = None
183 p_parsed = urlparse(url)
184 #: path is request-uri defined in RFC 2616 which should not be empty
185 path = p_parsed.path or "/"
186 if p_parsed.query:
187 path += f"?{p_parsed.query}"
188
189 A1 = f"{self.username}:{realm}:{self.password}"
190 A2 = f"{method}:{path}"
191
192 HA1 = hash_utf8(A1)
193 HA2 = hash_utf8(A2)
194
195 if nonce == self._thread_local.last_nonce:
196 self._thread_local.nonce_count += 1
197 else:
198 self._thread_local.nonce_count = 1
199 ncvalue = f"{self._thread_local.nonce_count:08x}"
200 s = str(self._thread_local.nonce_count).encode("utf-8")
201 s += nonce.encode("utf-8")
202 s += time.ctime().encode("utf-8")
203 s += os.urandom(8)
204
205 cnonce = hashlib.sha1(s).hexdigest()[:16]
206 if _algorithm == "MD5-SESS":
207 HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}")
208
209 if not qop:
210 respdig = KD(HA1, f"{nonce}:{HA2}")
211 elif qop == "auth" or "auth" in qop.split(","):
212 noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}"
213 respdig = KD(HA1, noncebit)
214 else:
215 # XXX handle auth-int.
216 return None
217
218 self._thread_local.last_nonce = nonce
219
220 # XXX should the partial digests be encoded too?
221 base = (
222 f'username="{self.username}", realm="{realm}", nonce="{nonce}", '
223 f'uri="{path}", response="{respdig}"'
224 )
225 if opaque:
226 base += f', opaque="{opaque}"'
227 if algorithm:
228 base += f', algorithm="{algorithm}"'
229 if entdig:
230 base += f', digest="{entdig}"'
231 if qop:
232 base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
233
234 return f"Digest {base}"
235
236 def handle_redirect(self, r, **kwargs):
237 """Reset num_401_calls counter on redirects."""
238 if r.is_redirect:
239 self._thread_local.num_401_calls = 1
240
241 def handle_401(self, r, **kwargs):
242 """
243 Takes the given response and tries digest-auth, if needed.
244
245 :rtype: requests.Response
246 """
247
248 # If response is not 4xx, do not auth
249 # See https://github.com/psf/requests/issues/3772
250 if not 400 <= r.status_code < 500:
251 self._thread_local.num_401_calls = 1
252 return r
253
254 if self._thread_local.pos is not None:
255 # Rewind the file position indicator of the body to where
256 # it was to resend the request.
257 r.request.body.seek(self._thread_local.pos)
258 s_auth = r.headers.get("www-authenticate", "")
259
260 if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
261
262 self._thread_local.num_401_calls += 1
263 pat = re.compile(r"digest ", flags=re.IGNORECASE)
264 self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
265
266 # Consume content and release the original connection
267 # to allow our new request to reuse the same one.
268 r.content
269 r.close()
270 prep = r.request.copy()
271 extract_cookies_to_jar(prep._cookies, r.request, r.raw)
272 prep.prepare_cookies(prep._cookies)
273
274 prep.headers["Authorization"] = self.build_digest_header(
275 prep.method, prep.url
276 )
277 _r = r.connection.send(prep, **kwargs)
278 _r.history.append(r)
279 _r.request = prep
280
281 return _r
282
283 self._thread_local.num_401_calls = 1
284 return r
285
286 def __call__(self, r):
287 # Initialize per-thread state, if needed
288 self.init_per_thread_state()
289 # If we have a saved nonce, skip the 401
290 if self._thread_local.last_nonce:
291 r.headers["Authorization"] = self.build_digest_header(r.method, r.url)
292 try:
293 self._thread_local.pos = r.body.tell()
294 except AttributeError:
295 # In the case of HTTPDigestAuth being reused and the body of
296 # the previous request was a file-like object, pos has the
297 # file position of the previous body. Ensure it's set to
298 # None.
299 self._thread_local.pos = None
300 r.register_hook("response", self.handle_401)
301 r.register_hook("response", self.handle_redirect)
302 self._thread_local.num_401_calls = 1
303
304 return r
305
306 def __eq__(self, other):
307 return all(
308 [
309 self.username == getattr(other, "username", None),
310 self.password == getattr(other, "password", None),
311 ]
312 )
313
314 def __ne__(self, other):
315 return not self == other