comparison env/lib/python3.7/site-packages/boto/auth.py @ 5:9b1c78e6ba9c draft default tip

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