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