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"""
26Handles authentication required to AWS and GS
27"""
28
29import base64
30import boto
31import boto.auth_handler
32import boto.exception
33import boto.plugin
34import boto.utils
35import copy
36import datetime
37from email.utils import formatdate
38import hmac
39import os
40import posixpath
41
42from boto.compat import urllib, encodebytes
43from boto.auth_handler import AuthHandler
44from boto.exception import BotoClientError
45
46try:
47    from hashlib import sha1 as sha
48    from hashlib import sha256 as sha256
49except ImportError:
50    import sha
51    sha256 = None
52
53
54# Region detection strings to determine if SigV4 should be used
55# by default.
56SIGV4_DETECT = [
57    '.cn-',
58    # In eu-central we support both host styles for S3
59    '.eu-central',
60    '-eu-central',
61]
62
63
64class 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
113class 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
127class 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
162class 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
192class 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
216class 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
291class 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
551class 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
776class 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
814class 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
844class 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
865class 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
893class 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
924class 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
952def 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
1002def 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
1023def 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
1041