Mercurial > repos > bcclaywell > argo_navis
comparison venv/lib/python2.7/site-packages/boto/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 # Copyright 2010 Google Inc. | |
2 # Copyright (c) 2011 Mitch Garnaat http://garnaat.org/ | |
3 # Copyright (c) 2011, Eucalyptus Systems, Inc. | |
4 # | |
5 # Permission is hereby granted, free of charge, to any person obtaining a | |
6 # copy of this software and associated documentation files (the | |
7 # "Software"), to deal in the Software without restriction, including | |
8 # without limitation the rights to use, copy, modify, merge, publish, dis- | |
9 # tribute, sublicense, and/or sell copies of the Software, and to permit | |
10 # persons to whom the Software is furnished to do so, subject to the fol- | |
11 # lowing conditions: | |
12 # | |
13 # The above copyright notice and this permission notice shall be included | |
14 # in all copies or substantial portions of the Software. | |
15 # | |
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
17 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | |
18 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | |
19 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
20 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
22 # IN THE SOFTWARE. | |
23 | |
24 | |
25 """ | |
26 Handles authentication required to AWS and GS | |
27 """ | |
28 | |
29 import base64 | |
30 import boto | |
31 import boto.auth_handler | |
32 import boto.exception | |
33 import boto.plugin | |
34 import boto.utils | |
35 import copy | |
36 import datetime | |
37 from email.utils import formatdate | |
38 import hmac | |
39 import os | |
40 import posixpath | |
41 | |
42 from boto.compat import urllib, encodebytes | |
43 from boto.auth_handler import AuthHandler | |
44 from boto.exception import BotoClientError | |
45 | |
46 try: | |
47 from hashlib import sha1 as sha | |
48 from hashlib import sha256 as sha256 | |
49 except ImportError: | |
50 import sha | |
51 sha256 = None | |
52 | |
53 | |
54 # Region detection strings to determine if SigV4 should be used | |
55 # by default. | |
56 SIGV4_DETECT = [ | |
57 '.cn-', | |
58 # In eu-central we support both host styles for S3 | |
59 '.eu-central', | |
60 '-eu-central', | |
61 ] | |
62 | |
63 | |
64 class HmacKeys(object): | |
65 """Key based Auth handler helper.""" | |
66 | |
67 def __init__(self, host, config, provider): | |
68 if provider.access_key is None or provider.secret_key is None: | |
69 raise boto.auth_handler.NotReadyToAuthenticate() | |
70 self.host = host | |
71 self.update_provider(provider) | |
72 | |
73 def update_provider(self, provider): | |
74 self._provider = provider | |
75 self._hmac = hmac.new(self._provider.secret_key.encode('utf-8'), | |
76 digestmod=sha) | |
77 if sha256: | |
78 self._hmac_256 = hmac.new(self._provider.secret_key.encode('utf-8'), | |
79 digestmod=sha256) | |
80 else: | |
81 self._hmac_256 = None | |
82 | |
83 def algorithm(self): | |
84 if self._hmac_256: | |
85 return 'HmacSHA256' | |
86 else: | |
87 return 'HmacSHA1' | |
88 | |
89 def _get_hmac(self): | |
90 if self._hmac_256: | |
91 digestmod = sha256 | |
92 else: | |
93 digestmod = sha | |
94 return hmac.new(self._provider.secret_key.encode('utf-8'), | |
95 digestmod=digestmod) | |
96 | |
97 def sign_string(self, string_to_sign): | |
98 new_hmac = self._get_hmac() | |
99 new_hmac.update(string_to_sign.encode('utf-8')) | |
100 return encodebytes(new_hmac.digest()).decode('utf-8').strip() | |
101 | |
102 def __getstate__(self): | |
103 pickled_dict = copy.copy(self.__dict__) | |
104 del pickled_dict['_hmac'] | |
105 del pickled_dict['_hmac_256'] | |
106 return pickled_dict | |
107 | |
108 def __setstate__(self, dct): | |
109 self.__dict__ = dct | |
110 self.update_provider(self._provider) | |
111 | |
112 | |
113 class AnonAuthHandler(AuthHandler, HmacKeys): | |
114 """ | |
115 Implements Anonymous requests. | |
116 """ | |
117 | |
118 capability = ['anon'] | |
119 | |
120 def __init__(self, host, config, provider): | |
121 super(AnonAuthHandler, self).__init__(host, config, provider) | |
122 | |
123 def add_auth(self, http_request, **kwargs): | |
124 pass | |
125 | |
126 | |
127 class HmacAuthV1Handler(AuthHandler, HmacKeys): | |
128 """ Implements the HMAC request signing used by S3 and GS.""" | |
129 | |
130 capability = ['hmac-v1', 's3'] | |
131 | |
132 def __init__(self, host, config, provider): | |
133 AuthHandler.__init__(self, host, config, provider) | |
134 HmacKeys.__init__(self, host, config, provider) | |
135 self._hmac_256 = None | |
136 | |
137 def update_provider(self, provider): | |
138 super(HmacAuthV1Handler, self).update_provider(provider) | |
139 self._hmac_256 = None | |
140 | |
141 def add_auth(self, http_request, **kwargs): | |
142 headers = http_request.headers | |
143 method = http_request.method | |
144 auth_path = http_request.auth_path | |
145 if 'Date' not in headers: | |
146 headers['Date'] = formatdate(usegmt=True) | |
147 | |
148 if self._provider.security_token: | |
149 key = self._provider.security_token_header | |
150 headers[key] = self._provider.security_token | |
151 string_to_sign = boto.utils.canonical_string(method, auth_path, | |
152 headers, None, | |
153 self._provider) | |
154 boto.log.debug('StringToSign:\n%s' % string_to_sign) | |
155 b64_hmac = self.sign_string(string_to_sign) | |
156 auth_hdr = self._provider.auth_header | |
157 auth = ("%s %s:%s" % (auth_hdr, self._provider.access_key, b64_hmac)) | |
158 boto.log.debug('Signature:\n%s' % auth) | |
159 headers['Authorization'] = auth | |
160 | |
161 | |
162 class HmacAuthV2Handler(AuthHandler, HmacKeys): | |
163 """ | |
164 Implements the simplified HMAC authorization used by CloudFront. | |
165 """ | |
166 capability = ['hmac-v2', 'cloudfront'] | |
167 | |
168 def __init__(self, host, config, provider): | |
169 AuthHandler.__init__(self, host, config, provider) | |
170 HmacKeys.__init__(self, host, config, provider) | |
171 self._hmac_256 = None | |
172 | |
173 def update_provider(self, provider): | |
174 super(HmacAuthV2Handler, self).update_provider(provider) | |
175 self._hmac_256 = None | |
176 | |
177 def add_auth(self, http_request, **kwargs): | |
178 headers = http_request.headers | |
179 if 'Date' not in headers: | |
180 headers['Date'] = formatdate(usegmt=True) | |
181 if self._provider.security_token: | |
182 key = self._provider.security_token_header | |
183 headers[key] = self._provider.security_token | |
184 | |
185 b64_hmac = self.sign_string(headers['Date']) | |
186 auth_hdr = self._provider.auth_header | |
187 headers['Authorization'] = ("%s %s:%s" % | |
188 (auth_hdr, | |
189 self._provider.access_key, b64_hmac)) | |
190 | |
191 | |
192 class HmacAuthV3Handler(AuthHandler, HmacKeys): | |
193 """Implements the new Version 3 HMAC authorization used by Route53.""" | |
194 | |
195 capability = ['hmac-v3', 'route53', 'ses'] | |
196 | |
197 def __init__(self, host, config, provider): | |
198 AuthHandler.__init__(self, host, config, provider) | |
199 HmacKeys.__init__(self, host, config, provider) | |
200 | |
201 def add_auth(self, http_request, **kwargs): | |
202 headers = http_request.headers | |
203 if 'Date' not in headers: | |
204 headers['Date'] = formatdate(usegmt=True) | |
205 | |
206 if self._provider.security_token: | |
207 key = self._provider.security_token_header | |
208 headers[key] = self._provider.security_token | |
209 | |
210 b64_hmac = self.sign_string(headers['Date']) | |
211 s = "AWS3-HTTPS AWSAccessKeyId=%s," % self._provider.access_key | |
212 s += "Algorithm=%s,Signature=%s" % (self.algorithm(), b64_hmac) | |
213 headers['X-Amzn-Authorization'] = s | |
214 | |
215 | |
216 class HmacAuthV3HTTPHandler(AuthHandler, HmacKeys): | |
217 """ | |
218 Implements the new Version 3 HMAC authorization used by DynamoDB. | |
219 """ | |
220 | |
221 capability = ['hmac-v3-http'] | |
222 | |
223 def __init__(self, host, config, provider): | |
224 AuthHandler.__init__(self, host, config, provider) | |
225 HmacKeys.__init__(self, host, config, provider) | |
226 | |
227 def headers_to_sign(self, http_request): | |
228 """ | |
229 Select the headers from the request that need to be included | |
230 in the StringToSign. | |
231 """ | |
232 headers_to_sign = {'Host': self.host} | |
233 for name, value in http_request.headers.items(): | |
234 lname = name.lower() | |
235 if lname.startswith('x-amz'): | |
236 headers_to_sign[name] = value | |
237 return headers_to_sign | |
238 | |
239 def canonical_headers(self, headers_to_sign): | |
240 """ | |
241 Return the headers that need to be included in the StringToSign | |
242 in their canonical form by converting all header keys to lower | |
243 case, sorting them in alphabetical order and then joining | |
244 them into a string, separated by newlines. | |
245 """ | |
246 l = sorted(['%s:%s' % (n.lower().strip(), | |
247 headers_to_sign[n].strip()) for n in headers_to_sign]) | |
248 return '\n'.join(l) | |
249 | |
250 def string_to_sign(self, http_request): | |
251 """ | |
252 Return the canonical StringToSign as well as a dict | |
253 containing the original version of all headers that | |
254 were included in the StringToSign. | |
255 """ | |
256 headers_to_sign = self.headers_to_sign(http_request) | |
257 canonical_headers = self.canonical_headers(headers_to_sign) | |
258 string_to_sign = '\n'.join([http_request.method, | |
259 http_request.auth_path, | |
260 '', | |
261 canonical_headers, | |
262 '', | |
263 http_request.body]) | |
264 return string_to_sign, headers_to_sign | |
265 | |
266 def add_auth(self, req, **kwargs): | |
267 """ | |
268 Add AWS3 authentication to a request. | |
269 | |
270 :type req: :class`boto.connection.HTTPRequest` | |
271 :param req: The HTTPRequest object. | |
272 """ | |
273 # This could be a retry. Make sure the previous | |
274 # authorization header is removed first. | |
275 if 'X-Amzn-Authorization' in req.headers: | |
276 del req.headers['X-Amzn-Authorization'] | |
277 req.headers['X-Amz-Date'] = formatdate(usegmt=True) | |
278 if self._provider.security_token: | |
279 req.headers['X-Amz-Security-Token'] = self._provider.security_token | |
280 string_to_sign, headers_to_sign = self.string_to_sign(req) | |
281 boto.log.debug('StringToSign:\n%s' % string_to_sign) | |
282 hash_value = sha256(string_to_sign.encode('utf-8')).digest() | |
283 b64_hmac = self.sign_string(hash_value) | |
284 s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key | |
285 s += "Algorithm=%s," % self.algorithm() | |
286 s += "SignedHeaders=%s," % ';'.join(headers_to_sign) | |
287 s += "Signature=%s" % b64_hmac | |
288 req.headers['X-Amzn-Authorization'] = s | |
289 | |
290 | |
291 class HmacAuthV4Handler(AuthHandler, HmacKeys): | |
292 """ | |
293 Implements the new Version 4 HMAC authorization. | |
294 """ | |
295 | |
296 capability = ['hmac-v4'] | |
297 | |
298 def __init__(self, host, config, provider, | |
299 service_name=None, region_name=None): | |
300 AuthHandler.__init__(self, host, config, provider) | |
301 HmacKeys.__init__(self, host, config, provider) | |
302 # You can set the service_name and region_name to override the | |
303 # values which would otherwise come from the endpoint, e.g. | |
304 # <service>.<region>.amazonaws.com. | |
305 self.service_name = service_name | |
306 self.region_name = region_name | |
307 | |
308 def _sign(self, key, msg, hex=False): | |
309 if not isinstance(key, bytes): | |
310 key = key.encode('utf-8') | |
311 | |
312 if hex: | |
313 sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest() | |
314 else: | |
315 sig = hmac.new(key, msg.encode('utf-8'), sha256).digest() | |
316 return sig | |
317 | |
318 def headers_to_sign(self, http_request): | |
319 """ | |
320 Select the headers from the request that need to be included | |
321 in the StringToSign. | |
322 """ | |
323 host_header_value = self.host_header(self.host, http_request) | |
324 if http_request.headers.get('Host'): | |
325 host_header_value = http_request.headers['Host'] | |
326 headers_to_sign = {'Host': host_header_value} | |
327 for name, value in http_request.headers.items(): | |
328 lname = name.lower() | |
329 if lname.startswith('x-amz'): | |
330 if isinstance(value, bytes): | |
331 value = value.decode('utf-8') | |
332 headers_to_sign[name] = value | |
333 return headers_to_sign | |
334 | |
335 def host_header(self, host, http_request): | |
336 port = http_request.port | |
337 secure = http_request.protocol == 'https' | |
338 if ((port == 80 and not secure) or (port == 443 and secure)): | |
339 return host | |
340 return '%s:%s' % (host, port) | |
341 | |
342 def query_string(self, http_request): | |
343 parameter_names = sorted(http_request.params.keys()) | |
344 pairs = [] | |
345 for pname in parameter_names: | |
346 pval = boto.utils.get_utf8_value(http_request.params[pname]) | |
347 pairs.append(urllib.parse.quote(pname, safe='') + '=' + | |
348 urllib.parse.quote(pval, safe='-_~')) | |
349 return '&'.join(pairs) | |
350 | |
351 def canonical_query_string(self, http_request): | |
352 # POST requests pass parameters in through the | |
353 # http_request.body field. | |
354 if http_request.method == 'POST': | |
355 return "" | |
356 l = [] | |
357 for param in sorted(http_request.params): | |
358 value = boto.utils.get_utf8_value(http_request.params[param]) | |
359 l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'), | |
360 urllib.parse.quote(value, safe='-_.~'))) | |
361 return '&'.join(l) | |
362 | |
363 def canonical_headers(self, headers_to_sign): | |
364 """ | |
365 Return the headers that need to be included in the StringToSign | |
366 in their canonical form by converting all header keys to lower | |
367 case, sorting them in alphabetical order and then joining | |
368 them into a string, separated by newlines. | |
369 """ | |
370 canonical = [] | |
371 | |
372 for header in headers_to_sign: | |
373 c_name = header.lower().strip() | |
374 raw_value = str(headers_to_sign[header]) | |
375 if '"' in raw_value: | |
376 c_value = raw_value.strip() | |
377 else: | |
378 c_value = ' '.join(raw_value.strip().split()) | |
379 canonical.append('%s:%s' % (c_name, c_value)) | |
380 return '\n'.join(sorted(canonical)) | |
381 | |
382 def signed_headers(self, headers_to_sign): | |
383 l = ['%s' % n.lower().strip() for n in headers_to_sign] | |
384 l = sorted(l) | |
385 return ';'.join(l) | |
386 | |
387 def canonical_uri(self, http_request): | |
388 path = http_request.auth_path | |
389 # Normalize the path | |
390 # in windows normpath('/') will be '\\' so we chane it back to '/' | |
391 normalized = posixpath.normpath(path).replace('\\', '/') | |
392 # Then urlencode whatever's left. | |
393 encoded = urllib.parse.quote(normalized) | |
394 if len(path) > 1 and path.endswith('/'): | |
395 encoded += '/' | |
396 return encoded | |
397 | |
398 def payload(self, http_request): | |
399 body = http_request.body | |
400 # If the body is a file like object, we can use | |
401 # boto.utils.compute_hash, which will avoid reading | |
402 # the entire body into memory. | |
403 if hasattr(body, 'seek') and hasattr(body, 'read'): | |
404 return boto.utils.compute_hash(body, hash_algorithm=sha256)[0] | |
405 elif not isinstance(body, bytes): | |
406 body = body.encode('utf-8') | |
407 return sha256(body).hexdigest() | |
408 | |
409 def canonical_request(self, http_request): | |
410 cr = [http_request.method.upper()] | |
411 cr.append(self.canonical_uri(http_request)) | |
412 cr.append(self.canonical_query_string(http_request)) | |
413 headers_to_sign = self.headers_to_sign(http_request) | |
414 cr.append(self.canonical_headers(headers_to_sign) + '\n') | |
415 cr.append(self.signed_headers(headers_to_sign)) | |
416 cr.append(self.payload(http_request)) | |
417 return '\n'.join(cr) | |
418 | |
419 def scope(self, http_request): | |
420 scope = [self._provider.access_key] | |
421 scope.append(http_request.timestamp) | |
422 scope.append(http_request.region_name) | |
423 scope.append(http_request.service_name) | |
424 scope.append('aws4_request') | |
425 return '/'.join(scope) | |
426 | |
427 def split_host_parts(self, host): | |
428 return host.split('.') | |
429 | |
430 def determine_region_name(self, host): | |
431 parts = self.split_host_parts(host) | |
432 if self.region_name is not None: | |
433 region_name = self.region_name | |
434 elif len(parts) > 1: | |
435 if parts[1] == 'us-gov': | |
436 region_name = 'us-gov-west-1' | |
437 else: | |
438 if len(parts) == 3: | |
439 region_name = 'us-east-1' | |
440 else: | |
441 region_name = parts[1] | |
442 else: | |
443 region_name = parts[0] | |
444 | |
445 return region_name | |
446 | |
447 def determine_service_name(self, host): | |
448 parts = self.split_host_parts(host) | |
449 if self.service_name is not None: | |
450 service_name = self.service_name | |
451 else: | |
452 service_name = parts[0] | |
453 return service_name | |
454 | |
455 def credential_scope(self, http_request): | |
456 scope = [] | |
457 http_request.timestamp = http_request.headers['X-Amz-Date'][0:8] | |
458 scope.append(http_request.timestamp) | |
459 # The service_name and region_name either come from: | |
460 # * The service_name/region_name attrs or (if these values are None) | |
461 # * parsed from the endpoint <service>.<region>.amazonaws.com. | |
462 region_name = self.determine_region_name(http_request.host) | |
463 service_name = self.determine_service_name(http_request.host) | |
464 http_request.service_name = service_name | |
465 http_request.region_name = region_name | |
466 | |
467 scope.append(http_request.region_name) | |
468 scope.append(http_request.service_name) | |
469 scope.append('aws4_request') | |
470 return '/'.join(scope) | |
471 | |
472 def string_to_sign(self, http_request, canonical_request): | |
473 """ | |
474 Return the canonical StringToSign as well as a dict | |
475 containing the original version of all headers that | |
476 were included in the StringToSign. | |
477 """ | |
478 sts = ['AWS4-HMAC-SHA256'] | |
479 sts.append(http_request.headers['X-Amz-Date']) | |
480 sts.append(self.credential_scope(http_request)) | |
481 sts.append(sha256(canonical_request.encode('utf-8')).hexdigest()) | |
482 return '\n'.join(sts) | |
483 | |
484 def signature(self, http_request, string_to_sign): | |
485 key = self._provider.secret_key | |
486 k_date = self._sign(('AWS4' + key).encode('utf-8'), | |
487 http_request.timestamp) | |
488 k_region = self._sign(k_date, http_request.region_name) | |
489 k_service = self._sign(k_region, http_request.service_name) | |
490 k_signing = self._sign(k_service, 'aws4_request') | |
491 return self._sign(k_signing, string_to_sign, hex=True) | |
492 | |
493 def add_auth(self, req, **kwargs): | |
494 """ | |
495 Add AWS4 authentication to a request. | |
496 | |
497 :type req: :class`boto.connection.HTTPRequest` | |
498 :param req: The HTTPRequest object. | |
499 """ | |
500 # This could be a retry. Make sure the previous | |
501 # authorization header is removed first. | |
502 if 'X-Amzn-Authorization' in req.headers: | |
503 del req.headers['X-Amzn-Authorization'] | |
504 now = datetime.datetime.utcnow() | |
505 req.headers['X-Amz-Date'] = now.strftime('%Y%m%dT%H%M%SZ') | |
506 if self._provider.security_token: | |
507 req.headers['X-Amz-Security-Token'] = self._provider.security_token | |
508 qs = self.query_string(req) | |
509 | |
510 qs_to_post = qs | |
511 | |
512 # We do not want to include any params that were mangled into | |
513 # the params if performing s3-sigv4 since it does not | |
514 # belong in the body of a post for some requests. Mangled | |
515 # refers to items in the query string URL being added to the | |
516 # http response params. However, these params get added to | |
517 # the body of the request, but the query string URL does not | |
518 # belong in the body of the request. ``unmangled_resp`` is the | |
519 # response that happened prior to the mangling. This ``unmangled_req`` | |
520 # kwarg will only appear for s3-sigv4. | |
521 if 'unmangled_req' in kwargs: | |
522 qs_to_post = self.query_string(kwargs['unmangled_req']) | |
523 | |
524 if qs_to_post and req.method == 'POST': | |
525 # Stash request parameters into post body | |
526 # before we generate the signature. | |
527 req.body = qs_to_post | |
528 req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' | |
529 req.headers['Content-Length'] = str(len(req.body)) | |
530 else: | |
531 # Safe to modify req.path here since | |
532 # the signature will use req.auth_path. | |
533 req.path = req.path.split('?')[0] | |
534 | |
535 if qs: | |
536 # Don't insert the '?' unless there's actually a query string | |
537 req.path = req.path + '?' + qs | |
538 canonical_request = self.canonical_request(req) | |
539 boto.log.debug('CanonicalRequest:\n%s' % canonical_request) | |
540 string_to_sign = self.string_to_sign(req, canonical_request) | |
541 boto.log.debug('StringToSign:\n%s' % string_to_sign) | |
542 signature = self.signature(req, string_to_sign) | |
543 boto.log.debug('Signature:\n%s' % signature) | |
544 headers_to_sign = self.headers_to_sign(req) | |
545 l = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(req)] | |
546 l.append('SignedHeaders=%s' % self.signed_headers(headers_to_sign)) | |
547 l.append('Signature=%s' % signature) | |
548 req.headers['Authorization'] = ','.join(l) | |
549 | |
550 | |
551 class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler): | |
552 """ | |
553 Implements a variant of Version 4 HMAC authorization specific to S3. | |
554 """ | |
555 capability = ['hmac-v4-s3'] | |
556 | |
557 def __init__(self, *args, **kwargs): | |
558 super(S3HmacAuthV4Handler, self).__init__(*args, **kwargs) | |
559 | |
560 if self.region_name: | |
561 self.region_name = self.clean_region_name(self.region_name) | |
562 | |
563 def clean_region_name(self, region_name): | |
564 if region_name.startswith('s3-'): | |
565 return region_name[3:] | |
566 | |
567 return region_name | |
568 | |
569 def canonical_uri(self, http_request): | |
570 # S3 does **NOT** do path normalization that SigV4 typically does. | |
571 # Urlencode the path, **NOT** ``auth_path`` (because vhosting). | |
572 path = urllib.parse.urlparse(http_request.path) | |
573 # Because some quoting may have already been applied, let's back it out. | |
574 unquoted = urllib.parse.unquote(path.path) | |
575 # Requote, this time addressing all characters. | |
576 encoded = urllib.parse.quote(unquoted) | |
577 return encoded | |
578 | |
579 def canonical_query_string(self, http_request): | |
580 # Note that we just do not return an empty string for | |
581 # POST request. Query strings in url are included in canonical | |
582 # query string. | |
583 l = [] | |
584 for param in sorted(http_request.params): | |
585 value = boto.utils.get_utf8_value(http_request.params[param]) | |
586 l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'), | |
587 urllib.parse.quote(value, safe='-_.~'))) | |
588 return '&'.join(l) | |
589 | |
590 def host_header(self, host, http_request): | |
591 port = http_request.port | |
592 secure = http_request.protocol == 'https' | |
593 if ((port == 80 and not secure) or (port == 443 and secure)): | |
594 return http_request.host | |
595 return '%s:%s' % (http_request.host, port) | |
596 | |
597 def headers_to_sign(self, http_request): | |
598 """ | |
599 Select the headers from the request that need to be included | |
600 in the StringToSign. | |
601 """ | |
602 host_header_value = self.host_header(self.host, http_request) | |
603 headers_to_sign = {'Host': host_header_value} | |
604 for name, value in http_request.headers.items(): | |
605 lname = name.lower() | |
606 # Hooray for the only difference! The main SigV4 signer only does | |
607 # ``Host`` + ``x-amz-*``. But S3 wants pretty much everything | |
608 # signed, except for authorization itself. | |
609 if lname not in ['authorization']: | |
610 headers_to_sign[name] = value | |
611 return headers_to_sign | |
612 | |
613 def determine_region_name(self, host): | |
614 # S3's different format(s) of representing region/service from the | |
615 # rest of AWS makes this hurt too. | |
616 # | |
617 # Possible domain formats: | |
618 # - s3.amazonaws.com (Classic) | |
619 # - s3-us-west-2.amazonaws.com (Specific region) | |
620 # - bukkit.s3.amazonaws.com (Vhosted Classic) | |
621 # - bukkit.s3-ap-northeast-1.amazonaws.com (Vhosted specific region) | |
622 # - s3.cn-north-1.amazonaws.com.cn - (Beijing region) | |
623 # - bukkit.s3.cn-north-1.amazonaws.com.cn - (Vhosted Beijing region) | |
624 parts = self.split_host_parts(host) | |
625 | |
626 if self.region_name is not None: | |
627 region_name = self.region_name | |
628 else: | |
629 # Classic URLs - s3-us-west-2.amazonaws.com | |
630 if len(parts) == 3: | |
631 region_name = self.clean_region_name(parts[0]) | |
632 | |
633 # Special-case for Classic. | |
634 if region_name == 's3': | |
635 region_name = 'us-east-1' | |
636 else: | |
637 # Iterate over the parts in reverse order. | |
638 for offset, part in enumerate(reversed(parts)): | |
639 part = part.lower() | |
640 | |
641 # Look for the first thing starting with 's3'. | |
642 # Until there's a ``.s3`` TLD, we should be OK. :P | |
643 if part == 's3': | |
644 # If it's by itself, the region is the previous part. | |
645 region_name = parts[-offset] | |
646 | |
647 # Unless it's Vhosted classic | |
648 if region_name == 'amazonaws': | |
649 region_name = 'us-east-1' | |
650 | |
651 break | |
652 elif part.startswith('s3-'): | |
653 region_name = self.clean_region_name(part) | |
654 break | |
655 | |
656 return region_name | |
657 | |
658 def determine_service_name(self, host): | |
659 # Should this signing mechanism ever be used for anything else, this | |
660 # will fail. Consider utilizing the logic from the parent class should | |
661 # you find yourself here. | |
662 return 's3' | |
663 | |
664 def mangle_path_and_params(self, req): | |
665 """ | |
666 Returns a copy of the request object with fixed ``auth_path/params`` | |
667 attributes from the original. | |
668 """ | |
669 modified_req = copy.copy(req) | |
670 | |
671 # Unlike the most other services, in S3, ``req.params`` isn't the only | |
672 # source of query string parameters. | |
673 # Because of the ``query_args``, we may already have a query string | |
674 # **ON** the ``path/auth_path``. | |
675 # Rip them apart, so the ``auth_path/params`` can be signed | |
676 # appropriately. | |
677 parsed_path = urllib.parse.urlparse(modified_req.auth_path) | |
678 modified_req.auth_path = parsed_path.path | |
679 | |
680 if modified_req.params is None: | |
681 modified_req.params = {} | |
682 else: | |
683 # To keep the original request object untouched. We must make | |
684 # a copy of the params dictionary. Because the copy of the | |
685 # original request directly refers to the params dictionary | |
686 # of the original request. | |
687 copy_params = req.params.copy() | |
688 modified_req.params = copy_params | |
689 | |
690 raw_qs = parsed_path.query | |
691 existing_qs = urllib.parse.parse_qs( | |
692 raw_qs, | |
693 keep_blank_values=True | |
694 ) | |
695 | |
696 # ``parse_qs`` will return lists. Don't do that unless there's a real, | |
697 # live list provided. | |
698 for key, value in existing_qs.items(): | |
699 if isinstance(value, (list, tuple)): | |
700 if len(value) == 1: | |
701 existing_qs[key] = value[0] | |
702 | |
703 modified_req.params.update(existing_qs) | |
704 return modified_req | |
705 | |
706 def payload(self, http_request): | |
707 if http_request.headers.get('x-amz-content-sha256'): | |
708 return http_request.headers['x-amz-content-sha256'] | |
709 | |
710 return super(S3HmacAuthV4Handler, self).payload(http_request) | |
711 | |
712 def add_auth(self, req, **kwargs): | |
713 if 'x-amz-content-sha256' not in req.headers: | |
714 if '_sha256' in req.headers: | |
715 req.headers['x-amz-content-sha256'] = req.headers.pop('_sha256') | |
716 else: | |
717 req.headers['x-amz-content-sha256'] = self.payload(req) | |
718 updated_req = self.mangle_path_and_params(req) | |
719 return super(S3HmacAuthV4Handler, self).add_auth(updated_req, | |
720 unmangled_req=req, | |
721 **kwargs) | |
722 | |
723 def presign(self, req, expires, iso_date=None): | |
724 """ | |
725 Presign a request using SigV4 query params. Takes in an HTTP request | |
726 and an expiration time in seconds and returns a URL. | |
727 | |
728 http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html | |
729 """ | |
730 if iso_date is None: | |
731 iso_date = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') | |
732 | |
733 region = self.determine_region_name(req.host) | |
734 service = self.determine_service_name(req.host) | |
735 | |
736 params = { | |
737 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', | |
738 'X-Amz-Credential': '%s/%s/%s/%s/aws4_request' % ( | |
739 self._provider.access_key, | |
740 iso_date[:8], | |
741 region, | |
742 service | |
743 ), | |
744 'X-Amz-Date': iso_date, | |
745 'X-Amz-Expires': expires, | |
746 'X-Amz-SignedHeaders': 'host' | |
747 } | |
748 | |
749 if self._provider.security_token: | |
750 params['X-Amz-Security-Token'] = self._provider.security_token | |
751 | |
752 headers_to_sign = self.headers_to_sign(req) | |
753 l = sorted(['%s' % n.lower().strip() for n in headers_to_sign]) | |
754 params['X-Amz-SignedHeaders'] = ';'.join(l) | |
755 | |
756 req.params.update(params) | |
757 | |
758 cr = self.canonical_request(req) | |
759 | |
760 # We need to replace the payload SHA with a constant | |
761 cr = '\n'.join(cr.split('\n')[:-1]) + '\nUNSIGNED-PAYLOAD' | |
762 | |
763 # Date header is expected for string_to_sign, but unused otherwise | |
764 req.headers['X-Amz-Date'] = iso_date | |
765 | |
766 sts = self.string_to_sign(req, cr) | |
767 signature = self.signature(req, sts) | |
768 | |
769 # Add signature to params now that we have it | |
770 req.params['X-Amz-Signature'] = signature | |
771 | |
772 return 'https://%s%s?%s' % (req.host, req.path, | |
773 urllib.parse.urlencode(req.params)) | |
774 | |
775 | |
776 class STSAnonHandler(AuthHandler): | |
777 """ | |
778 Provides pure query construction (no actual signing). | |
779 | |
780 Used for making anonymous STS request for operations like | |
781 ``assume_role_with_web_identity``. | |
782 """ | |
783 | |
784 capability = ['sts-anon'] | |
785 | |
786 def _escape_value(self, value): | |
787 # This is changed from a previous version because this string is | |
788 # being passed to the query string and query strings must | |
789 # be url encoded. In particular STS requires the saml_response to | |
790 # be urlencoded when calling assume_role_with_saml. | |
791 return urllib.parse.quote(value) | |
792 | |
793 def _build_query_string(self, params): | |
794 keys = list(params.keys()) | |
795 keys.sort(key=lambda x: x.lower()) | |
796 pairs = [] | |
797 for key in keys: | |
798 val = boto.utils.get_utf8_value(params[key]) | |
799 pairs.append(key + '=' + self._escape_value(val.decode('utf-8'))) | |
800 return '&'.join(pairs) | |
801 | |
802 def add_auth(self, http_request, **kwargs): | |
803 headers = http_request.headers | |
804 qs = self._build_query_string( | |
805 http_request.params | |
806 ) | |
807 boto.log.debug('query_string in body: %s' % qs) | |
808 headers['Content-Type'] = 'application/x-www-form-urlencoded' | |
809 # This will be a POST so the query string should go into the body | |
810 # as opposed to being in the uri | |
811 http_request.body = qs | |
812 | |
813 | |
814 class QuerySignatureHelper(HmacKeys): | |
815 """ | |
816 Helper for Query signature based Auth handler. | |
817 | |
818 Concrete sub class need to implement _calc_sigature method. | |
819 """ | |
820 | |
821 def add_auth(self, http_request, **kwargs): | |
822 headers = http_request.headers | |
823 params = http_request.params | |
824 params['AWSAccessKeyId'] = self._provider.access_key | |
825 params['SignatureVersion'] = self.SignatureVersion | |
826 params['Timestamp'] = boto.utils.get_ts() | |
827 qs, signature = self._calc_signature( | |
828 http_request.params, http_request.method, | |
829 http_request.auth_path, http_request.host) | |
830 boto.log.debug('query_string: %s Signature: %s' % (qs, signature)) | |
831 if http_request.method == 'POST': | |
832 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' | |
833 http_request.body = qs + '&Signature=' + urllib.parse.quote_plus(signature) | |
834 http_request.headers['Content-Length'] = str(len(http_request.body)) | |
835 else: | |
836 http_request.body = '' | |
837 # if this is a retried request, the qs from the previous try will | |
838 # already be there, we need to get rid of that and rebuild it | |
839 http_request.path = http_request.path.split('?')[0] | |
840 http_request.path = (http_request.path + '?' + qs + | |
841 '&Signature=' + urllib.parse.quote_plus(signature)) | |
842 | |
843 | |
844 class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler): | |
845 """Provides Signature V0 Signing""" | |
846 | |
847 SignatureVersion = 0 | |
848 capability = ['sign-v0'] | |
849 | |
850 def _calc_signature(self, params, *args): | |
851 boto.log.debug('using _calc_signature_0') | |
852 hmac = self._get_hmac() | |
853 s = params['Action'] + params['Timestamp'] | |
854 hmac.update(s.encode('utf-8')) | |
855 keys = params.keys() | |
856 keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) | |
857 pairs = [] | |
858 for key in keys: | |
859 val = boto.utils.get_utf8_value(params[key]) | |
860 pairs.append(key + '=' + urllib.parse.quote(val)) | |
861 qs = '&'.join(pairs) | |
862 return (qs, base64.b64encode(hmac.digest())) | |
863 | |
864 | |
865 class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler): | |
866 """ | |
867 Provides Query Signature V1 Authentication. | |
868 """ | |
869 | |
870 SignatureVersion = 1 | |
871 capability = ['sign-v1', 'mturk'] | |
872 | |
873 def __init__(self, *args, **kw): | |
874 QuerySignatureHelper.__init__(self, *args, **kw) | |
875 AuthHandler.__init__(self, *args, **kw) | |
876 self._hmac_256 = None | |
877 | |
878 def _calc_signature(self, params, *args): | |
879 boto.log.debug('using _calc_signature_1') | |
880 hmac = self._get_hmac() | |
881 keys = params.keys() | |
882 keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) | |
883 pairs = [] | |
884 for key in keys: | |
885 hmac.update(key.encode('utf-8')) | |
886 val = boto.utils.get_utf8_value(params[key]) | |
887 hmac.update(val) | |
888 pairs.append(key + '=' + urllib.parse.quote(val)) | |
889 qs = '&'.join(pairs) | |
890 return (qs, base64.b64encode(hmac.digest())) | |
891 | |
892 | |
893 class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler): | |
894 """Provides Query Signature V2 Authentication.""" | |
895 | |
896 SignatureVersion = 2 | |
897 capability = ['sign-v2', 'ec2', 'ec2', 'emr', 'fps', 'ecs', | |
898 'sdb', 'iam', 'rds', 'sns', 'sqs', 'cloudformation'] | |
899 | |
900 def _calc_signature(self, params, verb, path, server_name): | |
901 boto.log.debug('using _calc_signature_2') | |
902 string_to_sign = '%s\n%s\n%s\n' % (verb, server_name.lower(), path) | |
903 hmac = self._get_hmac() | |
904 params['SignatureMethod'] = self.algorithm() | |
905 if self._provider.security_token: | |
906 params['SecurityToken'] = self._provider.security_token | |
907 keys = sorted(params.keys()) | |
908 pairs = [] | |
909 for key in keys: | |
910 val = boto.utils.get_utf8_value(params[key]) | |
911 pairs.append(urllib.parse.quote(key, safe='') + '=' + | |
912 urllib.parse.quote(val, safe='-_~')) | |
913 qs = '&'.join(pairs) | |
914 boto.log.debug('query string: %s' % qs) | |
915 string_to_sign += qs | |
916 boto.log.debug('string_to_sign: %s' % string_to_sign) | |
917 hmac.update(string_to_sign.encode('utf-8')) | |
918 b64 = base64.b64encode(hmac.digest()) | |
919 boto.log.debug('len(b64)=%d' % len(b64)) | |
920 boto.log.debug('base64 encoded digest: %s' % b64) | |
921 return (qs, b64) | |
922 | |
923 | |
924 class POSTPathQSV2AuthHandler(QuerySignatureV2AuthHandler, AuthHandler): | |
925 """ | |
926 Query Signature V2 Authentication relocating signed query | |
927 into the path and allowing POST requests with Content-Types. | |
928 """ | |
929 | |
930 capability = ['mws'] | |
931 | |
932 def add_auth(self, req, **kwargs): | |
933 req.params['AWSAccessKeyId'] = self._provider.access_key | |
934 req.params['SignatureVersion'] = self.SignatureVersion | |
935 req.params['Timestamp'] = boto.utils.get_ts() | |
936 qs, signature = self._calc_signature(req.params, req.method, | |
937 req.auth_path, req.host) | |
938 boto.log.debug('query_string: %s Signature: %s' % (qs, signature)) | |
939 if req.method == 'POST': | |
940 req.headers['Content-Length'] = str(len(req.body)) | |
941 req.headers['Content-Type'] = req.headers.get('Content-Type', | |
942 'text/plain') | |
943 else: | |
944 req.body = '' | |
945 # if this is a retried req, the qs from the previous try will | |
946 # already be there, we need to get rid of that and rebuild it | |
947 req.path = req.path.split('?')[0] | |
948 req.path = (req.path + '?' + qs + | |
949 '&Signature=' + urllib.parse.quote_plus(signature)) | |
950 | |
951 | |
952 def get_auth_handler(host, config, provider, requested_capability=None): | |
953 """Finds an AuthHandler that is ready to authenticate. | |
954 | |
955 Lists through all the registered AuthHandlers to find one that is willing | |
956 to handle for the requested capabilities, config and provider. | |
957 | |
958 :type host: string | |
959 :param host: The name of the host | |
960 | |
961 :type config: | |
962 :param config: | |
963 | |
964 :type provider: | |
965 :param provider: | |
966 | |
967 Returns: | |
968 An implementation of AuthHandler. | |
969 | |
970 Raises: | |
971 boto.exception.NoAuthHandlerFound | |
972 """ | |
973 ready_handlers = [] | |
974 auth_handlers = boto.plugin.get_plugin(AuthHandler, requested_capability) | |
975 for handler in auth_handlers: | |
976 try: | |
977 ready_handlers.append(handler(host, config, provider)) | |
978 except boto.auth_handler.NotReadyToAuthenticate: | |
979 pass | |
980 | |
981 if not ready_handlers: | |
982 checked_handlers = auth_handlers | |
983 names = [handler.__name__ for handler in checked_handlers] | |
984 raise boto.exception.NoAuthHandlerFound( | |
985 'No handler was ready to authenticate. %d handlers were checked.' | |
986 ' %s ' | |
987 'Check your credentials' % (len(names), str(names))) | |
988 | |
989 # We select the last ready auth handler that was loaded, to allow users to | |
990 # customize how auth works in environments where there are shared boto | |
991 # config files (e.g., /etc/boto.cfg and ~/.boto): The more general, | |
992 # system-wide shared configs should be loaded first, and the user's | |
993 # customizations loaded last. That way, for example, the system-wide | |
994 # config might include a plugin_directory that includes a service account | |
995 # auth plugin shared by all users of a Google Compute Engine instance | |
996 # (allowing sharing of non-user data between various services), and the | |
997 # user could override this with a .boto config that includes user-specific | |
998 # credentials (for access to user data). | |
999 return ready_handlers[-1] | |
1000 | |
1001 | |
1002 def detect_potential_sigv4(func): | |
1003 def _wrapper(self): | |
1004 if os.environ.get('EC2_USE_SIGV4', False): | |
1005 return ['hmac-v4'] | |
1006 | |
1007 if boto.config.get('ec2', 'use-sigv4', False): | |
1008 return ['hmac-v4'] | |
1009 | |
1010 if hasattr(self, 'region'): | |
1011 # If you're making changes here, you should also check | |
1012 # ``boto/iam/connection.py``, as several things there are also | |
1013 # endpoint-related. | |
1014 if getattr(self.region, 'endpoint', ''): | |
1015 for test in SIGV4_DETECT: | |
1016 if test in self.region.endpoint: | |
1017 return ['hmac-v4'] | |
1018 | |
1019 return func(self) | |
1020 return _wrapper | |
1021 | |
1022 | |
1023 def detect_potential_s3sigv4(func): | |
1024 def _wrapper(self): | |
1025 if os.environ.get('S3_USE_SIGV4', False): | |
1026 return ['hmac-v4-s3'] | |
1027 | |
1028 if boto.config.get('s3', 'use-sigv4', False): | |
1029 return ['hmac-v4-s3'] | |
1030 | |
1031 if hasattr(self, 'host'): | |
1032 # If you're making changes here, you should also check | |
1033 # ``boto/iam/connection.py``, as several things there are also | |
1034 # endpoint-related. | |
1035 for test in SIGV4_DETECT: | |
1036 if test in self.host: | |
1037 return ['hmac-v4-s3'] | |
1038 | |
1039 return func(self) | |
1040 return _wrapper |