Mercurial > repos > bcclaywell > argo_navis
comparison venv/lib/python2.7/site-packages/requests/auth.py @ 0:d67268158946 draft
planemo upload commit a3f181f5f126803c654b3a66dd4e83a48f7e203b
author | bcclaywell |
---|---|
date | Mon, 12 Oct 2015 17:43:33 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:d67268158946 |
---|---|
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 | |
16 from base64 import b64encode | |
17 | |
18 from .compat import urlparse, str | |
19 from .cookies import extract_cookies_to_jar | |
20 from .utils import parse_dict_header, to_native_string | |
21 from .status_codes import codes | |
22 | |
23 CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' | |
24 CONTENT_TYPE_MULTI_PART = 'multipart/form-data' | |
25 | |
26 | |
27 def _basic_auth_str(username, password): | |
28 """Returns a Basic Auth string.""" | |
29 | |
30 authstr = 'Basic ' + to_native_string( | |
31 b64encode(('%s:%s' % (username, password)).encode('latin1')).strip() | |
32 ) | |
33 | |
34 return authstr | |
35 | |
36 | |
37 class AuthBase(object): | |
38 """Base class that all auth implementations derive from""" | |
39 | |
40 def __call__(self, r): | |
41 raise NotImplementedError('Auth hooks must be callable.') | |
42 | |
43 | |
44 class HTTPBasicAuth(AuthBase): | |
45 """Attaches HTTP Basic Authentication to the given Request object.""" | |
46 def __init__(self, username, password): | |
47 self.username = username | |
48 self.password = password | |
49 | |
50 def __call__(self, r): | |
51 r.headers['Authorization'] = _basic_auth_str(self.username, self.password) | |
52 return r | |
53 | |
54 | |
55 class HTTPProxyAuth(HTTPBasicAuth): | |
56 """Attaches HTTP Proxy Authentication to a given Request object.""" | |
57 def __call__(self, r): | |
58 r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password) | |
59 return r | |
60 | |
61 | |
62 class HTTPDigestAuth(AuthBase): | |
63 """Attaches HTTP Digest Authentication to the given Request object.""" | |
64 def __init__(self, username, password): | |
65 self.username = username | |
66 self.password = password | |
67 # Keep state in per-thread local storage | |
68 self._thread_local = threading.local() | |
69 | |
70 def init_per_thread_state(self): | |
71 # Ensure state is initialized just once per-thread | |
72 if not hasattr(self._thread_local, 'init'): | |
73 self._thread_local.init = True | |
74 self._thread_local.last_nonce = '' | |
75 self._thread_local.nonce_count = 0 | |
76 self._thread_local.chal = {} | |
77 self._thread_local.pos = None | |
78 self._thread_local.num_401_calls = None | |
79 | |
80 def build_digest_header(self, method, url): | |
81 | |
82 realm = self._thread_local.chal['realm'] | |
83 nonce = self._thread_local.chal['nonce'] | |
84 qop = self._thread_local.chal.get('qop') | |
85 algorithm = self._thread_local.chal.get('algorithm') | |
86 opaque = self._thread_local.chal.get('opaque') | |
87 | |
88 if algorithm is None: | |
89 _algorithm = 'MD5' | |
90 else: | |
91 _algorithm = algorithm.upper() | |
92 # lambdas assume digest modules are imported at the top level | |
93 if _algorithm == 'MD5' or _algorithm == 'MD5-SESS': | |
94 def md5_utf8(x): | |
95 if isinstance(x, str): | |
96 x = x.encode('utf-8') | |
97 return hashlib.md5(x).hexdigest() | |
98 hash_utf8 = md5_utf8 | |
99 elif _algorithm == 'SHA': | |
100 def sha_utf8(x): | |
101 if isinstance(x, str): | |
102 x = x.encode('utf-8') | |
103 return hashlib.sha1(x).hexdigest() | |
104 hash_utf8 = sha_utf8 | |
105 | |
106 KD = lambda s, d: hash_utf8("%s:%s" % (s, d)) | |
107 | |
108 if hash_utf8 is None: | |
109 return None | |
110 | |
111 # XXX not implemented yet | |
112 entdig = None | |
113 p_parsed = urlparse(url) | |
114 #: path is request-uri defined in RFC 2616 which should not be empty | |
115 path = p_parsed.path or "/" | |
116 if p_parsed.query: | |
117 path += '?' + p_parsed.query | |
118 | |
119 A1 = '%s:%s:%s' % (self.username, realm, self.password) | |
120 A2 = '%s:%s' % (method, path) | |
121 | |
122 HA1 = hash_utf8(A1) | |
123 HA2 = hash_utf8(A2) | |
124 | |
125 if nonce == self._thread_local.last_nonce: | |
126 self._thread_local.nonce_count += 1 | |
127 else: | |
128 self._thread_local.nonce_count = 1 | |
129 ncvalue = '%08x' % self._thread_local.nonce_count | |
130 s = str(self._thread_local.nonce_count).encode('utf-8') | |
131 s += nonce.encode('utf-8') | |
132 s += time.ctime().encode('utf-8') | |
133 s += os.urandom(8) | |
134 | |
135 cnonce = (hashlib.sha1(s).hexdigest()[:16]) | |
136 if _algorithm == 'MD5-SESS': | |
137 HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce)) | |
138 | |
139 if qop is None: | |
140 respdig = KD(HA1, "%s:%s" % (nonce, HA2)) | |
141 elif qop == 'auth' or 'auth' in qop.split(','): | |
142 noncebit = "%s:%s:%s:%s:%s" % ( | |
143 nonce, ncvalue, cnonce, 'auth', HA2 | |
144 ) | |
145 respdig = KD(HA1, noncebit) | |
146 else: | |
147 # XXX handle auth-int. | |
148 return None | |
149 | |
150 self._thread_local.last_nonce = nonce | |
151 | |
152 # XXX should the partial digests be encoded too? | |
153 base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ | |
154 'response="%s"' % (self.username, realm, nonce, path, respdig) | |
155 if opaque: | |
156 base += ', opaque="%s"' % opaque | |
157 if algorithm: | |
158 base += ', algorithm="%s"' % algorithm | |
159 if entdig: | |
160 base += ', digest="%s"' % entdig | |
161 if qop: | |
162 base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce) | |
163 | |
164 return 'Digest %s' % (base) | |
165 | |
166 def handle_redirect(self, r, **kwargs): | |
167 """Reset num_401_calls counter on redirects.""" | |
168 if r.is_redirect: | |
169 self._thread_local.num_401_calls = 1 | |
170 | |
171 def handle_401(self, r, **kwargs): | |
172 """Takes the given response and tries digest-auth, if needed.""" | |
173 | |
174 if self._thread_local.pos is not None: | |
175 # Rewind the file position indicator of the body to where | |
176 # it was to resend the request. | |
177 r.request.body.seek(self._thread_local.pos) | |
178 s_auth = r.headers.get('www-authenticate', '') | |
179 | |
180 if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2: | |
181 | |
182 self._thread_local.num_401_calls += 1 | |
183 pat = re.compile(r'digest ', flags=re.IGNORECASE) | |
184 self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1)) | |
185 | |
186 # Consume content and release the original connection | |
187 # to allow our new request to reuse the same one. | |
188 r.content | |
189 r.close() | |
190 prep = r.request.copy() | |
191 extract_cookies_to_jar(prep._cookies, r.request, r.raw) | |
192 prep.prepare_cookies(prep._cookies) | |
193 | |
194 prep.headers['Authorization'] = self.build_digest_header( | |
195 prep.method, prep.url) | |
196 _r = r.connection.send(prep, **kwargs) | |
197 _r.history.append(r) | |
198 _r.request = prep | |
199 | |
200 return _r | |
201 | |
202 self._thread_local.num_401_calls = 1 | |
203 return r | |
204 | |
205 def __call__(self, r): | |
206 # Initialize per-thread state, if needed | |
207 self.init_per_thread_state() | |
208 # If we have a saved nonce, skip the 401 | |
209 if self._thread_local.last_nonce: | |
210 r.headers['Authorization'] = self.build_digest_header(r.method, r.url) | |
211 try: | |
212 self._thread_local.pos = r.body.tell() | |
213 except AttributeError: | |
214 # In the case of HTTPDigestAuth being reused and the body of | |
215 # the previous request was a file-like object, pos has the | |
216 # file position of the previous body. Ensure it's set to | |
217 # None. | |
218 self._thread_local.pos = None | |
219 r.register_hook('response', self.handle_401) | |
220 r.register_hook('response', self.handle_redirect) | |
221 self._thread_local.num_401_calls = 1 | |
222 | |
223 return r |